Coverage for python/lsst/obs/subaru/instrument.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
1# This file is part of obs_subaru.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (http://www.lsst.org).
6# See the COPYRIGHT file at the top-level directory of this distribution
7# for details of code ownership.
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
22"""Gen3 Butler registry declarations for Hyper Suprime-Cam.
23"""
25__all__ = ("HyperSuprimeCam",)
27import os
28import pickle
29import logging
31import astropy.time
32from lsst.utils import getPackageDir
33from lsst.afw.cameraGeom import makeCameraFromPath, CameraConfig
34from lsst.daf.butler import (DatasetType, DataCoordinate, FileDataset, DatasetRef,
35 TIMESPAN_MIN)
36from lsst.daf.butler.core.utils import getFullTypeName
37from lsst.obs.base import Instrument, addUnboundedCalibrationLabel
39from ..hsc.hscPupil import HscPupilFactory
40from ..hsc.hscFilters import HSC_FILTER_DEFINITIONS
41from ..hsc.makeTransmissionCurves import (getSensorTransmission, getOpticsTransmission,
42 getFilterTransmission, getAtmosphereTransmission)
43from .strayLight.formatter import SubaruStrayLightDataFormatter
45log = logging.getLogger(__name__)
48class HyperSuprimeCam(Instrument):
49 """Gen3 Butler specialization class for Subaru's Hyper Suprime-Cam.
50 """
52 policyName = "hsc"
53 obsDataPackage = "obs_subaru_data"
54 filterDefinitions = HSC_FILTER_DEFINITIONS
56 def __init__(self, **kwargs):
57 super().__init__(**kwargs)
58 packageDir = getPackageDir("obs_subaru")
59 self.configPaths = [os.path.join(packageDir, "config"),
60 os.path.join(packageDir, "config", self.policyName)]
62 @classmethod
63 def getName(cls):
64 # Docstring inherited from Instrument.getName
65 return "HSC"
67 def register(self, registry):
68 # Docstring inherited from Instrument.register
69 camera = self.getCamera()
70 # The maximum values below make Gen3's ObservationDataIdPacker produce
71 # outputs that match Gen2's ccdExposureId.
72 obsMax = 21474800
73 registry.insertDimensionData(
74 "instrument",
75 {
76 "name": self.getName(),
77 "detector_max": 200,
78 "visit_max": obsMax,
79 "exposure_max": obsMax,
80 "class_name": getFullTypeName(self),
81 }
82 )
83 registry.insertDimensionData(
84 "detector",
85 *[
86 {
87 "instrument": self.getName(),
88 "id": detector.getId(),
89 "full_name": detector.getName(),
90 # TODO: make sure these definitions are consistent with those
91 # extracted by astro_metadata_translator, and test that they
92 # remain consistent somehow.
93 "name_in_raft": detector.getName().split("_")[1],
94 "raft": detector.getName().split("_")[0],
95 "purpose": str(detector.getType()).split(".")[-1],
96 }
97 for detector in camera
98 ]
99 )
100 self._registerFilters(registry)
102 def getRawFormatter(self, dataId):
103 # Docstring inherited from Instrument.getRawFormatter
104 # Import the formatter here to prevent a circular dependency.
105 from .rawFormatter import HyperSuprimeCamRawFormatter, HyperSuprimeCamCornerRawFormatter
106 if dataId["detector"] in (100, 101, 102, 103):
107 return HyperSuprimeCamCornerRawFormatter
108 else:
109 return HyperSuprimeCamRawFormatter
111 def getCamera(self):
112 """Retrieve the cameraGeom representation of HSC.
114 This is a temporary API that should go away once obs_ packages have
115 a standardized approach to writing versioned cameras to a Gen3 repo.
116 """
117 path = os.path.join(getPackageDir("obs_subaru"), self.policyName, "camera")
118 config = CameraConfig()
119 config.load(os.path.join(path, "camera.py"))
120 return makeCameraFromPath(
121 cameraConfig=config,
122 ampInfoPath=path,
123 shortNameFunc=lambda name: name.replace(" ", "_"),
124 pupilFactoryClass=HscPupilFactory
125 )
127 def getBrighterFatterKernel(self):
128 """Return the brighter-fatter kernel for HSC as a `numpy.ndarray`.
130 This is a temporary API that should go away once obs_ packages have
131 a standardized approach to writing versioned kernels to a Gen3 repo.
132 """
133 path = os.path.join(getPackageDir("obs_subaru"), self.policyName, "brighter_fatter_kernel.pkl")
134 with open(path, "rb") as fd:
135 kernel = pickle.load(fd, encoding='latin1') # encoding for pickle written with Python 2
136 return kernel
138 def writeCuratedCalibrations(self, butler):
139 """Write human-curated calibration Datasets to the given Butler with
140 the appropriate validity ranges.
142 This is a temporary API that should go away once obs_ packages have
143 a standardized approach to this problem.
144 """
146 # Write the generic calibrations supported by all instruments
147 super().writeCuratedCalibrations(butler)
149 # Get an unbounded calibration label
150 unboundedDataId = addUnboundedCalibrationLabel(butler.registry, self.getName())
152 # Write brighter-fatter kernel, with an infinite validity range.
153 datasetType = DatasetType("bfKernel", ("instrument", "calibration_label"), "NumpyArray",
154 universe=butler.registry.dimensions)
155 butler.registry.registerDatasetType(datasetType)
156 # Load and then put instead of just moving the file in part to ensure
157 # the version in-repo is written with Python 3 and does not need
158 # `encoding='latin1'` to be read.
159 bfKernel = self.getBrighterFatterKernel()
160 butler.put(bfKernel, datasetType, unboundedDataId)
162 # The following iterate over the values of the dictionaries returned by the transmission functions
163 # and ignore the date that is supplied. This is due to the dates not being ranges but single dates,
164 # which do not give the proper notion of validity. As such unbounded calibration labels are used
165 # when inserting into the database. In the future these could and probably should be updated to
166 # properly account for what ranges are considered valid.
168 # Write optical transmissions
169 opticsTransmissions = getOpticsTransmission()
170 datasetType = DatasetType("transmission_optics",
171 ("instrument", "calibration_label"),
172 "TransmissionCurve",
173 universe=butler.registry.dimensions)
174 butler.registry.registerDatasetType(datasetType)
175 for entry in opticsTransmissions.values():
176 if entry is None:
177 continue
178 butler.put(entry, datasetType, unboundedDataId)
180 # Write transmission sensor
181 sensorTransmissions = getSensorTransmission()
182 datasetType = DatasetType("transmission_sensor",
183 ("instrument", "detector", "calibration_label"),
184 "TransmissionCurve",
185 universe=butler.registry.dimensions)
186 butler.registry.registerDatasetType(datasetType)
187 for entry in sensorTransmissions.values():
188 if entry is None:
189 continue
190 for sensor, curve in entry.items():
191 dataId = DataCoordinate.standardize(unboundedDataId, detector=sensor)
192 butler.put(curve, datasetType, dataId)
194 # Write filter transmissions
195 filterTransmissions = getFilterTransmission()
196 datasetType = DatasetType("transmission_filter",
197 ("instrument", "physical_filter", "calibration_label"),
198 "TransmissionCurve",
199 universe=butler.registry.dimensions)
200 butler.registry.registerDatasetType(datasetType)
201 for entry in filterTransmissions.values():
202 if entry is None:
203 continue
204 for band, curve in entry.items():
205 dataId = DataCoordinate.standardize(unboundedDataId, physical_filter=band)
206 butler.put(curve, datasetType, dataId)
208 # Write atmospheric transmissions, this only as dimension of instrument as other areas will only
209 # look up along this dimension (ISR)
210 atmosphericTransmissions = getAtmosphereTransmission()
211 datasetType = DatasetType("transmission_atmosphere", ("instrument",),
212 "TransmissionCurve",
213 universe=butler.registry.dimensions)
214 butler.registry.registerDatasetType(datasetType)
215 for entry in atmosphericTransmissions.values():
216 if entry is None:
217 continue
218 butler.put(entry, datasetType, {"instrument": self.getName()})
220 def ingestStrayLightData(self, butler, directory, *, transfer=None):
221 """Ingest externally-produced y-band stray light data files into
222 a data repository.
224 Parameters
225 ----------
226 butler : `lsst.daf.butler.Butler`
227 Butler initialized with the collection to ingest into.
228 directory : `str`
229 Directory containing yBackground-*.fits files.
230 transfer : `str`, optional
231 If not `None`, must be one of 'move', 'copy', 'hardlink', or
232 'symlink', indicating how to transfer the files.
233 """
234 calibrationLabel = "y-LED-encoder-on"
235 # LEDs covered up around 2018-01-01, no need for correctin after that
236 # date.
237 datetime_begin = TIMESPAN_MIN
238 datetime_end = astropy.time.Time("2018-01-01", format="iso", scale="tai")
239 datasets = []
240 # TODO: should we use a more generic name for the dataset type?
241 # This is just the (rather HSC-specific) name used in Gen2, and while
242 # the instances of this dataset are camera-specific, the datasetType
243 # (which is used in the generic IsrTask) should not be.
244 datasetType = DatasetType("yBackground",
245 dimensions=("physical_filter", "detector", "calibration_label"),
246 storageClass="StrayLightData",
247 universe=butler.registry.dimensions)
248 for detector in self.getCamera():
249 path = os.path.join(directory, f"ybackground-{detector.getId():03d}.fits")
250 if not os.path.exists(path):
251 log.warn(f"No stray light data found for detector {detector.getId()} @ {path}.")
252 continue
253 ref = DatasetRef(datasetType, dataId={"instrument": self.getName(),
254 "detector": detector.getId(),
255 "physical_filter": "HSC-Y",
256 "calibration_label": calibrationLabel})
257 datasets.append(FileDataset(refs=ref, path=path, formatter=SubaruStrayLightDataFormatter))
258 with butler.transaction():
259 butler.registry.registerDatasetType(datasetType)
260 butler.registry.insertDimensionData("calibration_label", {"instrument": self.getName(),
261 "name": calibrationLabel,
262 "datetime_begin": datetime_begin,
263 "datetime_end": datetime_end})
264 butler.ingest(*datasets, transfer=transfer)