22 __all__ = (
"Instrument",
"makeExposureRecordFromObsInfo",
"addUnboundedCalibrationLabel",
"loadCamera")
25 from abc
import ABCMeta, abstractmethod
26 from typing
import Any, Tuple
29 from lsst.afw.cameraGeom
import Camera
30 from lsst.daf.butler
import Butler, DataId, TIMESPAN_MIN, TIMESPAN_MAX, DatasetType, DataCoordinate
35 StandardCuratedCalibrationDatasetTypes = {
36 "defects": {
"dimensions": (
"instrument",
"detector",
"calibration_label"),
37 "storageClass":
"Defects"},
38 "qe_curve": {
"dimensions": (
"instrument",
"detector",
"calibration_label"),
39 "storageClass":
"QECurve"},
44 """Base class for instrument-specific logic for the Gen3 Butler.
46 Concrete instrument subclasses should be directly constructable with no
51 """Paths to config files to read for specific Tasks.
53 The paths in this list should contain files of the form `task.py`, for
54 each of the Tasks that requires special configuration.
58 """Instrument specific name to use when locating a policy or configuration
59 file in the file system."""
62 """Name of the package containing the text curated calibration files.
63 Usually a obs _data package. If `None` no curated calibration files
64 will be read. (`str`)"""
66 standardCuratedDatasetTypes = tuple(StandardCuratedCalibrationDatasetTypes)
67 """The dataset types expected to be obtained from the obsDataPackage.
68 These dataset types are all required to have standard definitions and
69 must be known to the base class. Clearing this list will prevent
70 any of these calibrations from being stored. If a dataset type is not
71 known to a specific instrument it can still be included in this list
72 since the data package is the source of truth.
78 """`~lsst.obs.base.FilterDefinitionCollection`, defining the filters
91 raise NotImplementedError()
95 """Retrieve the cameraGeom representation of this instrument.
97 This is a temporary API that should go away once obs_ packages have
98 a standardized approach to writing versioned cameras to a Gen3 repo.
100 raise NotImplementedError()
104 """Insert instrument, physical_filter, and detector entries into a
107 raise NotImplementedError()
121 """Given an instrument name and a butler, retrieve a corresponding
122 instantiated instrument object.
127 Name of the instrument (must match the name property of
128 an instrument class).
129 registry : `lsst.daf.butler.Registry`
130 Butler registry to query to find the information.
134 instrument : `Instrument`
135 An instance of the relevant `Instrument`.
139 The instrument must be registered in the corresponding butler.
144 Raised if the instrument is not known to the supplied registry.
146 Raised if the class could not be imported. This could mean
147 that the relevant obs package has not been setup.
149 Raised if the class name retrieved is not a string.
151 dimensions = list(registry.queryDimensions(
"instrument", dataId={
"instrument": name}))
152 cls = dimensions[0].records[
"instrument"].class_name
153 if not isinstance(cls, str):
154 raise TypeError(f
"Unexpected class name retrieved from {name} instrument dimension (got {cls})")
155 instrument = doImport(cls)
158 def _registerFilters(self, registry):
159 """Register the physical and abstract filter Dimension relationships.
160 This should be called in the ``register`` implementation.
164 registry : `lsst.daf.butler.core.Registry`
165 The registry to add dimensions to.
169 if filter.abstract_filter
is None:
170 abstract_filter = filter.physical_filter
172 abstract_filter = filter.abstract_filter
174 registry.insertDimensionData(
"physical_filter",
176 "name": filter.physical_filter,
177 "abstract_filter": abstract_filter
182 """Return the Formatter class that should be used to read a particular
187 dataId : `DataCoordinate`
188 Dimension-based ID for the raw file or files being ingested.
192 formatter : `Formatter` class
193 Class to be used that reads the file into an
194 `lsst.afw.image.Exposure` instance.
196 raise NotImplementedError()
199 """Write human-curated calibration Datasets to the given Butler with
200 the appropriate validity ranges.
204 butler : `lsst.daf.butler.Butler`
205 Butler to use to store these calibrations.
209 Expected to be called from subclasses. The base method calls
210 ``writeCameraGeom`` and ``writeStandardTextCuratedCalibrations``.
216 """Apply instrument-specific overrides for a task config.
221 Name of the object being configured; typically the _DefaultName
223 config : `lsst.pex.config.Config`
224 Config instance to which overrides should be applied.
227 path = os.path.join(root, f
"{name}.py")
228 if os.path.exists(path):
232 """Write the default camera geometry to the butler repository
233 with an infinite validity range.
237 butler : `lsst.daf.butler.Butler`
238 Butler to receive these calibration datasets.
241 datasetType = DatasetType(
"camera", (
"instrument",
"calibration_label"),
"Camera",
242 universe=butler.registry.dimensions)
243 butler.registry.registerDatasetType(datasetType)
246 butler.put(camera, datasetType, unboundedDataId)
249 """Write the set of standardized curated text calibrations to
254 butler : `lsst.daf.butler.Butler`
255 Butler to receive these calibration datasets.
260 if datasetTypeName
not in StandardCuratedCalibrationDatasetTypes:
261 raise ValueError(f
"DatasetType {datasetTypeName} not in understood list"
262 f
" [{'.'.join(StandardCuratedCalibrationDatasetTypes)}]")
263 definition = StandardCuratedCalibrationDatasetTypes[datasetTypeName]
264 datasetType = DatasetType(datasetTypeName,
265 universe=butler.registry.dimensions,
269 def _writeSpecificCuratedCalibrationDatasets(self, butler, datasetType):
270 """Write standardized curated calibration datasets for this specific
271 dataset type from an obs data package.
275 butler : `lsst.daf.butler.Butler`
276 Gen3 butler in which to put the calibrations.
277 datasetType : `lsst.daf.butler.DatasetType`
278 Dataset type to be put.
282 This method scans the location defined in the ``obsDataPackageDir``
283 class attribute for curated calibrations corresponding to the
284 supplied dataset type. The directory name in the data package must
285 match the name of the dataset type. They are assumed to use the
286 standard layout and can be read by
287 `~lsst.pipe.tasks.read_curated_calibs.read_all` and provide standard
297 if not os.path.exists(calibPath):
301 butler.registry.registerDatasetType(datasetType)
305 from lsst.pipe.tasks.read_curated_calibs
import read_all
308 calibsDict = read_all(calibPath, camera)[0]
309 endOfTime = TIMESPAN_MAX
310 dimensionRecords = []
312 for det
in calibsDict:
313 times = sorted([k
for k
in calibsDict[det]])
314 calibs = [calibsDict[det][time]
for time
in times]
315 times = [astropy.time.Time(t, format=
"datetime", scale=
"utc")
for t
in times]
317 for calib, beginTime, endTime
in zip(calibs, times[:-1], times[1:]):
318 md = calib.getMetadata()
319 calibrationLabel = f
"{datasetType.name}/{md['CALIBDATE']}/{md['DETECTOR']}"
320 dataId = DataCoordinate.standardize(
321 universe=butler.registry.dimensions,
323 calibration_label=calibrationLabel,
324 detector=md[
"DETECTOR"],
326 datasetRecords.append((calib, dataId))
327 dimensionRecords.append({
329 "name": calibrationLabel,
330 "datetime_begin": beginTime,
331 "datetime_end": endTime,
335 with butler.transaction():
336 butler.registry.insertDimensionData(
"calibration_label", *dimensionRecords)
339 for calib, dataId
in datasetRecords:
340 butler.put(calib, datasetType, dataId)
344 """Construct an exposure DimensionRecord from
345 `astro_metadata_translator.ObservationInfo`.
349 obsInfo : `astro_metadata_translator.ObservationInfo`
350 A `~astro_metadata_translator.ObservationInfo` object corresponding to
352 universe : `DimensionUniverse`
353 Set of all known dimensions.
357 record : `DimensionRecord`
358 A record containing exposure metadata, suitable for insertion into
361 dimension = universe[
"exposure"]
362 return dimension.RecordClass.fromDict({
363 "instrument": obsInfo.instrument,
364 "id": obsInfo.exposure_id,
365 "name": obsInfo.observation_id,
366 "group_name": obsInfo.exposure_group,
367 "group_id": obsInfo.visit_id,
368 "datetime_begin": obsInfo.datetime_begin,
369 "datetime_end": obsInfo.datetime_end,
370 "exposure_time": obsInfo.exposure_time.to_value(
"s"),
371 "dark_time": obsInfo.dark_time.to_value(
"s"),
372 "observation_type": obsInfo.observation_type,
373 "physical_filter": obsInfo.physical_filter,
378 """Add a special 'unbounded' calibration_label dimension entry for the
379 given camera that is valid for any exposure.
381 If such an entry already exists, this function just returns a `DataId`
382 for the existing entry.
386 registry : `Registry`
387 Registry object in which to insert the dimension entry.
388 instrumentName : `str`
389 Name of the instrument this calibration label is associated with.
394 New or existing data ID for the unbounded calibration.
396 d = dict(instrument=instrumentName, calibration_label=
"unbounded")
398 return registry.expandDataId(d)
402 entry[
"datetime_begin"] = TIMESPAN_MIN
403 entry[
"datetime_end"] = TIMESPAN_MAX
404 registry.insertDimensionData(
"calibration_label", entry)
405 return registry.expandDataId(d)
408 def loadCamera(butler: Butler, dataId: DataId, *, collections: Any =
None) -> Tuple[Camera, bool]:
409 """Attempt to load versioned camera geometry from a butler, but fall back
410 to obtaining a nominal camera from the `Instrument` class if that fails.
414 butler : `lsst.daf.butler.Butler`
415 Butler instance to attempt to query for and load a ``camera`` dataset
417 dataId : `dict` or `DataCoordinate`
418 Data ID that identifies at least the ``instrument`` and ``exposure``
420 collections : Any, optional
421 Collections to be searched, overriding ``self.butler.collections``.
422 Can be any of the types supported by the ``collections`` argument
423 to butler construction.
427 camera : `lsst.afw.cameraGeom.Camera`
430 If `True`, the camera was obtained from the butler and should represent
431 a versioned camera from a calibration repository. If `False`, no
432 camera datasets were found, and the returned camera was produced by
433 instantiating the appropriate `Instrument` class and calling
434 `Instrument.getCamera`.
436 if collections
is None:
437 collections = butler.collections
442 dataId = butler.registry.expandDataId(dataId, graph=butler.registry.dimensions[
"exposure"].graph)
443 cameraRefs = list(butler.registry.queryDatasets(
"camera", dataId=dataId, collections=collections,
446 assert len(cameraRefs) == 1,
"Should be guaranteed by deduplicate=True above."
447 return butler.getDirect(cameraRefs[0]),
True
448 instrument = Instrument.fromName(dataId[
"instrument"], butler.registry)
449 return instrument.getCamera(),
False