Coverage for python/lsst/obs/lsst/translators/lsst.py : 21%

Hot-keys 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
# This file is currently part of obs_lsst but is written to allow it # to be migrated to the astro_metadata_translator package at a later date. # # This product includes software developed by the LSST Project # (http://www.lsst.org). # See the LICENSE file in this directory for details of code ownership. # # Use of this source code is governed by a 3-clause BSD-style # license that can be found in the LICENSE file.
"compute_detector_exposure_id_generic", "LsstBaseTranslator")
# LSST day clock starts at UTC+8
# LSST Default location in the absence of headers
"""Read a camera policy file and retrieve the mapping from CCD name to ID.
Parameters ---------- policyFile : `str` Name of YAML policy file to read, relative to the obs_lsst package.
Returns ------- mapping : `dict` of `str` to (`int`, `str`) A `dict` with keys being the full names of the detectors, and the value is a `tuple` containing the integer detector number and the detector serial number.
Notes ----- Reads the camera YAML definition file directly and extracts just the IDs and serials. This routine does not use the standard `~lsst.obs.base.yamlCamera.YAMLCamera` infrastructure or `lsst.afw.cameraGeom`. This is because the translators are intended to have minimal dependencies on LSST infrastructure. """
file = os.path.join(obs_lsst_packageDir, policyFile) try: with open(file) as fh: # Use the fast parser since these files are large camera = yaml.load(fh, Loader=yaml.CSafeLoader) except OSError as e: raise ValueError(f"Could not load camera policy file {file}") from e
mapping = {} for ccd, value in camera["CCDs"].items(): mapping[ccd] = (int(value["id"]), value["serial"])
return mapping
"""Compute the detector_exposure_id from the exposure id and the detector number.
Parameters ---------- exposure_id : `int` The exposure ID. detector_num : `int` The detector number. max_num : `int`, optional Maximum number of detectors to make space for. Defaults to 1000. mode : `str`, optional Computation mode. Defaults to "concat". - concat : Concatenate the exposure ID and detector number, making sure that there is space for max_num and zero padding. - multiply : Multiply the exposure ID by the maximum detector number and add the detector number.
Returns ------- detector_exposure_id : `int` Computed ID.
Raises ------ ValueError The detector number is out of range. """
if detector_num is None: raise ValueError("Detector number must be defined.") if detector_num >= max_num or detector_num < 0: raise ValueError(f"Detector number out of range 0 <= {detector_num} <= {max_num}")
if mode == "concat": npad = len(str(max_num)) return int(f"{exposure_id}{detector_num:0{npad}d}") elif mode == "multiply": return max_num*exposure_id + detector_num else: raise ValueError(f"Computation mode of '{mode}' is not understood")
"""Translation methods useful for all LSST-style headers."""
# Do not specify a name for this translator """Path to policy file relative to obs_lsst root."""
"""Mapping of detector name to detector number and serial."""
"""Mapping of detector serial number to raft, number, and name."""
"""Search paths to use for LSST data when looking for header correction files.
Returns ------- path : `list` List with a single element containing the full path to the ``corrections`` directory within the ``obs_lsst`` package. """ return [os.path.join(obs_lsst_packageDir, "corrections")]
def compute_detector_exposure_id(exposure_id, detector_num): """Compute the detector exposure ID from detector number and exposure ID.
This is a helper method to allow code working outside the translator infrastructure to use the same algorithm.
Parameters ---------- exposure_id : `int` Unique exposure ID. detector_num : `int` Detector number.
Returns ------- detector_exposure_id : `int` The calculated ID. """ return compute_detector_exposure_id_generic(exposure_id, detector_num, max_num=999, mode="concat")
def detector_mapping(cls): """Returns the mapping of full name to detector ID and serial.
Returns ------- mapping : `dict` of `str`:`tuple` Returns the mapping of full detector name (group+detector) to detector number and serial.
Raises ------ ValueError Raised if no camera policy file has been registered with this translation class.
Notes ----- Will construct the mapping if none has previously been constructed. """ if cls.cameraPolicyFile is not None: if cls.detectorMapping is None: cls.detectorMapping = read_detector_ids(cls.cameraPolicyFile) else: raise ValueError(f"Translation class '{cls.__name__}' has no registered camera policy file")
return cls.detectorMapping
def detector_serials(cls): """Obtain the mapping of detector serial to detector group, name, and number.
Returns ------- info : `dict` of `tuple` of (`str`, `str`, `int`) A `dict` with the serial numbers as keys and values of detector group, name, and number. """ if cls.detectorSerials is None: detector_mapping = cls.detector_mapping()
if detector_mapping is not None: # Form mapping to go from serial number to names/numbers serials = {} for fullname, (id, serial) in cls.detectorMapping.items(): raft, detector_name = fullname.split("_") if serial in serials: raise RuntimeError(f"Serial {serial} is defined in multiple places") serials[serial] = (raft, detector_name, id) cls.detectorSerials = serials else: raise RuntimeError("Unable to obtain detector mapping information")
return cls.detectorSerials
def compute_detector_num_from_name(cls, detector_group, detector_name): """Helper method to return the detector number from the name.
Parameters ---------- detector_group : `str` Name of the detector grouping. This is generally the raft name. detector_name : `str` Detector name.
Returns ------- num : `int` Detector number. """ fullname = f"{detector_group}_{detector_name}"
num = None detector_mapping = cls.detector_mapping() if detector_mapping is None: raise RuntimeError("Unable to obtain detector mapping information")
if fullname in detector_mapping: num = detector_mapping[fullname] else: log.warning(f"Unable to determine detector number from detector name {fullname}") return None
return num[0]
def compute_detector_info_from_serial(cls, detector_serial): """Helper method to return the detector information from the serial.
Parameters ---------- detector_serial : `str` Detector serial ID.
Returns ------- info : `tuple` of (`str`, `str`, `int`) Detector group, name, and number. """ serial_mapping = cls.detector_serials() if serial_mapping is None: raise RuntimeError("Unable to obtain serial mapping information")
if detector_serial in serial_mapping: info = serial_mapping[detector_serial] else: raise RuntimeError("Unable to determine detector information from detector serial" f" {detector_serial}")
return info
def compute_exposure_id(dayobs, seqnum): """Helper method to calculate the AuxTel exposure_id.
Parameters ---------- dayobs : `str` Day of observation in either YYYYMMDD or YYYY-MM-DD format. seqnum : `int` or `str` Sequence number.
Returns ------- exposure_id : `int` Exposure ID in form YYYYMMDDnnnnn form. """ dayobs = dayobs.replace("-", "")
if len(dayobs) != 8: raise ValueError(f"Malformed dayobs: {dayobs}")
# Expect no more than 99,999 exposures in a day maxdigits = 5 if seqnum >= 10**maxdigits: raise ValueError(f"Sequence number ({seqnum}) exceeds limit")
# Form the number as a string zero padding the sequence number idstr = f"{dayobs}{seqnum:0{maxdigits}d}" return int(idstr)
"""Indicate whether these data are coming from the instrument installed on the mountain.
Returns ------- is : `bool` `True` if instrument is on the mountain. """ if "TSTAND" in self._header: return False return True
def to_location(self): # Docstring will be inherited. Property defined in properties.py location = None if not self._is_on_mountain(): return location try: # Try standard FITS headers return super().to_location() except KeyError: return LSST_LOCATION
def to_datetime_begin(self): # Docstring will be inherited. Property defined in properties.py self._used_these_cards("MJD-OBS") return Time(self._header["MJD-OBS"], scale="tai", format="mjd")
def to_datetime_end(self): # Docstring will be inherited. Property defined in properties.py if self.is_key_ok("DATE-END"): return super().to_datetime_end()
return self.to_datetime_begin() + self.to_exposure_time()
def to_detector_num(self): # Docstring will be inherited. Property defined in properties.py raft = self.to_detector_group() detector = self.to_detector_name() return self.compute_detector_num_from_name(raft, detector)
def to_detector_exposure_id(self): # Docstring will be inherited. Property defined in properties.py exposure_id = self.to_exposure_id() num = self.to_detector_num() return self.compute_detector_exposure_id(exposure_id, num)
def to_observation_type(self): # Docstring will be inherited. Property defined in properties.py obstype = self._header["IMGTYPE"] self._used_these_cards("IMGTYPE") obstype = obstype.lower() if obstype == "skyexp": obstype = "science" return obstype
def to_dark_time(self): """Calculate the dark time.
If a DARKTIME header is not found, the value is assumed to be identical to the exposure time.
Returns ------- dark : `astropy.units.Quantity` The dark time in seconds. """ if self.is_key_ok("DARKTIME"): darktime = self._header["DARKTIME"]*u.s self._used_these_cards("DARKTIME") else: log.warning("Unable to determine dark time. Setting from exposure time.") darktime = self.to_exposure_time() return darktime
def to_exposure_id(self): """Generate a unique exposure ID number
This is a combination of DAYOBS and SEQNUM.
Returns ------- exposure_id : `int` Unique exposure number. """ if "CALIB_ID" in self._header: self._used_these_cards("CALIB_ID") return None
dayobs = self._header["DAYOBS"] seqnum = self._header["SEQNUM"] self._used_these_cards("DAYOBS", "SEQNUM")
return self.compute_exposure_id(dayobs, seqnum)
# For now "visits" are defined to be identical to exposures.
def to_physical_filter(self): """Calculate the physical filter name.
Returns ------- filter : `str` Name of filter. Can be a combination of FILTER, FILTER1 and FILTER2 headers joined by a "+". Returns "NONE" if no filter is declared. """ joined = self._join_keyword_values(["FILTER", "FILTER1", "FILTER2"], delim="+") if not joined: joined = "NONE"
return joined |