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
31 from lsst.afw.cameraGeom
import Camera
32 from lsst.daf.butler
import (
43 from .gen2to3
import TranslatorFactory
44 from lsst.daf.butler
import Registry
48 StandardCuratedCalibrationDatasetTypes = {
49 "defects": {
"dimensions": (
"instrument",
"detector",
"calibration_label"),
50 "storageClass":
"Defects"},
51 "qe_curve": {
"dimensions": (
"instrument",
"detector",
"calibration_label"),
52 "storageClass":
"QECurve"},
53 "crosstalk": {
"dimensions": (
"instrument",
"detector",
"calibration_label"),
54 "storageClass":
"CrosstalkCalib"},
59 """Base class for instrument-specific logic for the Gen3 Butler.
61 Concrete instrument subclasses should be directly constructable with no
66 """Paths to config files to read for specific Tasks.
68 The paths in this list should contain files of the form `task.py`, for
69 each of the Tasks that requires special configuration.
73 """Instrument specific name to use when locating a policy or configuration
74 file in the file system."""
77 """Name of the package containing the text curated calibration files.
78 Usually a obs _data package. If `None` no curated calibration files
79 will be read. (`str`)"""
81 standardCuratedDatasetTypes = tuple(StandardCuratedCalibrationDatasetTypes)
82 """The dataset types expected to be obtained from the obsDataPackage.
83 These dataset types are all required to have standard definitions and
84 must be known to the base class. Clearing this list will prevent
85 any of these calibrations from being stored. If a dataset type is not
86 known to a specific instrument it can still be included in this list
87 since the data package is the source of truth.
93 """`~lsst.obs.base.FilterDefinitionCollection`, defining the filters
106 """Return the short (dimension) name for this instrument.
108 This is not (in general) the same as the class name - it's what is used
109 as the value of the "instrument" field in data IDs, and is usually an
110 abbreviation of the full name.
112 raise NotImplementedError()
116 """Retrieve the cameraGeom representation of this instrument.
118 This is a temporary API that should go away once ``obs_`` packages have
119 a standardized approach to writing versioned cameras to a Gen3 repo.
121 raise NotImplementedError()
125 """Insert instrument, physical_filter, and detector entries into a
128 raise NotImplementedError()
132 """The root of the obs package that provides specializations for
133 this instrument (`str`).
144 def fromName(name: str, registry: Registry) -> Instrument:
145 """Given an instrument name and a butler, retrieve a corresponding
146 instantiated instrument object.
151 Name of the instrument (must match the return value of `getName`).
152 registry : `lsst.daf.butler.Registry`
153 Butler registry to query to find the information.
157 instrument : `Instrument`
158 An instance of the relevant `Instrument`.
162 The instrument must be registered in the corresponding butler.
167 Raised if the instrument is not known to the supplied registry.
169 Raised if the class could not be imported. This could mean
170 that the relevant obs package has not been setup.
172 Raised if the class name retrieved is not a string.
174 records = list(registry.queryDimensionRecords(
"instrument", instrument=name))
176 raise LookupError(f
"No registered instrument with name '{name}'.")
177 cls = records[0].class_name
178 if not isinstance(cls, str):
179 raise TypeError(f
"Unexpected class name retrieved from {name} instrument dimension (got {cls})")
180 instrument = doImport(cls)
185 """Import all the instruments known to this registry.
187 This will ensure that all metadata translators have been registered.
191 registry : `lsst.daf.butler.Registry`
192 Butler registry to query to find the information.
196 It is allowed for a particular instrument class to fail on import.
197 This might simply indicate that a particular obs package has
200 records = list(registry.queryDimensionRecords(
"instrument"))
201 for record
in records:
202 cls = record.class_name
208 def _registerFilters(self, registry):
209 """Register the physical and abstract filter Dimension relationships.
210 This should be called in the ``register`` implementation.
214 registry : `lsst.daf.butler.core.Registry`
215 The registry to add dimensions to.
219 if filter.abstract_filter
is None:
220 abstract_filter = filter.physical_filter
222 abstract_filter = filter.abstract_filter
224 registry.insertDimensionData(
"physical_filter",
226 "name": filter.physical_filter,
227 "abstract_filter": abstract_filter
232 """Return the Formatter class that should be used to read a particular
237 dataId : `DataCoordinate`
238 Dimension-based ID for the raw file or files being ingested.
242 formatter : `Formatter` class
243 Class to be used that reads the file into an
244 `lsst.afw.image.Exposure` instance.
246 raise NotImplementedError()
249 """Write human-curated calibration Datasets to the given Butler with
250 the appropriate validity ranges.
254 butler : `lsst.daf.butler.Butler`
255 Butler to use to store these calibrations.
257 Run to use for this collection of calibrations. If `None` the
258 collection name is worked out automatically from the instrument
259 name and other metadata.
263 Expected to be called from subclasses. The base method calls
264 ``writeCameraGeom`` and ``writeStandardTextCuratedCalibrations``.
271 butler.registry.registerCollection(run, type=CollectionType.RUN)
277 """Write additional curated calibrations that might be instrument
278 specific and are not part of the standard set.
280 Default implementation does nothing.
284 butler : `lsst.daf.butler.Butler`
285 Butler to use to store these calibrations.
286 run : `str`, optional
287 Name of the run to use to override the default run associated
293 """Apply instrument-specific overrides for a task config.
298 Name of the object being configured; typically the _DefaultName
300 config : `lsst.pex.config.Config`
301 Config instance to which overrides should be applied.
303 for root
in self.configPaths:
304 path = os.path.join(root, f
"{name}.py")
305 if os.path.exists(path):
309 """Write the default camera geometry to the butler repository
310 with an infinite validity range.
314 butler : `lsst.daf.butler.Butler`
315 Butler to receive these calibration datasets.
316 run : `str`, optional
317 Name of the run to use to override the default run associated
321 datasetType = DatasetType(
"camera", (
"instrument",
"calibration_label"),
"Camera",
322 universe=butler.registry.dimensions)
323 butler.registry.registerDatasetType(datasetType)
326 butler.put(camera, datasetType, unboundedDataId, run=run)
329 """Write the set of standardized curated text calibrations to
334 butler : `lsst.daf.butler.Butler`
335 Butler to receive these calibration datasets.
336 run : `str`, optional
337 Name of the run to use to override the default run associated
343 if datasetTypeName
not in StandardCuratedCalibrationDatasetTypes:
344 raise ValueError(f
"DatasetType {datasetTypeName} not in understood list"
345 f
" [{'.'.join(StandardCuratedCalibrationDatasetTypes)}]")
346 definition = StandardCuratedCalibrationDatasetTypes[datasetTypeName]
347 datasetType = DatasetType(datasetTypeName,
348 universe=butler.registry.dimensions,
352 def _writeSpecificCuratedCalibrationDatasets(self, butler, datasetType, run=None):
353 """Write standardized curated calibration datasets for this specific
354 dataset type from an obs data package.
358 butler : `lsst.daf.butler.Butler`
359 Gen3 butler in which to put the calibrations.
360 datasetType : `lsst.daf.butler.DatasetType`
361 Dataset type to be put.
362 run : `str`, optional
363 Name of the run to use to override the default run associated
368 This method scans the location defined in the ``obsDataPackageDir``
369 class attribute for curated calibrations corresponding to the
370 supplied dataset type. The directory name in the data package must
371 match the name of the dataset type. They are assumed to use the
372 standard layout and can be read by
373 `~lsst.pipe.tasks.read_curated_calibs.read_all` and provide standard
383 if not os.path.exists(calibPath):
387 butler.registry.registerDatasetType(datasetType)
391 from lsst.pipe.tasks.read_curated_calibs
import read_all
394 calibsDict = read_all(calibPath, camera)[0]
395 dimensionRecords = []
397 for det
in calibsDict:
398 times = sorted([k
for k
in calibsDict[det]])
399 calibs = [calibsDict[det][time]
for time
in times]
400 times = [astropy.time.Time(t, format=
"datetime", scale=
"utc")
for t
in times]
402 for calib, beginTime, endTime
in zip(calibs, times[:-1], times[1:]):
403 md = calib.getMetadata()
404 calibrationLabel = f
"{datasetType.name}/{md['CALIBDATE']}/{md['DETECTOR']}"
405 dataId = DataCoordinate.standardize(
406 universe=butler.registry.dimensions,
408 calibration_label=calibrationLabel,
409 detector=md[
"DETECTOR"],
411 datasetRecords.append((calib, dataId))
412 dimensionRecords.append({
414 "name": calibrationLabel,
415 "timespan": Timespan(beginTime, endTime),
419 with butler.transaction():
420 butler.registry.insertDimensionData(
"calibration_label", *dimensionRecords)
423 for calib, dataId
in datasetRecords:
424 butler.put(calib, datasetType, dataId, run=run)
428 """Return a factory for creating Gen2->Gen3 data ID translators,
429 specialized for this instrument.
431 Derived class implementations should generally call
432 `TranslatorFactory.addGenericInstrumentRules` with appropriate
433 arguments, but are not required to (and may not be able to if their
434 Gen2 raw data IDs are sufficiently different from the HSC/DECam/CFHT
439 factory : `TranslatorFactory`.
440 Factory for `Translator` objects.
442 raise NotImplementedError(
"Must be implemented by derived classes.")
446 """Make the default instrument-specific run collection string for raw
452 Run collection name to be used as the default for ingestion of
459 """Get the instrument-specific collection string to use as derived
460 from the supplied label.
465 String to be combined with the instrument name to form a
471 Collection name to use that includes the instrument name.
473 return f
"{cls.getName()}/{label}"
477 """Construct an exposure DimensionRecord from
478 `astro_metadata_translator.ObservationInfo`.
482 obsInfo : `astro_metadata_translator.ObservationInfo`
483 A `~astro_metadata_translator.ObservationInfo` object corresponding to
485 universe : `DimensionUniverse`
486 Set of all known dimensions.
490 record : `DimensionRecord`
491 A record containing exposure metadata, suitable for insertion into
494 dimension = universe[
"exposure"]
496 ra, dec, sky_angle, zenith_angle = (
None,
None,
None,
None)
497 if obsInfo.tracking_radec
is not None:
498 icrs = obsInfo.tracking_radec.icrs
500 dec = icrs.dec.degree
501 if obsInfo.boresight_rotation_coord ==
"sky":
502 sky_angle = obsInfo.boresight_rotation_angle.degree
503 if obsInfo.altaz_begin
is not None:
504 zenith_angle = obsInfo.altaz_begin.zen.degree
506 return dimension.RecordClass(
507 instrument=obsInfo.instrument,
508 id=obsInfo.exposure_id,
509 name=obsInfo.observation_id,
510 group_name=obsInfo.exposure_group,
511 group_id=obsInfo.visit_id,
512 datetime_begin=obsInfo.datetime_begin,
513 datetime_end=obsInfo.datetime_end,
514 exposure_time=obsInfo.exposure_time.to_value(
"s"),
515 dark_time=obsInfo.dark_time.to_value(
"s"),
516 observation_type=obsInfo.observation_type,
517 physical_filter=obsInfo.physical_filter,
518 science_program=obsInfo.science_program,
519 target_name=obsInfo.object,
523 zenith_angle=zenith_angle,
528 """Add a special 'unbounded' calibration_label dimension entry for the
529 given camera that is valid for any exposure.
531 If such an entry already exists, this function just returns a `DataId`
532 for the existing entry.
536 registry : `Registry`
537 Registry object in which to insert the dimension entry.
538 instrumentName : `str`
539 Name of the instrument this calibration label is associated with.
544 New or existing data ID for the unbounded calibration.
546 d = dict(instrument=instrumentName, calibration_label=
"unbounded")
548 return registry.expandDataId(d)
552 entry[
"timespan"] = Timespan(
None,
None)
553 registry.insertDimensionData(
"calibration_label", entry)
554 return registry.expandDataId(d)
557 def loadCamera(butler: Butler, dataId: DataId, *, collections: Any =
None) -> Tuple[Camera, bool]:
558 """Attempt to load versioned camera geometry from a butler, but fall back
559 to obtaining a nominal camera from the `Instrument` class if that fails.
563 butler : `lsst.daf.butler.Butler`
564 Butler instance to attempt to query for and load a ``camera`` dataset
566 dataId : `dict` or `DataCoordinate`
567 Data ID that identifies at least the ``instrument`` and ``exposure``
569 collections : Any, optional
570 Collections to be searched, overriding ``self.butler.collections``.
571 Can be any of the types supported by the ``collections`` argument
572 to butler construction.
576 camera : `lsst.afw.cameraGeom.Camera`
579 If `True`, the camera was obtained from the butler and should represent
580 a versioned camera from a calibration repository. If `False`, no
581 camera datasets were found, and the returned camera was produced by
582 instantiating the appropriate `Instrument` class and calling
583 `Instrument.getCamera`.
585 if collections
is None:
586 collections = butler.collections
591 dataId = butler.registry.expandDataId(dataId, graph=butler.registry.dimensions[
"exposure"].graph)
592 cameraRefs = list(butler.registry.queryDatasets(
"camera", dataId=dataId, collections=collections,
595 assert len(cameraRefs) == 1,
"Should be guaranteed by deduplicate=True above."
596 return butler.getDirect(cameraRefs[0]),
True
597 instrument = Instrument.fromName(dataId[
"instrument"], butler.registry)
598 return instrument.getCamera(),
False