22 from __future__
import annotations
24 __all__ = (
"Instrument",
"makeExposureRecordFromObsInfo",
"loadCamera")
27 from abc
import ABCMeta, abstractmethod
28 from collections
import defaultdict
30 from typing
import Any, Optional, Set, Sequence, Tuple, TYPE_CHECKING, Union
31 from functools
import lru_cache
36 from lsst.daf.butler
import (
47 from .gen2to3
import TranslatorFactory
48 from lsst.daf.butler
import Registry
52 StandardCuratedCalibrationDatasetTypes = {
53 "defects": {
"dimensions": (
"instrument",
"detector"),
"storageClass":
"Defects"},
54 "qe_curve": {
"dimensions": (
"instrument",
"detector"),
"storageClass":
"QECurve"},
55 "crosstalk": {
"dimensions": (
"instrument",
"detector"),
"storageClass":
"CrosstalkCalib"},
60 """Base class for instrument-specific logic for the Gen3 Butler.
62 Concrete instrument subclasses should be directly constructable with no
66 configPaths: Sequence[str] = ()
67 """Paths to config files to read for specific Tasks.
69 The paths in this list should contain files of the form `task.py`, for
70 each of the Tasks that requires special configuration.
73 policyName: Optional[str] =
None
74 """Instrument specific name to use when locating a policy or configuration
75 file in the file system."""
77 obsDataPackage: Optional[str] =
None
78 """Name of the package containing the text curated calibration files.
79 Usually a obs _data package. If `None` no curated calibration files
80 will be read. (`str`)"""
82 standardCuratedDatasetTypes: Set[str] = frozenset(StandardCuratedCalibrationDatasetTypes)
83 """The dataset types expected to be obtained from the obsDataPackage.
85 These dataset types are all required to have standard definitions and
86 must be known to the base class. Clearing this list will prevent
87 any of these calibrations from being stored. If a dataset type is not
88 known to a specific instrument it can still be included in this list
89 since the data package is the source of truth. (`set` of `str`)
92 additionalCuratedDatasetTypes: Set[str] = frozenset()
93 """Curated dataset types specific to this particular instrument that do
94 not follow the standard organization found in obs data packages.
96 These are the instrument-specific dataset types written by
97 `writeAdditionalCuratedCalibrations` in addition to the calibrations
98 found in obs data packages that follow the standard scheme.
104 """`~lsst.obs.base.FilterDefinitionCollection`, defining the filters
116 """Return the short (dimension) name for this instrument.
118 This is not (in general) the same as the class name - it's what is used
119 as the value of the "instrument" field in data IDs, and is usually an
120 abbreviation of the full name.
122 raise NotImplementedError()
127 """Return the names of all the curated calibration dataset types.
131 names : `set` of `str`
132 The dataset type names of all curated calibrations. This will
133 include the standard curated calibrations even if the particular
134 instrument does not support them.
138 The returned list does not indicate whether a particular dataset
139 is present in the Butler repository, simply that these are the
140 dataset types that are handled by ``writeCuratedCalibrations``.
149 for datasetTypeName
in cls.standardCuratedDatasetTypes:
151 if calibPath
is not None:
152 curated.add(datasetTypeName)
154 curated.update(cls.additionalCuratedDatasetTypes)
155 return frozenset(curated)
159 """Retrieve the cameraGeom representation of this instrument.
161 This is a temporary API that should go away once ``obs`` packages have
162 a standardized approach to writing versioned cameras to a Gen3 repo.
164 raise NotImplementedError()
168 """Insert instrument, physical_filter, and detector entries into a
171 Implementations should guarantee that registration is atomic (the
172 registry should not be modified if any error occurs) and idempotent at
173 the level of individual dimension entries; new detectors and filters
174 should be added, but changes to any existing record should not be.
175 This can generally be achieved via a block like::
177 with registry.transaction():
178 registry.syncDimensionData("instrument", ...)
179 registry.syncDimensionData("detector", ...)
180 self.registerFilters(registry)
184 lsst.daf.butler.registry.ConflictingDefinitionError
185 Raised if any existing record has the same key but a different
186 definition as one being registered.
188 raise NotImplementedError()
193 """The root of the obs data package that provides specializations for
199 The root of the relevat obs data package.
201 if cls.obsDataPackage
is None:
206 def fromName(name: str, registry: Registry) -> Instrument:
207 """Given an instrument name and a butler, retrieve a corresponding
208 instantiated instrument object.
213 Name of the instrument (must match the return value of `getName`).
214 registry : `lsst.daf.butler.Registry`
215 Butler registry to query to find the information.
219 instrument : `Instrument`
220 An instance of the relevant `Instrument`.
224 The instrument must be registered in the corresponding butler.
229 Raised if the instrument is not known to the supplied registry.
231 Raised if the class could not be imported. This could mean
232 that the relevant obs package has not been setup.
234 Raised if the class name retrieved is not a string.
236 records =
list(registry.queryDimensionRecords(
"instrument", instrument=name))
238 raise LookupError(f
"No registered instrument with name '{name}'.")
239 cls = records[0].class_name
240 if not isinstance(cls, str):
241 raise TypeError(f
"Unexpected class name retrieved from {name} instrument dimension (got {cls})")
247 """Import all the instruments known to this registry.
249 This will ensure that all metadata translators have been registered.
253 registry : `lsst.daf.butler.Registry`
254 Butler registry to query to find the information.
258 It is allowed for a particular instrument class to fail on import.
259 This might simply indicate that a particular obs package has
262 records =
list(registry.queryDimensionRecords(
"instrument"))
263 for record
in records:
264 cls = record.class_name
270 def _registerFilters(self, registry):
271 """Register the physical and abstract filter Dimension relationships.
272 This should be called in the `register` implementation, within
273 a transaction context manager block.
277 registry : `lsst.daf.butler.core.Registry`
278 The registry to add dimensions to.
283 if filter.band
is None:
284 band = filter.physical_filter
288 registry.syncDimensionData(
"physical_filter",
289 {
"instrument": self.
getNamegetName(),
290 "name": filter.physical_filter,
296 """Return the Formatter class that should be used to read a particular
301 dataId : `DataCoordinate`
302 Dimension-based ID for the raw file or files being ingested.
306 formatter : `Formatter` class
307 Class to be used that reads the file into an
308 `lsst.afw.image.Exposure` instance.
310 raise NotImplementedError()
313 """Apply instrument-specific overrides for a task config.
318 Name of the object being configured; typically the _DefaultName
320 config : `lsst.pex.config.Config`
321 Config instance to which overrides should be applied.
323 for root
in self.configPaths:
324 path = os.path.join(root, f
"{name}.py")
325 if os.path.exists(path):
329 labels: Sequence[str] = ()) ->
None:
330 """Write human-curated calibration Datasets to the given Butler with
331 the appropriate validity ranges.
335 butler : `lsst.daf.butler.Butler`
336 Butler to use to store these calibrations.
337 collection : `str`, optional
338 Name to use for the calibration collection that associates all
339 datasets with a validity range. If this collection already exists,
340 it must be a `~CollectionType.CALIBRATION` collection, and it must
341 not have any datasets that would conflict with those inserted by
342 this method. If `None`, a collection name is worked out
343 automatically from the instrument name and other metadata by
344 calling ``makeCalibrationCollectionName``, but this
345 default name may not work well for long-lived repositories unless
346 ``labels`` is also provided (and changed every time curated
347 calibrations are ingested).
348 labels : `Sequence` [ `str` ], optional
349 Extra strings to include in collection names, after concatenating
350 them with the standard collection name delimeter. If provided,
351 these are inserted into the names of the `~CollectionType.RUN`
352 collections that datasets are inserted directly into, as well the
353 `~CollectionType.CALIBRATION` collection if it is generated
354 automatically (i.e. if ``collection is None``). Usually this is
355 just the name of the ticket on which the calibration collection is
360 Expected to be called from subclasses. The base method calls
361 ``writeCameraGeom``, ``writeStandardTextCuratedCalibrations``,
362 and ``writeAdditionalCuratdCalibrations``.
375 labels: Sequence[str] = ()) ->
None:
376 """Write additional curated calibrations that might be instrument
377 specific and are not part of the standard set.
379 Default implementation does nothing.
383 butler : `lsst.daf.butler.Butler`
384 Butler to use to store these calibrations.
385 collection : `str`, optional
386 Name to use for the calibration collection that associates all
387 datasets with a validity range. If this collection already exists,
388 it must be a `~CollectionType.CALIBRATION` collection, and it must
389 not have any datasets that would conflict with those inserted by
390 this method. If `None`, a collection name is worked out
391 automatically from the instrument name and other metadata by
392 calling ``makeCalibrationCollectionName``, but this
393 default name may not work well for long-lived repositories unless
394 ``labels`` is also provided (and changed every time curated
395 calibrations are ingested).
396 labels : `Sequence` [ `str` ], optional
397 Extra strings to include in collection names, after concatenating
398 them with the standard collection name delimeter. If provided,
399 these are inserted into the names of the `~CollectionType.RUN`
400 collections that datasets are inserted directly into, as well the
401 `~CollectionType.CALIBRATION` collection if it is generated
402 automatically (i.e. if ``collection is None``). Usually this is
403 just the name of the ticket on which the calibration collection is
409 labels: Sequence[str] = ()) ->
None:
410 """Write the default camera geometry to the butler repository and
411 associate it with the appropriate validity range in a calibration
416 butler : `lsst.daf.butler.Butler`
417 Butler to use to store these calibrations.
418 collection : `str`, optional
419 Name to use for the calibration collection that associates all
420 datasets with a validity range. If this collection already exists,
421 it must be a `~CollectionType.CALIBRATION` collection, and it must
422 not have any datasets that would conflict with those inserted by
423 this method. If `None`, a collection name is worked out
424 automatically from the instrument name and other metadata by
425 calling ``makeCalibrationCollectionName``, but this
426 default name may not work well for long-lived repositories unless
427 ``labels`` is also provided (and changed every time curated
428 calibrations are ingested).
429 labels : `Sequence` [ `str` ], optional
430 Extra strings to include in collection names, after concatenating
431 them with the standard collection name delimeter. If provided,
432 these are inserted into the names of the `~CollectionType.RUN`
433 collections that datasets are inserted directly into, as well the
434 `~CollectionType.CALIBRATION` collection if it is generated
435 automatically (i.e. if ``collection is None``). Usually this is
436 just the name of the ticket on which the calibration collection is
439 if collection
is None:
441 butler.registry.registerCollection(collection, type=CollectionType.CALIBRATION)
443 butler.registry.registerRun(run)
444 datasetType = DatasetType(
"camera", (
"instrument",),
"Camera", isCalibration=
True,
445 universe=butler.registry.dimensions)
446 butler.registry.registerDatasetType(datasetType)
448 ref = butler.put(camera, datasetType, {
"instrument": self.
getNamegetName()}, run=run)
449 butler.registry.certify(collection, [ref], Timespan(begin=
None, end=
None))
452 labels: Sequence[str] = ()) ->
None:
453 """Write the set of standardized curated text calibrations to
458 butler : `lsst.daf.butler.Butler`
459 Butler to receive these calibration datasets.
460 collection : `str`, optional
461 Name to use for the calibration collection that associates all
462 datasets with a validity range. If this collection already exists,
463 it must be a `~CollectionType.CALIBRATION` collection, and it must
464 not have any datasets that would conflict with those inserted by
465 this method. If `None`, a collection name is worked out
466 automatically from the instrument name and other metadata by
467 calling ``makeCalibrationCollectionName``, but this
468 default name may not work well for long-lived repositories unless
469 ``labels`` is also provided (and changed every time curated
470 calibrations are ingested).
471 labels : `Sequence` [ `str` ], optional
472 Extra strings to include in collection names, after concatenating
473 them with the standard collection name delimeter. If provided,
474 these are inserted into the names of the `~CollectionType.RUN`
475 collections that datasets are inserted directly into, as well the
476 `~CollectionType.CALIBRATION` collection if it is generated
477 automatically (i.e. if ``collection is None``). Usually this is
478 just the name of the ticket on which the calibration collection is
481 if collection
is None:
483 butler.registry.registerCollection(collection, type=CollectionType.CALIBRATION)
485 for datasetTypeName
in self.standardCuratedDatasetTypes:
487 if datasetTypeName
not in StandardCuratedCalibrationDatasetTypes:
488 raise ValueError(f
"DatasetType {datasetTypeName} not in understood list"
489 f
" [{'.'.join(StandardCuratedCalibrationDatasetTypes)}]")
490 definition = StandardCuratedCalibrationDatasetTypes[datasetTypeName]
491 datasetType = DatasetType(datasetTypeName,
492 universe=butler.registry.dimensions,
499 def _getSpecificCuratedCalibrationPath(cls, datasetTypeName):
500 """Return the path of the curated calibration directory.
504 datasetTypeName : `str`
505 The name of the standard dataset type to find.
510 The path to the standard curated data directory. `None` if the
511 dataset type is not found or the obs data package is not
521 if os.path.exists(calibPath):
526 def _writeSpecificCuratedCalibrationDatasets(self, butler: Butler, datasetType: DatasetType,
527 collection: str, runs: Set[str], labels: Sequence[str]):
528 """Write standardized curated calibration datasets for this specific
529 dataset type from an obs data package.
533 butler : `lsst.daf.butler.Butler`
534 Gen3 butler in which to put the calibrations.
535 datasetType : `lsst.daf.butler.DatasetType`
536 Dataset type to be put.
538 Name of the `~CollectionType.CALIBRATION` collection that
539 associates all datasets with validity ranges. Must have been
540 registered prior to this call.
541 runs : `set` [ `str` ]
542 Names of runs that have already been registered by previous calls
543 and need not be registered again. Should be updated by this
544 method as new runs are registered.
545 labels : `Sequence` [ `str` ]
546 Extra strings to include in run names when creating them from
547 ``CALIBDATE`` metadata, via calls to `makeCuratedCalibrationName`.
548 Usually this is the name of the ticket on which the calibration
549 collection is being created.
553 This method scans the location defined in the ``obsDataPackageDir``
554 class attribute for curated calibrations corresponding to the
555 supplied dataset type. The directory name in the data package must
556 match the name of the dataset type. They are assumed to use the
557 standard layout and can be read by
558 `~lsst.pipe.tasks.read_curated_calibs.read_all` and provide standard
562 if calibPath
is None:
566 butler.registry.registerDatasetType(datasetType)
576 calibsDict =
read_all(calibPath, camera)[0]
578 for det
in calibsDict:
579 times = sorted([k
for k
in calibsDict[det]])
580 calibs = [calibsDict[det][time]
for time
in times]
581 times = [astropy.time.Time(t, format=
"datetime", scale=
"utc")
for t
in times]
583 for calib, beginTime, endTime
in zip(calibs, times[:-1], times[1:]):
584 md = calib.getMetadata()
587 butler.registry.registerRun(run)
589 dataId = DataCoordinate.standardize(
590 universe=butler.registry.dimensions,
591 instrument=self.
getNamegetName(),
592 detector=md[
"DETECTOR"],
594 datasetRecords.append((calib, dataId, run, Timespan(beginTime, endTime)))
600 refsByTimespan = defaultdict(list)
601 with butler.transaction():
602 for calib, dataId, run, timespan
in datasetRecords:
603 refsByTimespan[timespan].
append(butler.put(calib, datasetType, dataId, run=run))
604 for timespan, refs
in refsByTimespan.items():
605 butler.registry.certify(collection, refs, timespan)
609 """Return a factory for creating Gen2->Gen3 data ID translators,
610 specialized for this instrument.
612 Derived class implementations should generally call
613 `TranslatorFactory.addGenericInstrumentRules` with appropriate
614 arguments, but are not required to (and may not be able to if their
615 Gen2 raw data IDs are sufficiently different from the HSC/DECam/CFHT
620 factory : `TranslatorFactory`.
621 Factory for `Translator` objects.
623 raise NotImplementedError(
"Must be implemented by derived classes.")
627 """Format a timestamp for use in a collection name.
631 timestamp : `str` or `datetime.datetime`
632 Timestamp to format. May be a date or datetime string in extended
633 ISO format (assumed UTC), with or without a timezone specifier, a
634 datetime string in basic ISO format with a timezone specifier, a
635 naive `datetime.datetime` instance (assumed UTC) or a
636 timezone-aware `datetime.datetime` instance (converted to UTC).
637 This is intended to cover all forms that string ``CALIBDATE``
638 metadata values have taken in the past, as well as the format this
639 method itself writes out (to enable round-tripping).
644 Standardized string form for the timestamp.
646 if isinstance(timestamp, str):
649 timestamp = datetime.datetime.fromisoformat(timestamp)
653 timestamp = datetime.datetime.strptime(timestamp,
"%Y%m%dT%H%M%S%z")
654 if not isinstance(timestamp, datetime.datetime):
655 raise TypeError(f
"Unexpected date/time object: {timestamp!r}.")
656 if timestamp.tzinfo
is not None:
657 timestamp = timestamp.astimezone(datetime.timezone.utc)
658 return f
"{timestamp:%Y%m%dT%H%M%S}Z"
662 """Create a timestamp string for use in a collection name from the
668 Standardized string form of the current time.
670 return Instrument.formatCollectionTimestamp(datetime.datetime.now(tz=datetime.timezone.utc))
674 """Make the default instrument-specific run collection string for raw
680 Run collection name to be used as the default for ingestion of
687 """Make a RUN collection name appropriate for inserting calibration
688 datasets whose validity ranges are unbounded.
693 Extra strings to be included in the base name, using the default
694 delimiter for collection names. Usually this is the name of the
695 ticket on which the calibration collection is being created.
706 """Make a RUN collection name appropriate for inserting curated
707 calibration datasets with the given ``CALIBDATE`` metadata value.
712 The ``CALIBDATE`` metadata value.
714 Strings to be included in the collection name (before
715 ``calibDate``, but after all other terms), using the default
716 delimiter for collection names. Usually this is the name of the
717 ticket on which the calibration collection is being created.
728 """Make a CALIBRATION collection name appropriate for associating
729 calibration datasets with validity ranges.
734 Strings to be appended to the base name, using the default
735 delimiter for collection names. Usually this is the name of the
736 ticket on which the calibration collection is being created.
741 Calibration collection name.
747 """Return a global (not instrument-specific) name for a collection that
748 holds reference catalogs.
750 With no arguments, this returns the name of the collection that holds
751 all reference catalogs (usually a ``CHAINED`` collection, at least in
752 long-lived repos that may contain more than one reference catalog).
757 Strings to be added to the global collection name, in order to
758 define a collection name for one or more reference catalogs being
759 ingested at the same time.
768 This is a ``staticmethod``, not a ``classmethod``, because it should
769 be the same for all instruments.
771 return "/".join((
"refcats",) + labels)
775 """Return the name of the umbrella ``CHAINED`` collection for this
776 instrument that combines all standard recommended input collections.
778 This method should almost never be overridden by derived classes.
783 Name for the umbrella collection.
789 """Get the instrument-specific collection string to use as derived
790 from the supplied labels.
795 Strings to be combined with the instrument name to form a
801 Collection name to use that includes the instrument name.
803 return "/".join((cls.
getNamegetName(),) + labels)
807 """Construct an exposure DimensionRecord from
808 `astro_metadata_translator.ObservationInfo`.
812 obsInfo : `astro_metadata_translator.ObservationInfo`
813 A `~astro_metadata_translator.ObservationInfo` object corresponding to
815 universe : `DimensionUniverse`
816 Set of all known dimensions.
820 record : `DimensionRecord`
821 A record containing exposure metadata, suitable for insertion into
824 dimension = universe[
"exposure"]
826 ra, dec, sky_angle, zenith_angle = (
None,
None,
None,
None)
827 if obsInfo.tracking_radec
is not None:
828 icrs = obsInfo.tracking_radec.icrs
830 dec = icrs.dec.degree
831 if obsInfo.boresight_rotation_coord ==
"sky":
832 sky_angle = obsInfo.boresight_rotation_angle.degree
833 if obsInfo.altaz_begin
is not None:
834 zenith_angle = obsInfo.altaz_begin.zen.degree
836 return dimension.RecordClass(
837 instrument=obsInfo.instrument,
838 id=obsInfo.exposure_id,
839 obs_id=obsInfo.observation_id,
840 group_name=obsInfo.exposure_group,
841 group_id=obsInfo.visit_id,
842 datetime_begin=obsInfo.datetime_begin,
843 datetime_end=obsInfo.datetime_end,
844 exposure_time=obsInfo.exposure_time.to_value(
"s"),
846 dark_time=obsInfo.dark_time.to_value(
"s")
if obsInfo.dark_time
is not None else None,
847 observation_type=obsInfo.observation_type,
848 observation_reason=obsInfo.observation_reason,
849 day_obs=obsInfo.observing_day,
850 seq_num=obsInfo.observation_counter,
851 physical_filter=obsInfo.physical_filter,
852 science_program=obsInfo.science_program,
853 target_name=obsInfo.object,
857 zenith_angle=zenith_angle,
861 def loadCamera(butler: Butler, dataId: DataId, *, collections: Any =
None) -> Tuple[Camera, bool]:
862 """Attempt to load versioned camera geometry from a butler, but fall back
863 to obtaining a nominal camera from the `Instrument` class if that fails.
867 butler : `lsst.daf.butler.Butler`
868 Butler instance to attempt to query for and load a ``camera`` dataset
870 dataId : `dict` or `DataCoordinate`
871 Data ID that identifies at least the ``instrument`` and ``exposure``
873 collections : Any, optional
874 Collections to be searched, overriding ``self.butler.collections``.
875 Can be any of the types supported by the ``collections`` argument
876 to butler construction.
880 camera : `lsst.afw.cameraGeom.Camera`
883 If `True`, the camera was obtained from the butler and should represent
884 a versioned camera from a calibration repository. If `False`, no
885 camera datasets were found, and the returned camera was produced by
886 instantiating the appropriate `Instrument` class and calling
887 `Instrument.getCamera`.
889 if collections
is None:
890 collections = butler.collections
895 dataId = butler.registry.expandDataId(dataId, graph=butler.registry.dimensions[
"exposure"].graph)
897 cameraRef = butler.get(
"camera", dataId=dataId, collections=collections)
898 return cameraRef,
True
901 instrument = Instrument.fromName(dataId[
"instrument"], butler.registry)
902 return instrument.getCamera(),
False
def getRawFormatter(self, dataId)
str makeUmbrellaCollectionName(cls)
str makeCuratedCalibrationRunName(cls, str calibDate, *str labels)
str makeCollectionName(cls, *str labels)
Instrument fromName(str name, Registry registry)
None importAll(Registry registry)
def applyConfigOverrides(self, name, config)
None writeCameraGeom(self, Butler butler, Optional[str] collection=None, Sequence[str] labels=())
TranslatorFactory makeDataIdTranslatorFactory(self)
def _getSpecificCuratedCalibrationPath(cls, datasetTypeName)
str makeCalibrationCollectionName(cls, *str labels)
None writeStandardTextCuratedCalibrations(self, Butler butler, Optional[str] collection=None, Sequence[str] labels=())
def _writeSpecificCuratedCalibrationDatasets(self, Butler butler, DatasetType datasetType, str collection, Set[str] runs, Sequence[str] labels)
def filterDefinitions(self)
str formatCollectionTimestamp(Union[str, datetime.datetime] timestamp)
str makeRefCatCollectionName(*str labels)
None writeCuratedCalibrations(self, Butler butler, Optional[str] collection=None, Sequence[str] labels=())
None writeAdditionalCuratedCalibrations(self, Butler butler, Optional[str] collection=None, Sequence[str] labels=())
def register(self, registry)
str makeDefaultRawIngestRunName(cls)
str makeUnboundedCalibrationRunName(cls, *str labels)
def getObsDataPackageDir(cls)
Set[str] getCuratedCalibrationNames(cls)
str makeCollectionTimestamp()
std::shared_ptr< FrameSet > append(FrameSet const &first, FrameSet const &second)
Construct a FrameSet that performs two transformations in series.
def makeExposureRecordFromObsInfo(obsInfo, universe)
Tuple[Camera, bool] loadCamera(Butler butler, DataId dataId, *Any collections=None)
def read_all(root, camera)
std::string getPackageDir(std::string const &packageName)
return the root directory of a setup package
daf::base::PropertyList * list
daf::base::PropertySet * set