From 3c8ad409a1ba4173fa3b22dda6d5ee0adacfc6f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Thu, 18 Jan 2024 11:13:11 -0800 Subject: [PATCH 01/42] Python code for the package, transferred from lsst-dm. --- .../lsst/obs/fiberspectrograph/_instrument.py | 77 +++++++ python/lsst/obs/fiberspectrograph/filters.py | 5 + python/lsst/obs/fiberspectrograph/isrTask.py | 117 ++++++++++ .../obs/fiberspectrograph/rawFormatter.py | 43 ++++ python/lsst/obs/fiberspectrograph/spectrum.py | 198 ++++++++++++++++ .../lsst/obs/fiberspectrograph/translator.py | 214 ++++++++++++++++++ 6 files changed, 654 insertions(+) create mode 100644 python/lsst/obs/fiberspectrograph/_instrument.py create mode 100644 python/lsst/obs/fiberspectrograph/filters.py create mode 100644 python/lsst/obs/fiberspectrograph/isrTask.py create mode 100644 python/lsst/obs/fiberspectrograph/rawFormatter.py create mode 100644 python/lsst/obs/fiberspectrograph/spectrum.py create mode 100644 python/lsst/obs/fiberspectrograph/translator.py diff --git a/python/lsst/obs/fiberspectrograph/_instrument.py b/python/lsst/obs/fiberspectrograph/_instrument.py new file mode 100644 index 0000000..26a9dbc --- /dev/null +++ b/python/lsst/obs/fiberspectrograph/_instrument.py @@ -0,0 +1,77 @@ +# This file is part of obs_fiberSpectrograph +# +# Developed for the LSST Data Management System. +# This product includes software developed by the LSST Project +# (http://www.lsst.org). +# See the COPYRIGHT file at the top-level directory of this distribution +# for details of code ownership. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__all__ = ("FiberSpectrograph", ) + +import os.path + +import lsst.obs.base.yamlCamera as yamlCamera +from lsst.utils import getPackageDir +from lsst.obs.base import VisitSystem +from lsst.obs.lsst import LsstCam +from .filters import FIBER_SPECTROGRAPH_FILTER_DEFINITIONS +from .translator import FiberSpectrographTranslator + +PACKAGE_DIR = getPackageDir("obs_fiberSpectrograph") + + +class FiberSpectrograph(LsstCam): + """Gen3 instrument for the Rubin fiber spectrographs + + Parameters + ---------- + camera : `lsst.cameraGeom.Camera` + Camera object from which to extract detector information. + filters : `list` of `FilterDefinition` + An ordered list of filters to define the set of PhysicalFilters + associated with this instrument in the registry. + """ + filterDefinitions = FIBER_SPECTROGRAPH_FILTER_DEFINITIONS + instrument = "FiberSpec" + policyName = "fiberSpectrograph" + translatorClass = FiberSpectrographTranslator + visitSystem = VisitSystem.BY_SEQ_START_END + raw_definition = ("rawSpectrum", + ("instrument", "physical_filter", "exposure", "detector"), + "FiberSpectrum") + + @classmethod + def getCamera(cls): + # Constructing a YAML camera takes a long time but we rely on + # yamlCamera to cache for us. + # N.b. can't inherit as PACKAGE_DIR isn't in the class + cameraYamlFile = os.path.join(PACKAGE_DIR, "policy", f"{cls.policyName}.yaml") + return yamlCamera.makeCamera(cameraYamlFile) + + def getRawFormatter(self, dataId): + # Docstring inherited from Instrument.getRawFormatter + # local import to prevent circular dependency + from .rawFormatter import FiberSpectrographRawFormatter + return FiberSpectrographRawFormatter + + def extractDetectorRecord(self, camGeomDetector): + """Create a Gen3 Detector entry dict from a cameraGeom.Detector. + """ + return dict( + instrument=self.getName(), + id=camGeomDetector.getId(), + full_name=camGeomDetector.getName(), + ) diff --git a/python/lsst/obs/fiberspectrograph/filters.py b/python/lsst/obs/fiberspectrograph/filters.py new file mode 100644 index 0000000..3a805cd --- /dev/null +++ b/python/lsst/obs/fiberspectrograph/filters.py @@ -0,0 +1,5 @@ +from lsst.obs.base import FilterDefinition, FilterDefinitionCollection + +FIBER_SPECTROGRAPH_FILTER_DEFINITIONS = FilterDefinitionCollection( + FilterDefinition(band="empty", physical_filter="empty"), +) diff --git a/python/lsst/obs/fiberspectrograph/isrTask.py b/python/lsst/obs/fiberspectrograph/isrTask.py new file mode 100644 index 0000000..36d09d5 --- /dev/null +++ b/python/lsst/obs/fiberspectrograph/isrTask.py @@ -0,0 +1,117 @@ +# This file is part of obs_fiberSpectrograph +# +# Developed for the LSST Data Management System. +# This product includes software developed by the LSST Project +# (https://www.lsst.org). +# See the COPYRIGHT file at the top-level directory of this distribution +# for details of code ownership. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__all__ = ["IsrTask", "IsrTaskConfig"] + +import numpy as np + +import lsst.geom +import lsst.pex.config as pexConfig +import lsst.pipe.base.connectionTypes as cT + +import lsst.ip.isr + + +class IsrTaskConnections(lsst.ip.isr.isrTask.IsrTaskConnections): + ccdExposure = cT.Input( + name="rawSpectrum", + doc="Input spectrum to process.", + storageClass="FiberSpectrum", + dimensions=["instrument", "exposure", "detector"], + ) + bias = cT.PrerequisiteInput( + name="bias", + doc="Input bias calibration.", + storageClass="FiberSpectrum", + dimensions=["instrument", "detector"], + isCalibration=True, + ) + outputExposure = cT.Output( + name="spectrum", + doc="Corrected spectrum.", + storageClass="FiberSpectrum", + dimensions=["instrument", "exposure", "detector"], + ) + + def __init__(self, *, config=None): + super().__init__(config=config) + + +class IsrTaskConfig(lsst.ip.isr.IsrTaskConfig, pipelineConnections=IsrTaskConnections): + """Configuration parameters for IsrTask. + + Items are grouped in the order in which they are executed by the task. + """ + datasetType = pexConfig.Field( + dtype=str, + doc="Dataset type for input data; users will typically leave this alone, " + "but camera-specific ISR tasks will override it", + default="rawSpectrum", + ) + + +class IsrTask(lsst.ip.isr.IsrTask): + """Apply common instrument signature correction algorithms to a raw frame. + + The process for correcting imaging data is very similar from + camera to camera. This task provides a vanilla implementation of + doing these corrections, including the ability to turn certain + corrections off if they are not needed. The inputs to the primary + method, `run()`, are a raw exposure to be corrected and the + calibration data products. The raw input is a single chip sized + mosaic of all amps including overscans and other non-science + pixels. + + The __init__ method sets up the subtasks for ISR processing, using + the defaults from `lsst.ip.isr`. + + Parameters + ---------- + args : `list` + Positional arguments passed to the Task constructor. + None used at this time. + kwargs : `dict`, optional + Keyword arguments passed on to the Task constructor. + None used at this time. + """ + ConfigClass = IsrTaskConfig + _DefaultName = "isr" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + + def ensureExposure(self, inputExposure, *args, **kwargs): + return inputExposure + + def convertIntToFloat(self, ccdExposure): + return ccdExposure + + def maskAmplifier(self, ccdExposure, amp, defects): + flux = ccdExposure.flux + + saturated = flux > amp.getSaturation() + flux[saturated] = np.NaN + ccdExposure.mask[saturated] |= ccdExposure.getPlaneBitMask(self.config.saturatedMaskName) + + return False + + def roughZeroPoint(self, exposure): + pass diff --git a/python/lsst/obs/fiberspectrograph/rawFormatter.py b/python/lsst/obs/fiberspectrograph/rawFormatter.py new file mode 100644 index 0000000..e6383a3 --- /dev/null +++ b/python/lsst/obs/fiberspectrograph/rawFormatter.py @@ -0,0 +1,43 @@ +__all__ = [] + +from lsst.obs.base import FitsRawFormatterBase +from .filters import FIBER_SPECTROGRAPH_FILTER_DEFINITIONS +from ._instrument import FiberSpectrograph +from .translator import FiberSpectrographTranslator +import fitsio +import astropy.units as u +from lsst.daf.base import PropertyList + + +class FiberSpectrographRawFormatter(FitsRawFormatterBase): + cameraClass = FiberSpectrograph + translatorClass = FiberSpectrographTranslator + filterDefinitions = FIBER_SPECTROGRAPH_FILTER_DEFINITIONS + + def getDetector(self, id): + return self.cameraClass().getCamera()[id] + + def read(self, component=None): + """Read just the image component of the Exposure. + + Returns + ------- + image : `~lsst.afw.image.Image` + In-memory image component. + """ + pytype = self.fileDescriptor.storageClass.pytype + path = self.fileDescriptor.location.path + + sourceMd = dict(fitsio.read_header(path)) + md = PropertyList() + md.update(sourceMd) + if component is not None: + if component == 'metadata': + return md + + flux = fitsio.read(path) + wavelength = fitsio.read(path, ext=md["PS1_0"], columns=md["PS1_1"]).flatten() + + wavelength = u.Quantity(wavelength, u.Unit(md["CUNIT1"]), copy=False) + + return pytype(wavelength, flux, md) diff --git a/python/lsst/obs/fiberspectrograph/spectrum.py b/python/lsst/obs/fiberspectrograph/spectrum.py new file mode 100644 index 0000000..1addff1 --- /dev/null +++ b/python/lsst/obs/fiberspectrograph/spectrum.py @@ -0,0 +1,198 @@ +# This file is part of obs_fiberSpectrograph +# +# Developed for the LSST Data Management System. +# This product includes software developed by the LSST Project +# (http://www.lsst.org). +# See the COPYRIGHT file at the top-level directory of this distribution +# for details of code ownership. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +__all__ = ("FiberSpectrum",) + +import numpy as np +import astropy.io.fits +import fitsio +import astropy.units as u +from ._instrument import FiberSpectrograph +import lsst.afw.image as afwImage + + +class VisitInfo: + def __init__(self, md): + self.exposureTime = md["EXPTIME"] + + def getExposureTime(self): + return self.exposureTime + + +class Info: + def __init__(self, md): + self.visitInfo = VisitInfo(md) + + def getVisitInfo(self): + return self.visitInfo + + +class FiberSpectrum: + def __init__(self, wavelength, flux, md=None): + self.wavelength = wavelength + self.flux = flux + self.md = md + + self.info = Info(md) + self.detector = FiberSpectrograph().getCamera()[0] + + self.__Mask = afwImage.MaskX(1, 1) + self.getPlaneBitMask = self.__Mask.getPlaneBitMask # ughh, awful Mask API + self.mask = np.zeros(flux.shape, dtype=self.__Mask.array.dtype) + self.variance = np.zeros_like(flux) + + def getDetector(self): + return self.detector + + def getInfo(self): + return self.info + + def getMetadata(self): + return self.md + + def getFilter(self): + return FiberSpectrograph.filterDefinitions[0].makeFilterLabel() + + def getBBox(self): + return self.detector.getBBox() + + @staticmethod + def readFits(path): + """Read a Spectrum from disk" + + Parameters + ---------- + path : `str` + The file to read + + Returns + ------- + spectrum : `~lsst.obs.fiberSpectrograph.FiberSpectrum` + In-memory spectrum. + """ + md = dict(fitsio.read_header(path)) + flux = fitsio.read(path) + wavelength = fitsio.read(path, ext=md["PS1_0"], columns=md["PS1_1"]).flatten() + + wavelength = u.Quantity(wavelength, u.Unit(md["CUNIT1"]), copy=False) + + return FiberSpectrum(wavelength, flux, md) + + def writeFits(self, path): + """Write a Spectrum to disk + + Parameters + ---------- + path : `str` + The file to write + """ + hdl = DataManager(self).make_hdulist() + + hdl.writeto(path) + + +class DataManager: + """A data packager for `Spectrum` objects + """ + + wcs_table_name = "WCS-TAB" + """Name of the table containing the wavelength WCS (EXTNAME).""" + wcs_table_ver = 1 + """WCS table version (EXTVER).""" + wcs_column_name = "wavelength" + """Name of the table column containing the wavelength information.""" + + # The version of the FITS file format produced by this class. + FORMAT_VERSION = 1 + + def __init__(self, spectrum): + self.spectrum = spectrum + + def make_hdulist(self): + """Generate a FITS hdulist built from SpectrographData. + Parameters + ---------- + spec : `SpectrographData` + The data from which to build the FITS hdulist. + Returns + ------- + hdulist : `astropy.io.fits.HDUList` + The FITS hdulist. + """ + hdu1 = self.make_primary_hdu() + hdu2 = self.make_wavelength_hdu() + return astropy.io.fits.HDUList([hdu1, hdu2]) + + def make_fits_header(self): + """Return a FITS header built from a Spectrum""" + hdr = astropy.io.fits.Header() + + hdr["FORMAT_V"] = self.FORMAT_VERSION + for k, v in self.spectrum.md.items(): + hdr[k] = v + + # WCS headers - Use -TAB WCS definition + wcs_cards = [ + "WCSAXES = 1 / Number of WCS axes", + "CRPIX1 = 0.0 / Reference pixel on axis 1", + "CRVAL1 = 0.0 / Value at ref. pixel on axis 1", + "CNAME1 = 'Wavelength' / Axis name for labeling purposes", + "CTYPE1 = 'WAVE-TAB' / Wavelength axis by lookup table", + "CDELT1 = 1.0 / Pixel size on axis 1", + f"CUNIT1 = '{self.spectrum.wavelength.unit.name:8s}' / Units for axis 1", + f"PV1_1 = {self.wcs_table_ver:20d} / EXTVER of bintable extension for -TAB arrays", + f"PS1_0 = '{self.wcs_table_name:8s}' / EXTNAME of bintable extension for -TAB arrays", + f"PS1_1 = '{self.wcs_column_name:8s}' / Wavelength coordinate array", + ] + for c in wcs_cards: + hdr.append(astropy.io.fits.Card.fromstring(c)) + + return hdr + + def make_primary_hdu(self): + """Return the primary HDU built from a Spectrum.""" + + hdu = astropy.io.fits.PrimaryHDU( + data=self.spectrum.flux, header=self.make_fits_header() + ) + return hdu + + def make_wavelength_hdu(self): + """Return the wavelength HDU built from a Spectrum.""" + + # The wavelength array must be 2D (N, 1) in numpy but (1, N) in FITS + wavelength = self.spectrum.wavelength.reshape([self.spectrum.wavelength.size, 1]) + + # Create a Table. It will be a single element table + table = astropy.table.Table() + + # Create the wavelength column + # Create the column explicitly since it is easier to ensure the + # shape this way. + wavecol = astropy.table.Column([wavelength], unit=wavelength.unit.name) + + # The column name must match the PS1_1 entry from the primary HDU + table[self.wcs_column_name] = wavecol + + # The name MUST match the value of PS1_0 and the version MUST + # match the value of PV1_1 + hdu = astropy.io.fits.BinTableHDU(table, name=self.wcs_table_name, ver=1) + return hdu diff --git a/python/lsst/obs/fiberspectrograph/translator.py b/python/lsst/obs/fiberspectrograph/translator.py new file mode 100644 index 0000000..e1b8a4b --- /dev/null +++ b/python/lsst/obs/fiberspectrograph/translator.py @@ -0,0 +1,214 @@ +import logging +import os + +import astropy.units as u +from astropy.time import Time + +from astro_metadata_translator import cache_translation +from lsst.obs.lsst.translators.lsst import SIMONYI_TELESCOPE, LsstBaseTranslator + +from lsst.utils import getPackageDir + +__all__ = ["FiberSpectrographTranslator", ] + +log = logging.getLogger(__name__) + + +class FiberSpectrographTranslator(LsstBaseTranslator): + """Metadata translator for Rubin calibration fibre spectrographs headers""" + + name = "FiberSpectrograph" + """Name of this translation class""" + + supported_instrument = "FiberSpec" + """Supports the Rubin calibration fibre spectrographs.""" + + default_search_path = os.path.join(getPackageDir("obs_fiberSpectrograph"), "corrections") + """Default search path to use to locate header correction files.""" + + default_resource_root = os.path.join(getPackageDir("obs_fiberSpectrograph"), "corrections") + """Default resource path root to use to locate header correction files.""" + + DETECTOR_MAX = 1 + + _const_map = { + "detector_num": 0, + "detector_name": "0", + "exposure_group": None, + "object": None, + "physical_filter": "empty", + "detector_serial": "0xdeadbeef", + "detector_group": "None", + "relative_humidity": None, + "pressure": None, + "temperature": None, + "focus_z": None, + "boresight_airmass": None, + "boresight_rotation_angle": None, + "tracking_radec": None, + "telescope": SIMONYI_TELESCOPE, + "observation_type": "spectrum", # IMGTYPE is '' + } + """Constant mappings""" + + _trivial_map = { + "observation_id": "OBSID", + "science_program": ("PROGRAM", dict(default="unknown")), + } + """One-to-one mappings""" + + @classmethod + def can_translate(cls, header, filename=None): + """Indicate whether this translation class can translate the + supplied header. + + Parameters + ---------- + header : `dict`-like + Header to convert to standardized form. + filename : `str`, optional + Name of file being translated. + + Returns + ------- + can : `bool` + `True` if the header is recognized by this class. `False` + otherwise. + """ + + return "INSTRUME" in header and header["INSTRUME"] in ["FiberSpectrograph.Broad"] + + @cache_translation + def to_instrument(self): + return "FiberSpec" + + @cache_translation + def to_datetime_begin(self): + self._used_these_cards("DATE-BEG") + return Time(self._header["DATE-BEG"], scale="tai", format="isot") + + @cache_translation + def to_observing_day(self): + """Return the day of observation as YYYYMMDD integer. + + Returns + ------- + obs_day : `int` + The day of observation. + """ + date = self.to_datetime_begin() + date -= self._ROLLOVER_TIME + return int(date.strftime("%Y%m%d")) + + @cache_translation + def to_observation_counter(self): + """Return the sequence number within the observing day. + + Returns + ------- + counter : `int` + The sequence number for this day. + """ + if self.is_key_ok("OBSID"): + self._used_these_cards("OBSID") + return int(self._header["OBSID"]) + + # This indicates a problem so we warn and return a 0 + log.warning("%s: Unable to determine the observation counter so returning 0", + self._log_prefix) + return 0 + + @cache_translation + def to_exposure_time(self): + # Docstring will be inherited. Property defined in properties.py + # Some data is missing a value for EXPTIME. + # Have to be careful we do not have circular logic when trying to + # guess + if self.is_key_ok("EXPTIME"): + return self.quantity_from_card("EXPTIME", u.s) + + # A missing or undefined EXPTIME is problematic. Set to -1 + # to indicate that none was found. + log.warning("%s: Insufficient information to derive exposure time. Setting to -1.0s", + self._log_prefix) + return -1.0 * u.s + + @cache_translation + def to_dark_time(self): # N.b. defining this suppresses a warning re setting from exptime + if "DARKTIME" in self._header: + darkTime = self._header["DARKTIME"] + self._used_these_cards("DARKTIME") + return (darkTime, dict(unit=u.s)) + return self.to_exposure_time() + + @staticmethod + def compute_exposure_id(dayobs, seqnum, controller=None): + """Helper method to calculate the exposure_id. + + Parameters + ---------- + dayobs : `str` + Day of observation in either YYYYMMDD or YYYY-MM-DD format. + If the string looks like ISO format it will be truncated before the + ``T`` before being handled. + seqnum : `int` or `str` + Sequence number. + controller : `str`, optional + Controller to use. If this is "O", no change is made to the + exposure ID. If it is "C" a 1000 is added to the year component + of the exposure ID. If it is "H" a 2000 is added to the year + component. This sequence continues with "P" and "Q" controllers. + `None` indicates that the controller is not relevant to the + exposure ID calculation (generally this is the case for test + stand data). + + Returns + ------- + exposure_id : `int` + Exposure ID in form YYYYMMDDnnnnn form. + """ + if not isinstance(dayobs, int): + if "T" in dayobs: + dayobs = dayobs[:dayobs.find("T")] + + 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}" + + # Exposure ID has to be an integer + return int(idstr) + + @cache_translation + def to_visit_id(self): + """Calculate the visit associated with this exposure. + """ + return None + + @cache_translation + 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.to_observing_day() + seqnum = self.to_observation_counter() + + return self.compute_exposure_id(dayobs, seqnum) From 3b4a2aed2c41e686b620f717ff95f49b9af6dce5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Thu, 18 Jan 2024 11:16:10 -0800 Subject: [PATCH 02/42] Add policy, transferred from lsst-dm. --- policy/fiberSpectrograph.yaml | 80 +++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 policy/fiberSpectrograph.yaml diff --git a/policy/fiberSpectrograph.yaml b/policy/fiberSpectrograph.yaml new file mode 100644 index 0000000..ef45149 --- /dev/null +++ b/policy/fiberSpectrograph.yaml @@ -0,0 +1,80 @@ +# +# LSST Data Management System +# Copyright 2017 LSST Corporation. +# +# This product includes software developed by the +# LSST Project (http://www.lsst.org/). +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the LSST License Statement and +# the GNU General Public License along with this program. If not, +# see . +# +# + +name : "FiberSpectrograph" +plateScale : 1.0 + +# Provide transformations *from* the nativeSys *to* the specified system (e.g. FieldAngle) +transforms : + nativeSys : FocalPlane + FieldAngle : + transformType : radial + coeffs : [0.0, 1.0, 0.0] # radial distortion coefficients (c_0 + c_1 r + c_2 r^2 + ...) + +# +# A list of detectors in the camera; we only have one +# +CCDs : &CCDs + "0" : + detectorType : 0 + id : 0 + serial : "TBD" + offset : [0, 0] + refpos : [0, 0] + # + # [[x0, y0], [xSize, ySize]] + bbox : &bbox [[ 0, 0], [ 2048, 1]] # total bbox of trimmed detector + pixelSize : [1, 1] # in mm + transformDict : {nativeSys : 'Pixels', transforms : None} + transposeDetector : False + pitch : 0.0 # (degrees) + yaw : 0.0 # rotation in plane of camera (degrees) + roll : 0.0 # (degrees) + + amplifiers: # only 1 amplifier + "0": + hdu : 1 # Only one HDU in the file + + ixy : [0, 0] + readCorner : LL + flipXY : [False, False] + perAmpData : False # is the amp data split across multiple HDUs/Files? + + # [[x0, y0], [xSize, ySize]] + rawBBox : *bbox + rawDataBBox : *bbox + rawSerialPrescanBBox : [[0, 0], [0, 0]] # serial prescan + rawSerialOverscanBBox : [[0, 0], [0, 0]] # serial overscan + rawParallelPrescanBBox : [[0, 0], [0, 0]] # pixels digitised before first parallel + rawParallelOverscanBBox : [[0, 0], [0, 0]] # parallel overscan + + saturation : 16383 # saturation level, DN XXX Should this be in electrons? + + # Linearity correction is still under discussion, so this is a placeholder. + linearityType : PROPORTIONAL + linearityThreshold : 0 + linearityMax : 65535 + linearityCoeffs : [0, 65535] # == [linearityThreshold, linearityMax] + + gain : 1.00 + readNoise : 10 From fd56cc4a0f8db59fc4bdebbf279a89c5719bdddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Thu, 18 Jan 2024 11:19:18 -0800 Subject: [PATCH 03/42] Update eups packages list. --- ups/obs_fiberspectrograph.table | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/ups/obs_fiberspectrograph.table b/ups/obs_fiberspectrograph.table index 848450f..767db5e 100644 --- a/ups/obs_fiberspectrograph.table +++ b/ups/obs_fiberspectrograph.table @@ -1,9 +1,11 @@ -# List EUPS dependencies of this package here. -# - Any package whose API is used directly should be listed explicitly. -# - Common third-party packages can be assumed to be recursively included by -# the "base" package. -setupRequired(base) +setupRequired(obs_base) +setupRequired(obs_lsst) +setupRequired(astro_metadata_translator) -# The following is boilerplate for all packages. -# See https://dmtn-001.lsst.io for details on LSST_LIBRARY_PATH. +setupOptional(daf_butler) + +envPrepend(LD_LIBRARY_PATH, ${PRODUCT_DIR}/lib) +envPrepend(DYLD_LIBRARY_PATH, ${PRODUCT_DIR}/lib) +envPrepend(LSST_LIBRARY_PATH, ${PRODUCT_DIR}/lib) envPrepend(PYTHONPATH, ${PRODUCT_DIR}/python) +envPrepend(PATH, ${PRODUCT_DIR}/bin) \ No newline at end of file From 3c5637a138478a83b6e40cf6ce30f3d9856d8173 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Mon, 22 Jan 2024 17:54:38 -0800 Subject: [PATCH 04/42] Remove deprecated flag. --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 53fa4eb..dc6c6dd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,5 +10,4 @@ exclude = tests/.tests [tool:pytest] -addopts = --flake8 flake8-ignore = E133 E226 E228 N802 N803 N806 N812 N813 N815 N816 W503 From 78f5e7d48fc01e0cba421f2711fc6ed4a550b264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Mon, 22 Jan 2024 17:55:58 -0800 Subject: [PATCH 05/42] Harmonize names. --- policy/fiberSpectrograph.yaml | 2 +- python/lsst/obs/fiberspectrograph/_instrument.py | 4 ++-- python/lsst/obs/fiberspectrograph/spectrum.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/policy/fiberSpectrograph.yaml b/policy/fiberSpectrograph.yaml index ef45149..382ebfd 100644 --- a/policy/fiberSpectrograph.yaml +++ b/policy/fiberSpectrograph.yaml @@ -21,7 +21,7 @@ # # -name : "FiberSpectrograph" +name : "FiberSpec" plateScale : 1.0 # Provide transformations *from* the nativeSys *to* the specified system (e.g. FieldAngle) diff --git a/python/lsst/obs/fiberspectrograph/_instrument.py b/python/lsst/obs/fiberspectrograph/_instrument.py index 26a9dbc..e4c417e 100644 --- a/python/lsst/obs/fiberspectrograph/_instrument.py +++ b/python/lsst/obs/fiberspectrograph/_instrument.py @@ -1,4 +1,4 @@ -# This file is part of obs_fiberSpectrograph +# This file is part of obs_fiberspectrograph # # Developed for the LSST Data Management System. # This product includes software developed by the LSST Project @@ -30,7 +30,7 @@ from .filters import FIBER_SPECTROGRAPH_FILTER_DEFINITIONS from .translator import FiberSpectrographTranslator -PACKAGE_DIR = getPackageDir("obs_fiberSpectrograph") +PACKAGE_DIR = getPackageDir("obs_fiberspectrograph") class FiberSpectrograph(LsstCam): diff --git a/python/lsst/obs/fiberspectrograph/spectrum.py b/python/lsst/obs/fiberspectrograph/spectrum.py index 1addff1..78cd7a2 100644 --- a/python/lsst/obs/fiberspectrograph/spectrum.py +++ b/python/lsst/obs/fiberspectrograph/spectrum.py @@ -85,7 +85,7 @@ def readFits(path): Returns ------- - spectrum : `~lsst.obs.fiberSpectrograph.FiberSpectrum` + spectrum : `~lsst.obs.fiberspectrograph.FiberSpectrum` In-memory spectrum. """ md = dict(fitsio.read_header(path)) From f34a6c94dced76c0ea396652e8f588b0846d30b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Mon, 22 Jan 2024 17:56:28 -0800 Subject: [PATCH 06/42] Update init. --- python/lsst/obs/__init__.py | 2 ++ python/lsst/obs/fiberspectrograph/__init__.py | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 python/lsst/obs/__init__.py diff --git a/python/lsst/obs/__init__.py b/python/lsst/obs/__init__.py new file mode 100644 index 0000000..bb61062 --- /dev/null +++ b/python/lsst/obs/__init__.py @@ -0,0 +1,2 @@ +import pkgutil +__path__ = pkgutil.extend_path(__path__, __name__) diff --git a/python/lsst/obs/fiberspectrograph/__init__.py b/python/lsst/obs/fiberspectrograph/__init__.py index 05676d2..cc262c1 100644 --- a/python/lsst/obs/fiberspectrograph/__init__.py +++ b/python/lsst/obs/fiberspectrograph/__init__.py @@ -20,3 +20,5 @@ # along with this program. If not, see . from .version import * # Generated by sconsUtils +from ._instrument import * +from .spectrum import * \ No newline at end of file From 7d98960547ccdb06e204f9d3ae791f8401db366e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Mon, 22 Jan 2024 17:56:49 -0800 Subject: [PATCH 07/42] Add instrument test from lsst dm. --- tests/test_instrument.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/test_instrument.py diff --git a/tests/test_instrument.py b/tests/test_instrument.py new file mode 100644 index 0000000..e321856 --- /dev/null +++ b/tests/test_instrument.py @@ -0,0 +1,24 @@ +"""Tests of the FiberSpectrograph instrument class. +""" + +import unittest + +import lsst.utils.tests +import lsst.obs.fiberspectrograph +from lsst.obs.base.instrument_tests import InstrumentTests, InstrumentTestData + + +class TestStarTracker(InstrumentTests, lsst.utils.tests.TestCase): + def setUp(self): + physical_filters = set(["empty"]) + + self.data = InstrumentTestData(name="FiberSpec", + nDetectors=1, + firstDetectorName="0", + physical_filters=physical_filters) + self.instrument = lsst.obs.fiberspectrograph.FiberSpectrograph() + + +if __name__ == '__main__': + lsst.utils.tests.init() + unittest.main() From 06acd127211a9358be6eab0e90fccf19f518f2ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Tue, 23 Jan 2024 10:19:25 -0800 Subject: [PATCH 08/42] Harmonize naming. --- python/lsst/obs/fiberspectrograph/isrTask.py | 2 +- python/lsst/obs/fiberspectrograph/translator.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python/lsst/obs/fiberspectrograph/isrTask.py b/python/lsst/obs/fiberspectrograph/isrTask.py index 36d09d5..a4cccf4 100644 --- a/python/lsst/obs/fiberspectrograph/isrTask.py +++ b/python/lsst/obs/fiberspectrograph/isrTask.py @@ -1,4 +1,4 @@ -# This file is part of obs_fiberSpectrograph +# This file is part of obs_fiberspectrograph # # Developed for the LSST Data Management System. # This product includes software developed by the LSST Project diff --git a/python/lsst/obs/fiberspectrograph/translator.py b/python/lsst/obs/fiberspectrograph/translator.py index e1b8a4b..a55db31 100644 --- a/python/lsst/obs/fiberspectrograph/translator.py +++ b/python/lsst/obs/fiberspectrograph/translator.py @@ -23,10 +23,10 @@ class FiberSpectrographTranslator(LsstBaseTranslator): supported_instrument = "FiberSpec" """Supports the Rubin calibration fibre spectrographs.""" - default_search_path = os.path.join(getPackageDir("obs_fiberSpectrograph"), "corrections") + default_search_path = os.path.join(getPackageDir("obs_fiberspectrograph"), "corrections") """Default search path to use to locate header correction files.""" - default_resource_root = os.path.join(getPackageDir("obs_fiberSpectrograph"), "corrections") + default_resource_root = os.path.join(getPackageDir("obs_fiberspectrograph"), "corrections") """Default resource path root to use to locate header correction files.""" DETECTOR_MAX = 1 From d32061801e86eeed609c28351d9e09ce45ce5b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Tue, 23 Jan 2024 11:25:02 -0800 Subject: [PATCH 09/42] Add butler plugin. --- tests/SConscript | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/SConscript b/tests/SConscript index 5437cb4..76a9e2b 100644 --- a/tests/SConscript +++ b/tests/SConscript @@ -1,3 +1,8 @@ # -*- python -*- -from lsst.sconsUtils import scripts +import os + +from lsst.sconsUtils import env, scripts scripts.BasicSConscript.tests(pyList=[]) + +if "DAF_BUTLER_PLUGINS" in os.environ: + env["ENV"]["DAF_BUTLER_PLUGINS"] = os.environ["DAF_BUTLER_PLUGINS"] \ No newline at end of file From 6441a4f6c4af98fa13ff853d492de1b01aaac654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Tue, 23 Jan 2024 11:25:27 -0800 Subject: [PATCH 10/42] Update name in doc. --- python/lsst/obs/fiberspectrograph/spectrum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lsst/obs/fiberspectrograph/spectrum.py b/python/lsst/obs/fiberspectrograph/spectrum.py index 78cd7a2..0b29cae 100644 --- a/python/lsst/obs/fiberspectrograph/spectrum.py +++ b/python/lsst/obs/fiberspectrograph/spectrum.py @@ -1,4 +1,4 @@ -# This file is part of obs_fiberSpectrograph +# This file is part of obs_fiberspectrograph # # Developed for the LSST Data Management System. # This product includes software developed by the LSST Project From c8ce2b7f6b005129dff2e9422e4c95c7076905e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Tue, 23 Jan 2024 11:27:40 -0800 Subject: [PATCH 11/42] Add tests and update default setup. Add ingestion test. Add test data. Update default setup. Update to do in ingestion test. --- ...iberSpec_empty_21_0_FiberSpec_raw_all.fits | Bin 0 -> 40320 bytes tests/test_ingestion.py | 46 ++++++++++++++++++ ups/obs_fiberspectrograph.cfg | 13 +++++ ups/obs_fiberspectrograph.table | 4 +- 4 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 tests/data/rawSpectrum_FiberSpec_empty_21_0_FiberSpec_raw_all.fits create mode 100644 tests/test_ingestion.py create mode 100644 ups/obs_fiberspectrograph.cfg diff --git a/tests/data/rawSpectrum_FiberSpec_empty_21_0_FiberSpec_raw_all.fits b/tests/data/rawSpectrum_FiberSpec_empty_21_0_FiberSpec_raw_all.fits new file mode 100644 index 0000000000000000000000000000000000000000..81ad1c7dbfb8d9ab8a99ad536c2d700d170c6435 GIT binary patch literal 40320 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 HGw{Cv*+g-n literal 0 HcmV?d00001 diff --git a/tests/test_ingestion.py b/tests/test_ingestion.py new file mode 100644 index 0000000..b3425b9 --- /dev/null +++ b/tests/test_ingestion.py @@ -0,0 +1,46 @@ +"""Unit tests for Gen3 fiberspectrograph raw data ingest. +""" + +import unittest +import os +import lsst.utils.tests + +from lsst.obs.base.ingest_tests import IngestTestBase +from lsst.obs.fiberspectrograph import FiberSpectrograph +from lsst.obs.fiberspectrograph.filters import FIBER_SPECTROGRAPH_FILTER_DEFINITIONS + +# TODO DM 42620 +# testDataPackage = "testdata_fiberSpectrograph" +# try: +# testDataDirectory = lsst.utils.getPackageDir(testDataPackage) +# except (LookupError, lsst.pex.exceptions.NotFoundError): +# testDataDirectory = None +testDataDirectory = os.path.join(os.path.dirname(__file__), "data") + + +class FiberSpectrographIngestTestCase(IngestTestBase, lsst.utils.tests.TestCase): + instrumentClassName = "lsst.obs.fiberspectrograph.FiberSpectrograph" + visits = None # we don't have a definition of visits + ingestDatasetTypeName = "rawSpectrum" + + def setUp(self): + self.ingestdir = os.path.dirname(__file__) + self.instrument = FiberSpectrograph() + self.file = os.path.join(testDataDirectory, + "rawSpectrum_FiberSpec_empty_21_0_FiberSpec_raw_all.fits") + + day_obs = 20230116 + seq_num = 21 + self.dataIds = [dict(instrument="FiberSpec", exposure=100000 * day_obs + seq_num, detector=0)] + self.filterLabel = FIBER_SPECTROGRAPH_FILTER_DEFINITIONS[0].makeFilterLabel() + + super().setUp() + + +def setup_module(module): + lsst.utils.tests.init() + + +if __name__ == "__main__": + lsst.utils.tests.init() + unittest.main() diff --git a/ups/obs_fiberspectrograph.cfg b/ups/obs_fiberspectrograph.cfg new file mode 100644 index 0000000..575629c --- /dev/null +++ b/ups/obs_fiberspectrograph.cfg @@ -0,0 +1,13 @@ +# -*- python -*- + +import lsst.sconsUtils + +dependencies = { +} + +config = lsst.sconsUtils.Configuration( + __file__, + headers=[], libs=[], + hasDoxygenInclude=False, + hasSwigFiles=False, +) diff --git a/ups/obs_fiberspectrograph.table b/ups/obs_fiberspectrograph.table index 767db5e..900e124 100644 --- a/ups/obs_fiberspectrograph.table +++ b/ups/obs_fiberspectrograph.table @@ -1,8 +1,8 @@ setupRequired(obs_base) setupRequired(obs_lsst) setupRequired(astro_metadata_translator) - -setupOptional(daf_butler) +setupRequired(daf_butler) +setupRequired(ip_isr) envPrepend(LD_LIBRARY_PATH, ${PRODUCT_DIR}/lib) envPrepend(DYLD_LIBRARY_PATH, ${PRODUCT_DIR}/lib) From 97f125e902b529bd44f3d6f723969ce686ac0b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Tue, 23 Jan 2024 12:20:18 -0800 Subject: [PATCH 12/42] Add isr pipeline. --- pipelines/fiberspectrograph/ISR.yaml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 pipelines/fiberspectrograph/ISR.yaml diff --git a/pipelines/fiberspectrograph/ISR.yaml b/pipelines/fiberspectrograph/ISR.yaml new file mode 100644 index 0000000..29b3926 --- /dev/null +++ b/pipelines/fiberspectrograph/ISR.yaml @@ -0,0 +1,24 @@ +description: ISR for Rubin fiber spectrographs +instrument: lsst.obs.fiberspectrograph.FiberSpectrograph + +tasks: + isr: + class: lsst.obs.fiberspectrograph.isrTask.IsrTask + config: + doBias: false + doCrosstalk: false + doVariance: false + doLinearize: false + doDefect: false + doDark: false + doFlat: false + doFringe: false + doAssembleCcd: false + doNanMasking: false + doSaturation: true + doWidenSaturationTrails: false + doCameraSpecificMasking: false + doSetBadRegions: false + doInterpolate: false + doMeasureBackground: false + doStandardStatistics: false From 229442af5e65d5d40d859836eeb4f07622d4a2e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Tue, 23 Jan 2024 12:27:19 -0800 Subject: [PATCH 13/42] Readme, setup and linting. Update readme. Update readme. Change readme format. Update setup. Update readme. Update lint file. Update policy header. Add new lines. Remove unnecessary config file. --- .github/workflows/lint.yaml | 23 +++++-------------- README.md | 5 ++++ README.rst | 7 ------ policy/fiberSpectrograph.yaml | 8 +++++-- python/lsst/obs/fiberspectrograph/__init__.py | 2 +- setup.cfg | 1 - ups/obs_fiberspectrograph.cfg | 13 ----------- 7 files changed, 18 insertions(+), 41 deletions(-) create mode 100644 README.md delete mode 100644 README.rst delete mode 100644 ups/obs_fiberspectrograph.cfg diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 2b20981..8330a70 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -1,22 +1,11 @@ name: lint on: - - push - - pull_request + push: + branches: + - main + pull_request: jobs: - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: 3.7 - - - name: Install - run: pip install -r <(curl https://raw.githubusercontent.com/lsst/linting/main/requirements.txt) - - - name: Run linter - run: flake8 + call-workflow: + uses: lsst/rubin_workflows/.github/workflows/lint.yaml@main \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..60b2797 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# obs_fiberspectrograph + +``obs_fiberspectrograph`` is a package in the `LSST Science Pipelines `_. + +Package to ingest fiber spectrograph data. diff --git a/README.rst b/README.rst deleted file mode 100644 index 1dbfb6c..0000000 --- a/README.rst +++ /dev/null @@ -1,7 +0,0 @@ -##################### -obs_fiberspectrograph -##################### - -``obs_fiberspectrograph`` is a package in the `LSST Science Pipelines `_. - -.. Add a brief (few sentence) description of what this package provides. diff --git a/policy/fiberSpectrograph.yaml b/policy/fiberSpectrograph.yaml index 382ebfd..08df454 100644 --- a/policy/fiberSpectrograph.yaml +++ b/policy/fiberSpectrograph.yaml @@ -1,6 +1,10 @@ +# This file is part of obs_fiberspectrograph. # -# LSST Data Management System -# Copyright 2017 LSST Corporation. +# Developed for the LSST Data Management System. +# This product includes software developed by the LSST Project +# (https://www.lsst.org). +# See the COPYRIGHT file at the top-level directory of this distribution +# for details of code ownership. # # This product includes software developed by the # LSST Project (http://www.lsst.org/). diff --git a/python/lsst/obs/fiberspectrograph/__init__.py b/python/lsst/obs/fiberspectrograph/__init__.py index cc262c1..4c46211 100644 --- a/python/lsst/obs/fiberspectrograph/__init__.py +++ b/python/lsst/obs/fiberspectrograph/__init__.py @@ -21,4 +21,4 @@ from .version import * # Generated by sconsUtils from ._instrument import * -from .spectrum import * \ No newline at end of file +from .spectrum import * diff --git a/setup.cfg b/setup.cfg index dc6c6dd..df45691 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,4 +10,3 @@ exclude = tests/.tests [tool:pytest] -flake8-ignore = E133 E226 E228 N802 N803 N806 N812 N813 N815 N816 W503 diff --git a/ups/obs_fiberspectrograph.cfg b/ups/obs_fiberspectrograph.cfg deleted file mode 100644 index 575629c..0000000 --- a/ups/obs_fiberspectrograph.cfg +++ /dev/null @@ -1,13 +0,0 @@ -# -*- python -*- - -import lsst.sconsUtils - -dependencies = { -} - -config = lsst.sconsUtils.Configuration( - __file__, - headers=[], libs=[], - hasDoxygenInclude=False, - hasSwigFiles=False, -) From 9ce6b420c45f9ebe4794a56d833f6237423ba123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Mon, 29 Jan 2024 13:49:49 -0800 Subject: [PATCH 14/42] Change detector name. Update detector name. Update detector name in test. --- policy/fiberSpectrograph.yaml | 2 +- tests/test_instrument.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/policy/fiberSpectrograph.yaml b/policy/fiberSpectrograph.yaml index 08df454..0a8e413 100644 --- a/policy/fiberSpectrograph.yaml +++ b/policy/fiberSpectrograph.yaml @@ -39,7 +39,7 @@ transforms : # A list of detectors in the camera; we only have one # CCDs : &CCDs - "0" : + "ccd0" : detectorType : 0 id : 0 serial : "TBD" diff --git a/tests/test_instrument.py b/tests/test_instrument.py index e321856..b7bbb6b 100644 --- a/tests/test_instrument.py +++ b/tests/test_instrument.py @@ -14,7 +14,7 @@ def setUp(self): self.data = InstrumentTestData(name="FiberSpec", nDetectors=1, - firstDetectorName="0", + firstDetectorName="ccd0", physical_filters=physical_filters) self.instrument = lsst.obs.fiberspectrograph.FiberSpectrograph() From 8b21fde0088135eacecb37a6049cd5acfbdd446e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Tue, 30 Jan 2024 17:05:06 -0800 Subject: [PATCH 15/42] Update default band name. --- python/lsst/obs/fiberspectrograph/filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lsst/obs/fiberspectrograph/filters.py b/python/lsst/obs/fiberspectrograph/filters.py index 3a805cd..b009d57 100644 --- a/python/lsst/obs/fiberspectrograph/filters.py +++ b/python/lsst/obs/fiberspectrograph/filters.py @@ -1,5 +1,5 @@ from lsst.obs.base import FilterDefinition, FilterDefinitionCollection FIBER_SPECTROGRAPH_FILTER_DEFINITIONS = FilterDefinitionCollection( - FilterDefinition(band="empty", physical_filter="empty"), + FilterDefinition(band="white", physical_filter="empty"), ) From a71b9caa5f484a6bda524e5dc8a714b542537353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Tue, 30 Jan 2024 17:53:17 -0800 Subject: [PATCH 16/42] Update method type in spectrum. --- python/lsst/obs/fiberspectrograph/spectrum.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/lsst/obs/fiberspectrograph/spectrum.py b/python/lsst/obs/fiberspectrograph/spectrum.py index 0b29cae..4a99b84 100644 --- a/python/lsst/obs/fiberspectrograph/spectrum.py +++ b/python/lsst/obs/fiberspectrograph/spectrum.py @@ -74,8 +74,8 @@ def getFilter(self): def getBBox(self): return self.detector.getBBox() - @staticmethod - def readFits(path): + @classmethod + def readFits(cls,path): """Read a Spectrum from disk" Parameters @@ -94,7 +94,7 @@ def readFits(path): wavelength = u.Quantity(wavelength, u.Unit(md["CUNIT1"]), copy=False) - return FiberSpectrum(wavelength, flux, md) + return cls(wavelength, flux, md) def writeFits(self, path): """Write a Spectrum to disk From 00b16812687154646923385ccce5ed7949b087d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Wed, 31 Jan 2024 10:17:40 -0800 Subject: [PATCH 17/42] Update setup requirements. --- ups/obs_fiberspectrograph.table | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ups/obs_fiberspectrograph.table b/ups/obs_fiberspectrograph.table index 900e124..af78a75 100644 --- a/ups/obs_fiberspectrograph.table +++ b/ups/obs_fiberspectrograph.table @@ -3,9 +3,6 @@ setupRequired(obs_lsst) setupRequired(astro_metadata_translator) setupRequired(daf_butler) setupRequired(ip_isr) +setupRequired(afw) -envPrepend(LD_LIBRARY_PATH, ${PRODUCT_DIR}/lib) -envPrepend(DYLD_LIBRARY_PATH, ${PRODUCT_DIR}/lib) -envPrepend(LSST_LIBRARY_PATH, ${PRODUCT_DIR}/lib) envPrepend(PYTHONPATH, ${PRODUCT_DIR}/python) -envPrepend(PATH, ${PRODUCT_DIR}/bin) \ No newline at end of file From 24106dbd970b85c79540e68eed05654c154d8f10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Thu, 1 Feb 2024 13:14:11 -0800 Subject: [PATCH 18/42] Formatter with consistent methods. --- .../obs/fiberspectrograph/rawFormatter.py | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/python/lsst/obs/fiberspectrograph/rawFormatter.py b/python/lsst/obs/fiberspectrograph/rawFormatter.py index e6383a3..17a27e7 100644 --- a/python/lsst/obs/fiberspectrograph/rawFormatter.py +++ b/python/lsst/obs/fiberspectrograph/rawFormatter.py @@ -1,43 +1,34 @@ __all__ = [] -from lsst.obs.base import FitsRawFormatterBase +from lsst.daf.butler import Formatter from .filters import FIBER_SPECTROGRAPH_FILTER_DEFINITIONS from ._instrument import FiberSpectrograph from .translator import FiberSpectrographTranslator -import fitsio -import astropy.units as u -from lsst.daf.base import PropertyList +from .spectrum import FiberSpectrum -class FiberSpectrographRawFormatter(FitsRawFormatterBase): +class FiberSpectrographRawFormatter(Formatter): cameraClass = FiberSpectrograph translatorClass = FiberSpectrographTranslator + fiberSpectrumClass = FiberSpectrum filterDefinitions = FIBER_SPECTROGRAPH_FILTER_DEFINITIONS def getDetector(self, id): return self.cameraClass().getCamera()[id] def read(self, component=None): - """Read just the image component of the Exposure. + """Read fiberspectrograph data. Returns ------- - image : `~lsst.afw.image.Image` - In-memory image component. + image : `~lsst.obs.fiberspectrograph.FiberSpectrum` + In-memory spectrum. """ - pytype = self.fileDescriptor.storageClass.pytype path = self.fileDescriptor.location.path - sourceMd = dict(fitsio.read_header(path)) - md = PropertyList() - md.update(sourceMd) - if component is not None: - if component == 'metadata': - return md + return self.fiberSpectrumClass.readFits(path) - flux = fitsio.read(path) - wavelength = fitsio.read(path, ext=md["PS1_0"], columns=md["PS1_1"]).flatten() - - wavelength = u.Quantity(wavelength, u.Unit(md["CUNIT1"]), copy=False) + def write(self): + path = self.fileDescriptor.location.path - return pytype(wavelength, flux, md) + return self.fiberSpectrumClass.writeFits(path) From 924193a53956f8a02e3f075351a9b6e523b13b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Thu, 1 Feb 2024 13:14:47 -0800 Subject: [PATCH 19/42] Change from fitsio to astropy. --- python/lsst/obs/fiberspectrograph/spectrum.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/python/lsst/obs/fiberspectrograph/spectrum.py b/python/lsst/obs/fiberspectrograph/spectrum.py index 4a99b84..58db9ee 100644 --- a/python/lsst/obs/fiberspectrograph/spectrum.py +++ b/python/lsst/obs/fiberspectrograph/spectrum.py @@ -23,7 +23,6 @@ import numpy as np import astropy.io.fits -import fitsio import astropy.units as u from ._instrument import FiberSpectrograph import lsst.afw.image as afwImage @@ -75,7 +74,7 @@ def getBBox(self): return self.detector.getBBox() @classmethod - def readFits(cls,path): + def readFits(cls, path): """Read a Spectrum from disk" Parameters @@ -88,11 +87,15 @@ def readFits(cls,path): spectrum : `~lsst.obs.fiberspectrograph.FiberSpectrum` In-memory spectrum. """ - md = dict(fitsio.read_header(path)) - flux = fitsio.read(path) - wavelength = fitsio.read(path, ext=md["PS1_0"], columns=md["PS1_1"]).flatten() - wavelength = u.Quantity(wavelength, u.Unit(md["CUNIT1"]), copy=False) + fitsfile = astropy.io.fits.open(path) + md = dict(fitsfile[0].header) + + if md["FORMAT_V"] >= 1: + flux = fitsfile[0].data + wavelength = fitsfile[md["PS1_0"]].data[md["PS1_1"]].flatten() + + wavelength = u.Quantity(wavelength, u.Unit(md["CUNIT1"]), copy=False) return cls(wavelength, flux, md) From 275fc7c14be71267091c9fc67afbb3d398928f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Thu, 1 Feb 2024 14:30:54 -0800 Subject: [PATCH 20/42] Add extension to raw formatter. --- python/lsst/obs/fiberspectrograph/rawFormatter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/lsst/obs/fiberspectrograph/rawFormatter.py b/python/lsst/obs/fiberspectrograph/rawFormatter.py index 17a27e7..c28467d 100644 --- a/python/lsst/obs/fiberspectrograph/rawFormatter.py +++ b/python/lsst/obs/fiberspectrograph/rawFormatter.py @@ -12,6 +12,7 @@ class FiberSpectrographRawFormatter(Formatter): translatorClass = FiberSpectrographTranslator fiberSpectrumClass = FiberSpectrum filterDefinitions = FIBER_SPECTROGRAPH_FILTER_DEFINITIONS + extension = ".fits" def getDetector(self, id): return self.cameraClass().getCamera()[id] From ac6aa9192170e8d14828140d57246a5620a9b9e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Thu, 1 Feb 2024 14:33:02 -0800 Subject: [PATCH 21/42] Add serial number. --- policy/fiberSpectrograph.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/policy/fiberSpectrograph.yaml b/policy/fiberSpectrograph.yaml index 0a8e413..49cdaef 100644 --- a/policy/fiberSpectrograph.yaml +++ b/policy/fiberSpectrograph.yaml @@ -42,7 +42,7 @@ CCDs : &CCDs "ccd0" : detectorType : 0 id : 0 - serial : "TBD" + serial : "1606191U1" offset : [0, 0] refpos : [0, 0] # From 9d4a7f10da9bb1ffdab31e173160186d1d6d3e4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Fri, 2 Feb 2024 12:45:51 -0800 Subject: [PATCH 22/42] Update to translator. --- python/lsst/obs/fiberspectrograph/translator.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/python/lsst/obs/fiberspectrograph/translator.py b/python/lsst/obs/fiberspectrograph/translator.py index a55db31..f504192 100644 --- a/python/lsst/obs/fiberspectrograph/translator.py +++ b/python/lsst/obs/fiberspectrograph/translator.py @@ -34,10 +34,9 @@ class FiberSpectrographTranslator(LsstBaseTranslator): _const_map = { "detector_num": 0, "detector_name": "0", - "exposure_group": None, "object": None, "physical_filter": "empty", - "detector_serial": "0xdeadbeef", + "detector_serial": "1606191U1", "detector_group": "None", "relative_humidity": None, "pressure": None, @@ -191,7 +190,7 @@ def compute_exposure_id(dayobs, seqnum, controller=None): def to_visit_id(self): """Calculate the visit associated with this exposure. """ - return None + return self.exposure_id @cache_translation def to_exposure_id(self): @@ -204,9 +203,6 @@ def to_exposure_id(self): exposure_id : `int` Unique exposure number. """ - if "CALIB_ID" in self._header: - self._used_these_cards("CALIB_ID") - return None dayobs = self.to_observing_day() seqnum = self.to_observation_counter() From 6b3ab9c6748f52372a6523cc954c5cf51606a9da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Fri, 2 Feb 2024 12:46:36 -0800 Subject: [PATCH 23/42] Simplify header udpate. --- python/lsst/obs/fiberspectrograph/spectrum.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/python/lsst/obs/fiberspectrograph/spectrum.py b/python/lsst/obs/fiberspectrograph/spectrum.py index 58db9ee..32c1884 100644 --- a/python/lsst/obs/fiberspectrograph/spectrum.py +++ b/python/lsst/obs/fiberspectrograph/spectrum.py @@ -149,8 +149,7 @@ def make_fits_header(self): hdr = astropy.io.fits.Header() hdr["FORMAT_V"] = self.FORMAT_VERSION - for k, v in self.spectrum.md.items(): - hdr[k] = v + hdr.update(self.spectrum.md) # WCS headers - Use -TAB WCS definition wcs_cards = [ From a58c5fc7a418fe5a3c028270e406ce3249972e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Fri, 2 Feb 2024 13:00:10 -0800 Subject: [PATCH 24/42] Remove physical filter in instrument definition. --- python/lsst/obs/fiberspectrograph/_instrument.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lsst/obs/fiberspectrograph/_instrument.py b/python/lsst/obs/fiberspectrograph/_instrument.py index e4c417e..7a0f11e 100644 --- a/python/lsst/obs/fiberspectrograph/_instrument.py +++ b/python/lsst/obs/fiberspectrograph/_instrument.py @@ -50,7 +50,7 @@ class FiberSpectrograph(LsstCam): translatorClass = FiberSpectrographTranslator visitSystem = VisitSystem.BY_SEQ_START_END raw_definition = ("rawSpectrum", - ("instrument", "physical_filter", "exposure", "detector"), + ("instrument", "exposure", "detector"), "FiberSpectrum") @classmethod From e9a0e6e85eb03e4495637fd9540338c117acb08c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Fri, 2 Feb 2024 13:55:04 -0800 Subject: [PATCH 25/42] Update seq num and exposure id. --- python/lsst/obs/fiberspectrograph/translator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/python/lsst/obs/fiberspectrograph/translator.py b/python/lsst/obs/fiberspectrograph/translator.py index f504192..f597356 100644 --- a/python/lsst/obs/fiberspectrograph/translator.py +++ b/python/lsst/obs/fiberspectrograph/translator.py @@ -108,9 +108,9 @@ def to_observation_counter(self): counter : `int` The sequence number for this day. """ - if self.is_key_ok("OBSID"): - self._used_these_cards("OBSID") - return int(self._header["OBSID"]) + if self.is_key_ok("SEQNUM"): + self._used_these_cards("SEQNUM") + return int(self._header["SEQNUM"]) # This indicates a problem so we warn and return a 0 log.warning("%s: Unable to determine the observation counter so returning 0", @@ -190,7 +190,7 @@ def compute_exposure_id(dayobs, seqnum, controller=None): def to_visit_id(self): """Calculate the visit associated with this exposure. """ - return self.exposure_id + return self.to_exposure_id() @cache_translation def to_exposure_id(self): From 6eeb8b5b1b2ed16794fba7c8709983cc4e92bbb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Fri, 2 Feb 2024 13:55:40 -0800 Subject: [PATCH 26/42] Change test data. --- ..._fiberSpecBroad_2024-01-09T17:41:34.996.fits | Bin 0 -> 40320 bytes ..._FiberSpec_empty_21_0_FiberSpec_raw_all.fits | Bin 40320 -> 0 bytes tests/test_ingestion.py | 6 +++--- 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 tests/data/Broad_fiberSpecBroad_2024-01-09T17:41:34.996.fits delete mode 100644 tests/data/rawSpectrum_FiberSpec_empty_21_0_FiberSpec_raw_all.fits diff --git a/tests/data/Broad_fiberSpecBroad_2024-01-09T17:41:34.996.fits b/tests/data/Broad_fiberSpecBroad_2024-01-09T17:41:34.996.fits new file mode 100644 index 0000000000000000000000000000000000000000..59229ce9b1479eedb3ab537990c881ba23e14deb GIT binary patch literal 40320 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 j((B%oeEmUHR>!wE-6`DX?GS%Lq5EAW2+pchJ< literal 0 HcmV?d00001 diff --git a/tests/data/rawSpectrum_FiberSpec_empty_21_0_FiberSpec_raw_all.fits b/tests/data/rawSpectrum_FiberSpec_empty_21_0_FiberSpec_raw_all.fits deleted file mode 100644 index 81ad1c7dbfb8d9ab8a99ad536c2d700d170c6435..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 40320 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 HGw{Cv*+g-n diff --git a/tests/test_ingestion.py b/tests/test_ingestion.py index b3425b9..4595f21 100644 --- a/tests/test_ingestion.py +++ b/tests/test_ingestion.py @@ -27,10 +27,10 @@ def setUp(self): self.ingestdir = os.path.dirname(__file__) self.instrument = FiberSpectrograph() self.file = os.path.join(testDataDirectory, - "rawSpectrum_FiberSpec_empty_21_0_FiberSpec_raw_all.fits") + "Broad_fiberSpecBroad_2024-01-09T17:41:34.996.fits") - day_obs = 20230116 - seq_num = 21 + day_obs = 20240109 + seq_num = 4 self.dataIds = [dict(instrument="FiberSpec", exposure=100000 * day_obs + seq_num, detector=0)] self.filterLabel = FIBER_SPECTROGRAPH_FILTER_DEFINITIONS[0].makeFilterLabel() From 7995fdad5ef03c42dcbc8d9690319d2321cd1d19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Fri, 2 Feb 2024 16:09:23 -0800 Subject: [PATCH 27/42] Add class documentation. --- python/lsst/obs/fiberspectrograph/spectrum.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/python/lsst/obs/fiberspectrograph/spectrum.py b/python/lsst/obs/fiberspectrograph/spectrum.py index 32c1884..790bd1a 100644 --- a/python/lsst/obs/fiberspectrograph/spectrum.py +++ b/python/lsst/obs/fiberspectrograph/spectrum.py @@ -45,6 +45,18 @@ def getVisitInfo(self): class FiberSpectrum: + """Define a spectrum from a fiber spectrograph + + Parameters + ---------- + wavelength : `numpy.ndarray` + Spectrum wavelength in units provided by the spectrum file. + flux: `numpy.ndarray` + Spectrum flux. + md: `dict` + Dictionary of the spectrum headers. + """ + def __init__(self, wavelength, flux, md=None): self.wavelength = wavelength self.flux = flux From 425f43baf9a8255b13783631bf4a3d01c256d84e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Fri, 2 Feb 2024 16:10:28 -0800 Subject: [PATCH 28/42] Remove translator methods to parent class methods instead. --- .../lsst/obs/fiberspectrograph/translator.py | 48 ------------------- 1 file changed, 48 deletions(-) diff --git a/python/lsst/obs/fiberspectrograph/translator.py b/python/lsst/obs/fiberspectrograph/translator.py index f597356..34d8a96 100644 --- a/python/lsst/obs/fiberspectrograph/translator.py +++ b/python/lsst/obs/fiberspectrograph/translator.py @@ -86,37 +86,6 @@ def to_datetime_begin(self): self._used_these_cards("DATE-BEG") return Time(self._header["DATE-BEG"], scale="tai", format="isot") - @cache_translation - def to_observing_day(self): - """Return the day of observation as YYYYMMDD integer. - - Returns - ------- - obs_day : `int` - The day of observation. - """ - date = self.to_datetime_begin() - date -= self._ROLLOVER_TIME - return int(date.strftime("%Y%m%d")) - - @cache_translation - def to_observation_counter(self): - """Return the sequence number within the observing day. - - Returns - ------- - counter : `int` - The sequence number for this day. - """ - if self.is_key_ok("SEQNUM"): - self._used_these_cards("SEQNUM") - return int(self._header["SEQNUM"]) - - # This indicates a problem so we warn and return a 0 - log.warning("%s: Unable to determine the observation counter so returning 0", - self._log_prefix) - return 0 - @cache_translation def to_exposure_time(self): # Docstring will be inherited. Property defined in properties.py @@ -191,20 +160,3 @@ def to_visit_id(self): """Calculate the visit associated with this exposure. """ return self.to_exposure_id() - - @cache_translation - 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. - """ - - dayobs = self.to_observing_day() - seqnum = self.to_observation_counter() - - return self.compute_exposure_id(dayobs, seqnum) From abd1cdfb29871f6b9b739bf7555069424affec58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Fri, 2 Feb 2024 17:00:23 -0800 Subject: [PATCH 29/42] Update doc. --- python/lsst/obs/fiberspectrograph/rawFormatter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lsst/obs/fiberspectrograph/rawFormatter.py b/python/lsst/obs/fiberspectrograph/rawFormatter.py index c28467d..260b828 100644 --- a/python/lsst/obs/fiberspectrograph/rawFormatter.py +++ b/python/lsst/obs/fiberspectrograph/rawFormatter.py @@ -22,7 +22,7 @@ def read(self, component=None): Returns ------- - image : `~lsst.obs.fiberspectrograph.FiberSpectrum` + FiberSpectrum: `~lsst.obs.fiberspectrograph.FiberSpectrum` In-memory spectrum. """ path = self.fileDescriptor.location.path From c5cce552fc698c021ebbfe7fe9eaf853a30130c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Tue, 6 Feb 2024 11:56:56 -0800 Subject: [PATCH 30/42] Add purpose. --- python/lsst/obs/fiberspectrograph/_instrument.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/lsst/obs/fiberspectrograph/_instrument.py b/python/lsst/obs/fiberspectrograph/_instrument.py index 7a0f11e..9c81646 100644 --- a/python/lsst/obs/fiberspectrograph/_instrument.py +++ b/python/lsst/obs/fiberspectrograph/_instrument.py @@ -74,4 +74,5 @@ def extractDetectorRecord(self, camGeomDetector): instrument=self.getName(), id=camGeomDetector.getId(), full_name=camGeomDetector.getName(), + purpose=str(camGeomDetector.getType()).split(".")[-1] ) From 0ff0dd47213ead9fe5a4a7c458fd8e8ce4369411 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Fri, 9 Feb 2024 11:51:32 -0800 Subject: [PATCH 31/42] Remove data manager class from spectrum and add detector to FiberSpectrum constructor. --- python/lsst/obs/fiberspectrograph/spectrum.py | 96 +------------------ 1 file changed, 5 insertions(+), 91 deletions(-) diff --git a/python/lsst/obs/fiberspectrograph/spectrum.py b/python/lsst/obs/fiberspectrograph/spectrum.py index 790bd1a..8392c9a 100644 --- a/python/lsst/obs/fiberspectrograph/spectrum.py +++ b/python/lsst/obs/fiberspectrograph/spectrum.py @@ -25,6 +25,7 @@ import astropy.io.fits import astropy.units as u from ._instrument import FiberSpectrograph +from .data_manager import DataManager import lsst.afw.image as afwImage @@ -55,15 +56,16 @@ class FiberSpectrum: Spectrum flux. md: `dict` Dictionary of the spectrum headers. + detectorId : `int` + Optional Detector ID for this data. """ - def __init__(self, wavelength, flux, md=None): + def __init__(self, wavelength, flux, md=None, detectorId=0): self.wavelength = wavelength self.flux = flux self.md = md - self.info = Info(md) - self.detector = FiberSpectrograph().getCamera()[0] + self.detector = FiberSpectrograph().getCamera()[detectorId] self.__Mask = afwImage.MaskX(1, 1) self.getPlaneBitMask = self.__Mask.getPlaneBitMask # ughh, awful Mask API @@ -122,91 +124,3 @@ def writeFits(self, path): hdl = DataManager(self).make_hdulist() hdl.writeto(path) - - -class DataManager: - """A data packager for `Spectrum` objects - """ - - wcs_table_name = "WCS-TAB" - """Name of the table containing the wavelength WCS (EXTNAME).""" - wcs_table_ver = 1 - """WCS table version (EXTVER).""" - wcs_column_name = "wavelength" - """Name of the table column containing the wavelength information.""" - - # The version of the FITS file format produced by this class. - FORMAT_VERSION = 1 - - def __init__(self, spectrum): - self.spectrum = spectrum - - def make_hdulist(self): - """Generate a FITS hdulist built from SpectrographData. - Parameters - ---------- - spec : `SpectrographData` - The data from which to build the FITS hdulist. - Returns - ------- - hdulist : `astropy.io.fits.HDUList` - The FITS hdulist. - """ - hdu1 = self.make_primary_hdu() - hdu2 = self.make_wavelength_hdu() - return astropy.io.fits.HDUList([hdu1, hdu2]) - - def make_fits_header(self): - """Return a FITS header built from a Spectrum""" - hdr = astropy.io.fits.Header() - - hdr["FORMAT_V"] = self.FORMAT_VERSION - hdr.update(self.spectrum.md) - - # WCS headers - Use -TAB WCS definition - wcs_cards = [ - "WCSAXES = 1 / Number of WCS axes", - "CRPIX1 = 0.0 / Reference pixel on axis 1", - "CRVAL1 = 0.0 / Value at ref. pixel on axis 1", - "CNAME1 = 'Wavelength' / Axis name for labeling purposes", - "CTYPE1 = 'WAVE-TAB' / Wavelength axis by lookup table", - "CDELT1 = 1.0 / Pixel size on axis 1", - f"CUNIT1 = '{self.spectrum.wavelength.unit.name:8s}' / Units for axis 1", - f"PV1_1 = {self.wcs_table_ver:20d} / EXTVER of bintable extension for -TAB arrays", - f"PS1_0 = '{self.wcs_table_name:8s}' / EXTNAME of bintable extension for -TAB arrays", - f"PS1_1 = '{self.wcs_column_name:8s}' / Wavelength coordinate array", - ] - for c in wcs_cards: - hdr.append(astropy.io.fits.Card.fromstring(c)) - - return hdr - - def make_primary_hdu(self): - """Return the primary HDU built from a Spectrum.""" - - hdu = astropy.io.fits.PrimaryHDU( - data=self.spectrum.flux, header=self.make_fits_header() - ) - return hdu - - def make_wavelength_hdu(self): - """Return the wavelength HDU built from a Spectrum.""" - - # The wavelength array must be 2D (N, 1) in numpy but (1, N) in FITS - wavelength = self.spectrum.wavelength.reshape([self.spectrum.wavelength.size, 1]) - - # Create a Table. It will be a single element table - table = astropy.table.Table() - - # Create the wavelength column - # Create the column explicitly since it is easier to ensure the - # shape this way. - wavecol = astropy.table.Column([wavelength], unit=wavelength.unit.name) - - # The column name must match the PS1_1 entry from the primary HDU - table[self.wcs_column_name] = wavecol - - # The name MUST match the value of PS1_0 and the version MUST - # match the value of PV1_1 - hdu = astropy.io.fits.BinTableHDU(table, name=self.wcs_table_name, ver=1) - return hdu From 424583150684e5653f4944e2e0ad11f8b416d206 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Fri, 9 Feb 2024 11:52:06 -0800 Subject: [PATCH 32/42] Add data manager. --- .../obs/fiberspectrograph/data_manager.py | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 python/lsst/obs/fiberspectrograph/data_manager.py diff --git a/python/lsst/obs/fiberspectrograph/data_manager.py b/python/lsst/obs/fiberspectrograph/data_manager.py new file mode 100644 index 0000000..5787b6c --- /dev/null +++ b/python/lsst/obs/fiberspectrograph/data_manager.py @@ -0,0 +1,111 @@ +# This file is part of obs_fiberspectrograph +# +# Developed for the LSST Data Management System. +# This product includes software developed by the LSST Project +# (https://www.lsst.org). +# See the COPYRIGHT file at the top-level directory of this distribution +# for details of code ownership. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import astropy.io.fits + + +class DataManager: + """A data packager for `Spectrum` objects + that comes from the ts_fiberspectrograph package + """ + + wcs_table_name = "WCS-TAB" + """Name of the table containing the wavelength WCS (EXTNAME).""" + wcs_table_ver = 1 + """WCS table version (EXTVER).""" + wcs_column_name = "wavelength" + """Name of the table column containing the wavelength information.""" + + # The version of the FITS file format produced by this class. + FORMAT_VERSION = 1 + + def __init__(self, spectrum): + self.spectrum = spectrum + + def make_hdulist(self): + """Generate a FITS hdulist built from SpectrographData. + Parameters + ---------- + spec : `SpectrographData` + The data from which to build the FITS hdulist. + Returns + ------- + hdulist : `astropy.io.fits.HDUList` + The FITS hdulist. + """ + hdu1 = self.make_primary_hdu() + hdu2 = self.make_wavelength_hdu() + return astropy.io.fits.HDUList([hdu1, hdu2]) + + def make_fits_header(self): + """Return a FITS header built from a Spectrum""" + hdr = astropy.io.fits.Header() + + hdr["FORMAT_V"] = self.FORMAT_VERSION + hdr.update(self.spectrum.md) + + # WCS headers - Use -TAB WCS definition + wcs_cards = [ + "WCSAXES = 1 / Number of WCS axes", + "CRPIX1 = 0.0 / Reference pixel on axis 1", + "CRVAL1 = 0.0 / Value at ref. pixel on axis 1", + "CNAME1 = 'Wavelength' / Axis name for labeling purposes", + "CTYPE1 = 'WAVE-TAB' / Wavelength axis by lookup table", + "CDELT1 = 1.0 / Pixel size on axis 1", + f"CUNIT1 = '{self.spectrum.wavelength.unit.name:8s}' / Units for axis 1", + f"PV1_1 = {self.wcs_table_ver:20d} / EXTVER of bintable extension for -TAB arrays", + f"PS1_0 = '{self.wcs_table_name:8s}' / EXTNAME of bintable extension for -TAB arrays", + f"PS1_1 = '{self.wcs_column_name:8s}' / Wavelength coordinate array", + ] + for c in wcs_cards: + hdr.append(astropy.io.fits.Card.fromstring(c)) + + return hdr + + def make_primary_hdu(self): + """Return the primary HDU built from a Spectrum.""" + + hdu = astropy.io.fits.PrimaryHDU( + data=self.spectrum.flux, header=self.make_fits_header() + ) + return hdu + + def make_wavelength_hdu(self): + """Return the wavelength HDU built from a Spectrum.""" + + # The wavelength array must be 2D (N, 1) in numpy but (1, N) in FITS + wavelength = self.spectrum.wavelength.reshape([self.spectrum.wavelength.size, 1]) + + # Create a Table. It will be a single element table + table = astropy.table.Table() + + # Create the wavelength column + # Create the column explicitly since it is easier to ensure the + # shape this way. + wavecol = astropy.table.Column([wavelength], unit=wavelength.unit.name) + + # The column name must match the PS1_1 entry from the primary HDU + table[self.wcs_column_name] = wavecol + + # The name MUST match the value of PS1_0 and the version MUST + # match the value of PV1_1 + hdu = astropy.io.fits.BinTableHDU(table, name=self.wcs_table_name, ver=1) + return hdu From 39c280e154947d91e396dc8d34c7ab99e25e9f9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Fri, 9 Feb 2024 13:04:11 -0800 Subject: [PATCH 33/42] Use observation info. --- python/lsst/obs/fiberspectrograph/spectrum.py | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/python/lsst/obs/fiberspectrograph/spectrum.py b/python/lsst/obs/fiberspectrograph/spectrum.py index 8392c9a..f224d77 100644 --- a/python/lsst/obs/fiberspectrograph/spectrum.py +++ b/python/lsst/obs/fiberspectrograph/spectrum.py @@ -27,22 +27,7 @@ from ._instrument import FiberSpectrograph from .data_manager import DataManager import lsst.afw.image as afwImage - - -class VisitInfo: - def __init__(self, md): - self.exposureTime = md["EXPTIME"] - - def getExposureTime(self): - return self.exposureTime - - -class Info: - def __init__(self, md): - self.visitInfo = VisitInfo(md) - - def getVisitInfo(self): - return self.visitInfo +from astro_metadata_translator import ObservationInfo class FiberSpectrum: @@ -65,6 +50,7 @@ def __init__(self, wavelength, flux, md=None, detectorId=0): self.flux = flux self.md = md + self.info = ObservationInfo(md) self.detector = FiberSpectrograph().getCamera()[detectorId] self.__Mask = afwImage.MaskX(1, 1) From 7848cd415c65523beb5092c13c5e8a53d94fc33b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Thu, 22 Feb 2024 13:38:11 -0800 Subject: [PATCH 34/42] Add new lines. --- .github/workflows/lint.yaml | 2 +- tests/SConscript | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 8330a70..796ef92 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -8,4 +8,4 @@ on: jobs: call-workflow: - uses: lsst/rubin_workflows/.github/workflows/lint.yaml@main \ No newline at end of file + uses: lsst/rubin_workflows/.github/workflows/lint.yaml@main diff --git a/tests/SConscript b/tests/SConscript index 76a9e2b..23d3252 100644 --- a/tests/SConscript +++ b/tests/SConscript @@ -5,4 +5,4 @@ from lsst.sconsUtils import env, scripts scripts.BasicSConscript.tests(pyList=[]) if "DAF_BUTLER_PLUGINS" in os.environ: - env["ENV"]["DAF_BUTLER_PLUGINS"] = os.environ["DAF_BUTLER_PLUGINS"] \ No newline at end of file + env["ENV"]["DAF_BUTLER_PLUGINS"] = os.environ["DAF_BUTLER_PLUGINS"] From c2ba25be0718d624772f1df05deb22cdb3701de7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Thu, 22 Feb 2024 13:43:02 -0800 Subject: [PATCH 35/42] Replace instance attribute to metadata. --- python/lsst/obs/fiberspectrograph/spectrum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lsst/obs/fiberspectrograph/spectrum.py b/python/lsst/obs/fiberspectrograph/spectrum.py index f224d77..389f6f1 100644 --- a/python/lsst/obs/fiberspectrograph/spectrum.py +++ b/python/lsst/obs/fiberspectrograph/spectrum.py @@ -48,7 +48,7 @@ class FiberSpectrum: def __init__(self, wavelength, flux, md=None, detectorId=0): self.wavelength = wavelength self.flux = flux - self.md = md + self.metadata = md self.info = ObservationInfo(md) self.detector = FiberSpectrograph().getCamera()[detectorId] From 740e31d023d25dcf668438d99f057e5f5185c064 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Thu, 22 Feb 2024 13:48:00 -0800 Subject: [PATCH 36/42] Changed attribute to temporary variable. --- python/lsst/obs/fiberspectrograph/spectrum.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/python/lsst/obs/fiberspectrograph/spectrum.py b/python/lsst/obs/fiberspectrograph/spectrum.py index 389f6f1..193aac6 100644 --- a/python/lsst/obs/fiberspectrograph/spectrum.py +++ b/python/lsst/obs/fiberspectrograph/spectrum.py @@ -53,9 +53,9 @@ def __init__(self, wavelength, flux, md=None, detectorId=0): self.info = ObservationInfo(md) self.detector = FiberSpectrograph().getCamera()[detectorId] - self.__Mask = afwImage.MaskX(1, 1) - self.getPlaneBitMask = self.__Mask.getPlaneBitMask # ughh, awful Mask API - self.mask = np.zeros(flux.shape, dtype=self.__Mask.array.dtype) + mask = afwImage.MaskX(1, 1) + self.getPlaneBitMask = mask.getPlaneBitMask # ughh, awful Mask API + self.mask = np.zeros(flux.shape, dtype=mask.array.dtype) self.variance = np.zeros_like(flux) def getDetector(self): From 0dbe239a60df8df187be979e92db79902bf96fc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Thu, 22 Feb 2024 14:37:31 -0800 Subject: [PATCH 37/42] Add doc strings. --- python/lsst/obs/fiberspectrograph/spectrum.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/python/lsst/obs/fiberspectrograph/spectrum.py b/python/lsst/obs/fiberspectrograph/spectrum.py index 193aac6..ee76bfe 100644 --- a/python/lsst/obs/fiberspectrograph/spectrum.py +++ b/python/lsst/obs/fiberspectrograph/spectrum.py @@ -59,18 +59,28 @@ def __init__(self, wavelength, flux, md=None, detectorId=0): self.variance = np.zeros_like(flux) def getDetector(self): + """Get fiber spectrograph detector" + """ return self.detector def getInfo(self): + """Get observation information" + """ return self.info def getMetadata(self): + """Get the spectrum metadata" + """ return self.md def getFilter(self): + """Get filter label" + """ return FiberSpectrograph.filterDefinitions[0].makeFilterLabel() def getBBox(self): + """Get bounding box" + """ return self.detector.getBBox() @classmethod From de5467a73c814a7fe6feabc08240667ef6ce0803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Thu, 22 Feb 2024 14:41:30 -0800 Subject: [PATCH 38/42] Add raise when format v changes value. --- python/lsst/obs/fiberspectrograph/spectrum.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/python/lsst/obs/fiberspectrograph/spectrum.py b/python/lsst/obs/fiberspectrograph/spectrum.py index ee76bfe..f6fb299 100644 --- a/python/lsst/obs/fiberspectrograph/spectrum.py +++ b/python/lsst/obs/fiberspectrograph/spectrum.py @@ -31,7 +31,7 @@ class FiberSpectrum: - """Define a spectrum from a fiber spectrograph + """Define a spectrum from a fiber spectrograph. Parameters ---------- @@ -59,33 +59,33 @@ def __init__(self, wavelength, flux, md=None, detectorId=0): self.variance = np.zeros_like(flux) def getDetector(self): - """Get fiber spectrograph detector" + """Get fiber spectrograph detector." """ return self.detector def getInfo(self): - """Get observation information" + """Get observation information." """ return self.info def getMetadata(self): - """Get the spectrum metadata" + """Get the spectrum metadata." """ return self.md def getFilter(self): - """Get filter label" + """Get filter label." """ return FiberSpectrograph.filterDefinitions[0].makeFilterLabel() def getBBox(self): - """Get bounding box" + """Get bounding box." """ return self.detector.getBBox() @classmethod def readFits(cls, path): - """Read a Spectrum from disk" + """Read a Spectrum from disk." Parameters ---------- @@ -101,16 +101,18 @@ def readFits(cls, path): fitsfile = astropy.io.fits.open(path) md = dict(fitsfile[0].header) - if md["FORMAT_V"] >= 1: + if md["FORMAT_V"] == 1: flux = fitsfile[0].data wavelength = fitsfile[md["PS1_0"]].data[md["PS1_1"]].flatten() wavelength = u.Quantity(wavelength, u.Unit(md["CUNIT1"]), copy=False) + else: + raise ValueError(f"FORMAT_V has changed from 1 to {md["FORMAT_V"]}") return cls(wavelength, flux, md) def writeFits(self, path): - """Write a Spectrum to disk + """Write a Spectrum to disk. Parameters ---------- From 6a731876da55dcbc2decbd7b40cfdc33087b61e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Thu, 22 Feb 2024 14:46:15 -0800 Subject: [PATCH 39/42] Small update. Fix typo. Complete gitignore. --- .gitignore | 1 + python/lsst/obs/fiberspectrograph/translator.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index bb98353..37b3b3a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .sconsign.dblite config.log .sconf_temp +.scons* *.o *.os *.so diff --git a/python/lsst/obs/fiberspectrograph/translator.py b/python/lsst/obs/fiberspectrograph/translator.py index 34d8a96..4fcfd4b 100644 --- a/python/lsst/obs/fiberspectrograph/translator.py +++ b/python/lsst/obs/fiberspectrograph/translator.py @@ -21,7 +21,7 @@ class FiberSpectrographTranslator(LsstBaseTranslator): """Name of this translation class""" supported_instrument = "FiberSpec" - """Supports the Rubin calibration fibre spectrographs.""" + """Supports the Rubin calibration fiber spectrographs.""" default_search_path = os.path.join(getPackageDir("obs_fiberspectrograph"), "corrections") """Default search path to use to locate header correction files.""" From 9a9a293fb416ab6e4ccf4d9aabdbb3f3fa096a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Mon, 26 Feb 2024 16:51:53 -0800 Subject: [PATCH 40/42] Write and read mask and variance plane and change check on formatv. --- .../obs/fiberspectrograph/data_manager.py | 22 +++++++++++++++-- python/lsst/obs/fiberspectrograph/spectrum.py | 24 ++++++++++++------- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/python/lsst/obs/fiberspectrograph/data_manager.py b/python/lsst/obs/fiberspectrograph/data_manager.py index 5787b6c..389fcf6 100644 --- a/python/lsst/obs/fiberspectrograph/data_manager.py +++ b/python/lsst/obs/fiberspectrograph/data_manager.py @@ -53,14 +53,15 @@ def make_hdulist(self): """ hdu1 = self.make_primary_hdu() hdu2 = self.make_wavelength_hdu() - return astropy.io.fits.HDUList([hdu1, hdu2]) + hdu3, hdu4 = self.make_maskvariance_hdu() + return astropy.io.fits.HDUList([hdu1, hdu2, hdu3, hdu4]) def make_fits_header(self): """Return a FITS header built from a Spectrum""" hdr = astropy.io.fits.Header() hdr["FORMAT_V"] = self.FORMAT_VERSION - hdr.update(self.spectrum.md) + hdr.update(self.spectrum.metadata) # WCS headers - Use -TAB WCS definition wcs_cards = [ @@ -88,6 +89,23 @@ def make_primary_hdu(self): ) return hdu + def make_maskvariance_hdu(self): + """Return the HDU for the mask and variance plane.""" + hdr_mask = astropy.io.fits.Header() + hdr_mask["EXTTYPE"] = 'MASK ' + hdr_mask["EXTNAME"] = 'MASK ' + hdu_mask = astropy.io.fits.ImageHDU( + data=self.spectrum.mask, header=hdr_mask + ) + + hdr_variance = astropy.io.fits.Header() + hdr_variance["EXTTYPE"] = 'VARIANCE' + hdr_variance["EXTNAME"] = 'VARIANCE' + hdu_variance = astropy.io.fits.ImageHDU( + data=self.spectrum.variance, header=hdr_variance + ) + return hdu_mask, hdu_variance + def make_wavelength_hdu(self): """Return the wavelength HDU built from a Spectrum.""" diff --git a/python/lsst/obs/fiberspectrograph/spectrum.py b/python/lsst/obs/fiberspectrograph/spectrum.py index f6fb299..71bbf28 100644 --- a/python/lsst/obs/fiberspectrograph/spectrum.py +++ b/python/lsst/obs/fiberspectrograph/spectrum.py @@ -45,7 +45,7 @@ class FiberSpectrum: Optional Detector ID for this data. """ - def __init__(self, wavelength, flux, md=None, detectorId=0): + def __init__(self, wavelength, flux, md=None, detectorId=0, mask=None, variance=None): self.wavelength = wavelength self.flux = flux self.metadata = md @@ -53,10 +53,10 @@ def __init__(self, wavelength, flux, md=None, detectorId=0): self.info = ObservationInfo(md) self.detector = FiberSpectrograph().getCamera()[detectorId] - mask = afwImage.MaskX(1, 1) - self.getPlaneBitMask = mask.getPlaneBitMask # ughh, awful Mask API - self.mask = np.zeros(flux.shape, dtype=mask.array.dtype) - self.variance = np.zeros_like(flux) + mask_temp = afwImage.MaskX(1, 1) + self.getPlaneBitMask = mask_temp.getPlaneBitMask # ughh, awful Mask API + self.mask = mask + self.variance = variance def getDetector(self): """Get fiber spectrograph detector." @@ -100,16 +100,24 @@ def readFits(cls, path): fitsfile = astropy.io.fits.open(path) md = dict(fitsfile[0].header) + format_v = md["FORMAT_V"] - if md["FORMAT_V"] == 1: + if format_v == 1: flux = fitsfile[0].data wavelength = fitsfile[md["PS1_0"]].data[md["PS1_1"]].flatten() wavelength = u.Quantity(wavelength, u.Unit(md["CUNIT1"]), copy=False) + + mask_temp = afwImage.MaskX(1, 1) + mask = np.zeros(flux.shape, dtype=mask_temp.array.dtype) + variance = np.zeros_like(flux) + if len(fitsfile) == 4: + mask = fitsfile[2].data + variance = fitsfile[3].data else: - raise ValueError(f"FORMAT_V has changed from 1 to {md["FORMAT_V"]}") + raise ValueError(f"FORMAT_V has changed from 1 to {format_v}") - return cls(wavelength, flux, md) + return cls(wavelength, flux, md=md, mask=mask, variance=variance) def writeFits(self, path): """Write a Spectrum to disk. From 8fb4874d91295a5e4e248b44a1b915b58e023204 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Mon, 26 Feb 2024 16:53:48 -0800 Subject: [PATCH 41/42] Update translator. Update to dos and serial number mapping. Update comments. --- python/lsst/obs/fiberspectrograph/translator.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/python/lsst/obs/fiberspectrograph/translator.py b/python/lsst/obs/fiberspectrograph/translator.py index 4fcfd4b..6621227 100644 --- a/python/lsst/obs/fiberspectrograph/translator.py +++ b/python/lsst/obs/fiberspectrograph/translator.py @@ -32,11 +32,12 @@ class FiberSpectrographTranslator(LsstBaseTranslator): DETECTOR_MAX = 1 _const_map = { + # TODO: DM-43041 DATE, detector name and controller should be put + # in file header and add to mapping "detector_num": 0, - "detector_name": "0", + "detector_name": "ccd0", "object": None, "physical_filter": "empty", - "detector_serial": "1606191U1", "detector_group": "None", "relative_humidity": None, "pressure": None, @@ -53,6 +54,7 @@ class FiberSpectrographTranslator(LsstBaseTranslator): _trivial_map = { "observation_id": "OBSID", "science_program": ("PROGRAM", dict(default="unknown")), + "detector_serial": "SERIAL", } """One-to-one mappings""" @@ -75,6 +77,7 @@ def can_translate(cls, header, filename=None): otherwise. """ + # TODO: DM-43041 need to be updated with new fiber spec return "INSTRUME" in header and header["INSTRUME"] in ["FiberSpectrograph.Broad"] @cache_translation From c255fbcd07831a84b489df25ccb4efd1860d6c77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agn=C3=A8s=20Fert=C3=A9?= Date: Thu, 29 Feb 2024 11:49:41 -0800 Subject: [PATCH 42/42] Correct attribute in returm. --- python/lsst/obs/fiberspectrograph/spectrum.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/lsst/obs/fiberspectrograph/spectrum.py b/python/lsst/obs/fiberspectrograph/spectrum.py index 71bbf28..cb1c536 100644 --- a/python/lsst/obs/fiberspectrograph/spectrum.py +++ b/python/lsst/obs/fiberspectrograph/spectrum.py @@ -71,7 +71,7 @@ def getInfo(self): def getMetadata(self): """Get the spectrum metadata." """ - return self.md + return self.metadata def getFilter(self): """Get filter label."