22 from __future__
import annotations
24 __all__ = (
"Instrument",
"makeExposureRecordFromObsInfo",
"addUnboundedCalibrationLabel",
"loadCamera")
27 from abc
import ABCMeta, abstractmethod
28 from typing
import Any, Tuple, TYPE_CHECKING
32 from lsst.daf.butler
import Butler, DataId, TIMESPAN_MIN, TIMESPAN_MAX, DatasetType, DataCoordinate
36 from .gen2to3
import TranslatorFactory
40 StandardCuratedCalibrationDatasetTypes = {
41 "defects": {
"dimensions": (
"instrument",
"detector",
"calibration_label"),
42 "storageClass":
"Defects"},
43 "qe_curve": {
"dimensions": (
"instrument",
"detector",
"calibration_label"),
44 "storageClass":
"QECurve"},
49 """Base class for instrument-specific logic for the Gen3 Butler.
51 Concrete instrument subclasses should be directly constructable with no
56 """Paths to config files to read for specific Tasks.
58 The paths in this list should contain files of the form `task.py`, for
59 each of the Tasks that requires special configuration.
63 """Instrument specific name to use when locating a policy or configuration
64 file in the file system."""
67 """Name of the package containing the text curated calibration files.
68 Usually a obs _data package. If `None` no curated calibration files
69 will be read. (`str`)"""
71 standardCuratedDatasetTypes = tuple(StandardCuratedCalibrationDatasetTypes)
72 """The dataset types expected to be obtained from the obsDataPackage.
73 These dataset types are all required to have standard definitions and
74 must be known to the base class. Clearing this list will prevent
75 any of these calibrations from being stored. If a dataset type is not
76 known to a specific instrument it can still be included in this list
77 since the data package is the source of truth.
83 """`~lsst.obs.base.FilterDefinitionCollection`, defining the filters
96 """Return the short (dimension) name for this instrument.
98 This is not (in general) the same as the class name - it's what is used
99 as the value of the "instrument" field in data IDs, and is usually an
100 abbreviation of the full name.
102 raise NotImplementedError()
106 """Retrieve the cameraGeom representation of this instrument.
108 This is a temporary API that should go away once obs_ packages have
109 a standardized approach to writing versioned cameras to a Gen3 repo.
111 raise NotImplementedError()
115 """Insert instrument, physical_filter, and detector entries into a
118 raise NotImplementedError()
122 """The root of the obs package that provides specializations for
123 this instrument (`str`).
135 """Given an instrument name and a butler, retrieve a corresponding
136 instantiated instrument object.
141 Name of the instrument (must match the return value of `getName`).
142 registry : `lsst.daf.butler.Registry`
143 Butler registry to query to find the information.
147 instrument : `Instrument`
148 An instance of the relevant `Instrument`.
152 The instrument must be registered in the corresponding butler.
157 Raised if the instrument is not known to the supplied registry.
159 Raised if the class could not be imported. This could mean
160 that the relevant obs package has not been setup.
162 Raised if the class name retrieved is not a string.
164 dimensions =
list(registry.queryDimensions(
"instrument", dataId={
"instrument": name}))
165 cls = dimensions[0].records[
"instrument"].class_name
166 if not isinstance(cls, str):
167 raise TypeError(f
"Unexpected class name retrieved from {name} instrument dimension (got {cls})")
171 def _registerFilters(self, registry):
172 """Register the physical and abstract filter Dimension relationships.
173 This should be called in the ``register`` implementation.
177 registry : `lsst.daf.butler.core.Registry`
178 The registry to add dimensions to.
182 if filter.abstract_filter
is None:
183 abstract_filter = filter.physical_filter
185 abstract_filter = filter.abstract_filter
187 registry.insertDimensionData(
"physical_filter",
189 "name": filter.physical_filter,
190 "abstract_filter": abstract_filter
195 """Return the Formatter class that should be used to read a particular
200 dataId : `DataCoordinate`
201 Dimension-based ID for the raw file or files being ingested.
205 formatter : `Formatter` class
206 Class to be used that reads the file into an
207 `lsst.afw.image.Exposure` instance.
209 raise NotImplementedError()
212 """Write human-curated calibration Datasets to the given Butler with
213 the appropriate validity ranges.
217 butler : `lsst.daf.butler.Butler`
218 Butler to use to store these calibrations.
222 Expected to be called from subclasses. The base method calls
223 ``writeCameraGeom`` and ``writeStandardTextCuratedCalibrations``.
229 """Apply instrument-specific overrides for a task config.
234 Name of the object being configured; typically the _DefaultName
236 config : `lsst.pex.config.Config`
237 Config instance to which overrides should be applied.
240 path = os.path.join(root, f
"{name}.py")
241 if os.path.exists(path):
245 """Write the default camera geometry to the butler repository
246 with an infinite validity range.
250 butler : `lsst.daf.butler.Butler`
251 Butler to receive these calibration datasets.
254 datasetType = DatasetType(
"camera", (
"instrument",
"calibration_label"),
"Camera",
255 universe=butler.registry.dimensions)
256 butler.registry.registerDatasetType(datasetType)
259 butler.put(camera, datasetType, unboundedDataId)
262 """Write the set of standardized curated text calibrations to
267 butler : `lsst.daf.butler.Butler`
268 Butler to receive these calibration datasets.
273 if datasetTypeName
not in StandardCuratedCalibrationDatasetTypes:
274 raise ValueError(f
"DatasetType {datasetTypeName} not in understood list"
275 f
" [{'.'.join(StandardCuratedCalibrationDatasetTypes)}]")
276 definition = StandardCuratedCalibrationDatasetTypes[datasetTypeName]
277 datasetType = DatasetType(datasetTypeName,
278 universe=butler.registry.dimensions,
282 def _writeSpecificCuratedCalibrationDatasets(self, butler, datasetType):
283 """Write standardized curated calibration datasets for this specific
284 dataset type from an obs data package.
288 butler : `lsst.daf.butler.Butler`
289 Gen3 butler in which to put the calibrations.
290 datasetType : `lsst.daf.butler.DatasetType`
291 Dataset type to be put.
295 This method scans the location defined in the ``obsDataPackageDir``
296 class attribute for curated calibrations corresponding to the
297 supplied dataset type. The directory name in the data package must
298 match the name of the dataset type. They are assumed to use the
299 standard layout and can be read by
300 `~lsst.pipe.tasks.read_curated_calibs.read_all` and provide standard
310 if not os.path.exists(calibPath):
314 butler.registry.registerDatasetType(datasetType)
321 calibsDict =
read_all(calibPath, camera)[0]
322 endOfTime = TIMESPAN_MAX
323 dimensionRecords = []
325 for det
in calibsDict:
326 times = sorted([k
for k
in calibsDict[det]])
327 calibs = [calibsDict[det][time]
for time
in times]
328 times = [astropy.time.Time(t, format=
"datetime", scale=
"utc")
for t
in times]
330 for calib, beginTime, endTime
in zip(calibs, times[:-1], times[1:]):
331 md = calib.getMetadata()
332 calibrationLabel = f
"{datasetType.name}/{md['CALIBDATE']}/{md['DETECTOR']}"
333 dataId = DataCoordinate.standardize(
334 universe=butler.registry.dimensions,
336 calibration_label=calibrationLabel,
337 detector=md[
"DETECTOR"],
339 datasetRecords.append((calib, dataId))
340 dimensionRecords.append({
342 "name": calibrationLabel,
343 "datetime_begin": beginTime,
344 "datetime_end": endTime,
348 with butler.transaction():
349 butler.registry.insertDimensionData(
"calibration_label", *dimensionRecords)
352 for calib, dataId
in datasetRecords:
353 butler.put(calib, datasetType, dataId)
357 """Return a factory for creating Gen2->Gen3 data ID translators,
358 specialized for this instrument.
360 Derived class implementations should generally call
361 `TranslatorFactory.addGenericInstrumentRules` with appropriate
362 arguments, but are not required to (and may not be able to if their
363 Gen2 raw data IDs are sufficiently different from the HSC/DECam/CFHT
368 factory : `TranslatorFactory`.
369 Factory for `Translator` objects.
371 raise NotImplementedError(
"Must be implemented by derived classes.")
375 """Construct an exposure DimensionRecord from
376 `astro_metadata_translator.ObservationInfo`.
380 obsInfo : `astro_metadata_translator.ObservationInfo`
381 A `~astro_metadata_translator.ObservationInfo` object corresponding to
383 universe : `DimensionUniverse`
384 Set of all known dimensions.
388 record : `DimensionRecord`
389 A record containing exposure metadata, suitable for insertion into
392 dimension = universe[
"exposure"]
393 return dimension.RecordClass.fromDict({
394 "instrument": obsInfo.instrument,
395 "id": obsInfo.exposure_id,
396 "name": obsInfo.observation_id,
397 "group_name": obsInfo.exposure_group,
398 "group_id": obsInfo.visit_id,
399 "datetime_begin": obsInfo.datetime_begin,
400 "datetime_end": obsInfo.datetime_end,
401 "exposure_time": obsInfo.exposure_time.to_value(
"s"),
402 "dark_time": obsInfo.dark_time.to_value(
"s"),
403 "observation_type": obsInfo.observation_type,
404 "physical_filter": obsInfo.physical_filter,
409 """Add a special 'unbounded' calibration_label dimension entry for the
410 given camera that is valid for any exposure.
412 If such an entry already exists, this function just returns a `DataId`
413 for the existing entry.
417 registry : `Registry`
418 Registry object in which to insert the dimension entry.
419 instrumentName : `str`
420 Name of the instrument this calibration label is associated with.
425 New or existing data ID for the unbounded calibration.
427 d = dict(instrument=instrumentName, calibration_label=
"unbounded")
429 return registry.expandDataId(d)
433 entry[
"datetime_begin"] = TIMESPAN_MIN
434 entry[
"datetime_end"] = TIMESPAN_MAX
435 registry.insertDimensionData(
"calibration_label", entry)
436 return registry.expandDataId(d)
439 def loadCamera(butler: Butler, dataId: DataId, *, collections: Any =
None) -> Tuple[Camera, bool]:
440 """Attempt to load versioned camera geometry from a butler, but fall back
441 to obtaining a nominal camera from the `Instrument` class if that fails.
445 butler : `lsst.daf.butler.Butler`
446 Butler instance to attempt to query for and load a ``camera`` dataset
448 dataId : `dict` or `DataCoordinate`
449 Data ID that identifies at least the ``instrument`` and ``exposure``
451 collections : Any, optional
452 Collections to be searched, overriding ``self.butler.collections``.
453 Can be any of the types supported by the ``collections`` argument
454 to butler construction.
458 camera : `lsst.afw.cameraGeom.Camera`
461 If `True`, the camera was obtained from the butler and should represent
462 a versioned camera from a calibration repository. If `False`, no
463 camera datasets were found, and the returned camera was produced by
464 instantiating the appropriate `Instrument` class and calling
465 `Instrument.getCamera`.
467 if collections
is None:
468 collections = butler.collections
473 dataId = butler.registry.expandDataId(dataId, graph=butler.registry.dimensions[
"exposure"].graph)
474 cameraRefs =
list(butler.registry.queryDatasets(
"camera", dataId=dataId, collections=collections,
477 assert len(cameraRefs) == 1,
"Should be guaranteed by deduplicate=True above."
478 return butler.getDirect(cameraRefs[0]),
True
479 instrument = Instrument.fromName(dataId[
"instrument"], butler.registry)
480 return instrument.getCamera(),
False