Coverage for python/lsst/obs/base/_instrument.py : 22%

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_base.
2#
3# Developed for the LSST Data Management System.
4# This product includes software developed by the LSST Project
5# (https://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/>.
22from __future__ import annotations
24__all__ = ("Instrument", "makeExposureRecordFromObsInfo", "loadCamera")
26import os.path
27from abc import ABCMeta, abstractmethod
28from collections import defaultdict
29from typing import Any, Optional, Set, Sequence, Tuple, TYPE_CHECKING
30from functools import lru_cache
32import astropy.time
34from lsst.afw.cameraGeom import Camera
35from lsst.daf.butler import (
36 Butler,
37 CollectionType,
38 DataCoordinate,
39 DataId,
40 DatasetType,
41 Timespan,
42)
43from lsst.utils import getPackageDir, doImport
45if TYPE_CHECKING: 45 ↛ 46line 45 didn't jump to line 46, because the condition on line 45 was never true
46 from .gen2to3 import TranslatorFactory
47 from lsst.daf.butler import Registry
49# To be a standard text curated calibration means that we use a
50# standard definition for the corresponding DatasetType.
51StandardCuratedCalibrationDatasetTypes = {
52 "defects": {"dimensions": ("instrument", "detector"), "storageClass": "Defects"},
53 "qe_curve": {"dimensions": ("instrument", "detector"), "storageClass": "QECurve"},
54 "crosstalk": {"dimensions": ("instrument", "detector"), "storageClass": "CrosstalkCalib"},
55}
58class Instrument(metaclass=ABCMeta):
59 """Base class for instrument-specific logic for the Gen3 Butler.
61 Concrete instrument subclasses should be directly constructable with no
62 arguments.
63 """
65 configPaths: Sequence[str] = ()
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.
70 """
72 policyName: Optional[str] = None
73 """Instrument specific name to use when locating a policy or configuration
74 file in the file system."""
76 obsDataPackage: Optional[str] = None
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: Set[str] = frozenset(StandardCuratedCalibrationDatasetTypes)
82 """The dataset types expected to be obtained from the obsDataPackage.
84 These dataset types are all required to have standard definitions and
85 must be known to the base class. Clearing this list will prevent
86 any of these calibrations from being stored. If a dataset type is not
87 known to a specific instrument it can still be included in this list
88 since the data package is the source of truth. (`set` of `str`)
89 """
91 additionalCuratedDatasetTypes: Set[str] = frozenset()
92 """Curated dataset types specific to this particular instrument that do
93 not follow the standard organization found in obs data packages.
95 These are the instrument-specific dataset types written by
96 `writeAdditionalCuratedCalibrations` in addition to the calibrations
97 found in obs data packages that follow the standard scheme.
98 (`set` of `str`)"""
100 @property
101 @abstractmethod
102 def filterDefinitions(self):
103 """`~lsst.obs.base.FilterDefinitionCollection`, defining the filters
104 for this instrument.
105 """
106 return None
108 def __init__(self):
109 self.filterDefinitions.reset()
110 self.filterDefinitions.defineFilters()
112 @classmethod
113 @abstractmethod
114 def getName(cls):
115 """Return the short (dimension) name for this instrument.
117 This is not (in general) the same as the class name - it's what is used
118 as the value of the "instrument" field in data IDs, and is usually an
119 abbreviation of the full name.
120 """
121 raise NotImplementedError()
123 @classmethod
124 @lru_cache()
125 def getCuratedCalibrationNames(cls) -> Set[str]:
126 """Return the names of all the curated calibration dataset types.
128 Returns
129 -------
130 names : `set` of `str`
131 The dataset type names of all curated calibrations. This will
132 include the standard curated calibrations even if the particular
133 instrument does not support them.
135 Notes
136 -----
137 The returned list does not indicate whether a particular dataset
138 is present in the Butler repository, simply that these are the
139 dataset types that are handled by ``writeCuratedCalibrations``.
140 """
142 # Camera is a special dataset type that is also handled as a
143 # curated calibration.
144 curated = {"camera"}
146 # Make a cursory attempt to filter out curated dataset types
147 # that are not present for this instrument
148 for datasetTypeName in cls.standardCuratedDatasetTypes:
149 calibPath = cls._getSpecificCuratedCalibrationPath(datasetTypeName)
150 if calibPath is not None:
151 curated.add(datasetTypeName)
153 curated.update(cls.additionalCuratedDatasetTypes)
154 return frozenset(curated)
156 @abstractmethod
157 def getCamera(self):
158 """Retrieve the cameraGeom representation of this instrument.
160 This is a temporary API that should go away once ``obs_`` packages have
161 a standardized approach to writing versioned cameras to a Gen3 repo.
162 """
163 raise NotImplementedError()
165 @abstractmethod
166 def register(self, registry):
167 """Insert instrument, physical_filter, and detector entries into a
168 `Registry`.
169 """
170 raise NotImplementedError()
172 @classmethod
173 @lru_cache()
174 def getObsDataPackageDir(cls):
175 """The root of the obs data package that provides specializations for
176 this instrument.
178 returns
179 -------
180 dir : `str`
181 The root of the relevat obs data package.
182 """
183 if cls.obsDataPackage is None:
184 return None
185 return getPackageDir(cls.obsDataPackage)
187 @staticmethod
188 def fromName(name: str, registry: Registry) -> Instrument:
189 """Given an instrument name and a butler, retrieve a corresponding
190 instantiated instrument object.
192 Parameters
193 ----------
194 name : `str`
195 Name of the instrument (must match the return value of `getName`).
196 registry : `lsst.daf.butler.Registry`
197 Butler registry to query to find the information.
199 Returns
200 -------
201 instrument : `Instrument`
202 An instance of the relevant `Instrument`.
204 Notes
205 -----
206 The instrument must be registered in the corresponding butler.
208 Raises
209 ------
210 LookupError
211 Raised if the instrument is not known to the supplied registry.
212 ModuleNotFoundError
213 Raised if the class could not be imported. This could mean
214 that the relevant obs package has not been setup.
215 TypeError
216 Raised if the class name retrieved is not a string.
217 """
218 records = list(registry.queryDimensionRecords("instrument", instrument=name))
219 if not records:
220 raise LookupError(f"No registered instrument with name '{name}'.")
221 cls = records[0].class_name
222 if not isinstance(cls, str):
223 raise TypeError(f"Unexpected class name retrieved from {name} instrument dimension (got {cls})")
224 instrument = doImport(cls)
225 return instrument()
227 @staticmethod
228 def importAll(registry: Registry) -> None:
229 """Import all the instruments known to this registry.
231 This will ensure that all metadata translators have been registered.
233 Parameters
234 ----------
235 registry : `lsst.daf.butler.Registry`
236 Butler registry to query to find the information.
238 Notes
239 -----
240 It is allowed for a particular instrument class to fail on import.
241 This might simply indicate that a particular obs package has
242 not been setup.
243 """
244 records = list(registry.queryDimensionRecords("instrument"))
245 for record in records:
246 cls = record.class_name
247 try:
248 doImport(cls)
249 except Exception:
250 pass
252 def _registerFilters(self, registry):
253 """Register the physical and abstract filter Dimension relationships.
254 This should be called in the ``register`` implementation.
256 Parameters
257 ----------
258 registry : `lsst.daf.butler.core.Registry`
259 The registry to add dimensions to.
260 """
261 for filter in self.filterDefinitions:
262 # fix for undefined abstract filters causing trouble in the registry:
263 if filter.band is None:
264 band = filter.physical_filter
265 else:
266 band = filter.band
268 registry.insertDimensionData("physical_filter",
269 {"instrument": self.getName(),
270 "name": filter.physical_filter,
271 "band": band
272 })
274 @abstractmethod
275 def getRawFormatter(self, dataId):
276 """Return the Formatter class that should be used to read a particular
277 raw file.
279 Parameters
280 ----------
281 dataId : `DataCoordinate`
282 Dimension-based ID for the raw file or files being ingested.
284 Returns
285 -------
286 formatter : `Formatter` class
287 Class to be used that reads the file into an
288 `lsst.afw.image.Exposure` instance.
289 """
290 raise NotImplementedError()
292 def applyConfigOverrides(self, name, config):
293 """Apply instrument-specific overrides for a task config.
295 Parameters
296 ----------
297 name : `str`
298 Name of the object being configured; typically the _DefaultName
299 of a Task.
300 config : `lsst.pex.config.Config`
301 Config instance to which overrides should be applied.
302 """
303 for root in self.configPaths:
304 path = os.path.join(root, f"{name}.py")
305 if os.path.exists(path):
306 config.load(path)
308 def writeCuratedCalibrations(self, butler: Butler, collection: Optional[str] = None,
309 suffixes: Sequence[str] = ()) -> None:
310 """Write human-curated calibration Datasets to the given Butler with
311 the appropriate validity ranges.
313 Parameters
314 ----------
315 butler : `lsst.daf.butler.Butler`
316 Butler to use to store these calibrations.
317 collection : `str`, optional
318 Name to use for the calibration collection that associates all
319 datasets with a validity range. If this collection already exists,
320 it must be a `~CollectionType.CALIBRATION` collection, and it must
321 not have any datasets that would conflict with those inserted by
322 this method. If `None`, a collection name is worked out
323 automatically from the instrument name and other metadata by
324 calling ``makeCalibrationCollectionName``, but this
325 default name may not work well for long-lived repositories unless
326 one or more ``suffixes`` are also provided (and changed every time
327 curated calibrations are ingested).
328 suffixes : `Sequence` [ `str` ], optional
329 Name suffixes to append to collection names, after concatenating
330 them with the standard collection name delimeter. If provided,
331 these are appended to the names of the `~CollectionType.RUN`
332 collections that datasets are inserted directly into, as well the
333 `~CollectionType.CALIBRATION` collection if it is generated
334 automatically (i.e. if ``collection is None``).
336 Notes
337 -----
338 Expected to be called from subclasses. The base method calls
339 ``writeCameraGeom``, ``writeStandardTextCuratedCalibrations``,
340 and ``writeAdditionalCuratdCalibrations``.
341 """
342 # Delegate registration of collections (and creating names for them)
343 # to other methods so they can be called independently with the same
344 # preconditions. Collection registration is idempotent, so this is
345 # safe, and while it adds a bit of overhead, as long as it's one
346 # registration attempt per method (not per dataset or dataset type),
347 # that's negligible.
348 self.writeCameraGeom(butler, collection, *suffixes)
349 self.writeStandardTextCuratedCalibrations(butler, collection, suffixes=suffixes)
350 self.writeAdditionalCuratedCalibrations(butler, collection, suffixes=suffixes)
352 def writeAdditionalCuratedCalibrations(self, butler: Butler, collection: Optional[str] = None,
353 suffixes: Sequence[str] = ()) -> None:
354 """Write additional curated calibrations that might be instrument
355 specific and are not part of the standard set.
357 Default implementation does nothing.
359 Parameters
360 ----------
361 butler : `lsst.daf.butler.Butler`
362 Butler to use to store these calibrations.
363 collection : `str`, optional
364 Name to use for the calibration collection that associates all
365 datasets with a validity range. If this collection already exists,
366 it must be a `~CollectionType.CALIBRATION` collection, and it must
367 not have any datasets that would conflict with those inserted by
368 this method. If `None`, a collection name is worked out
369 automatically from the instrument name and other metadata by
370 calling ``makeCalibrationCollectionName``, but this
371 default name may not work well for long-lived repositories unless
372 one or more ``suffixes`` are also provided (and changed every time
373 curated calibrations are ingested).
374 suffixes : `Sequence` [ `str` ], optional
375 Name suffixes to append to collection names, after concatenating
376 them with the standard collection name delimeter. If provided,
377 these are appended to the names of the `~CollectionType.RUN`
378 collections that datasets are inserted directly into, as well the
379 `~CollectionType.CALIBRATION` collection if it is generated
380 automatically (i.e. if ``collection is None``).
381 """
382 return
384 def writeCameraGeom(self, butler: Butler, collection: Optional[str] = None,
385 suffixes: Sequence[str] = ()) -> None:
386 """Write the default camera geometry to the butler repository and
387 associate it with the appropriate validity range in a calibration
388 collection.
390 Parameters
391 ----------
392 butler : `lsst.daf.butler.Butler`
393 Butler to use to store these calibrations.
394 collection : `str`, optional
395 Name to use for the calibration collection that associates all
396 datasets with a validity range. If this collection already exists,
397 it must be a `~CollectionType.CALIBRATION` collection, and it must
398 not have any datasets that would conflict with those inserted by
399 this method. If `None`, a collection name is worked out
400 automatically from the instrument name and other metadata by
401 calling ``makeCalibrationCollectionName``, but this
402 default name may not work well for long-lived repositories unless
403 one or more ``suffixes`` are also provided (and changed every time
404 curated calibrations are ingested).
405 suffixes : `Sequence` [ `str` ], optional
406 Name suffixes to append to collection names, after concatenating
407 them with the standard collection name delimeter. If provided,
408 these are appended to the names of the `~CollectionType.RUN`
409 collections that datasets are inserted directly into, as well the
410 `~CollectionType.CALIBRATION` collection if it is generated
411 automatically (i.e. if ``collection is None``).
412 """
413 if collection is None:
414 collection = self.makeCalibrationCollectionName(*suffixes)
415 butler.registry.registerCollection(collection, type=CollectionType.CALIBRATION)
416 run = self.makeUnboundedCalibrationRunName(*suffixes)
417 butler.registry.registerRun(run)
418 datasetType = DatasetType("camera", ("instrument",), "Camera", isCalibration=True,
419 universe=butler.registry.dimensions)
420 butler.registry.registerDatasetType(datasetType)
421 camera = self.getCamera()
422 ref = butler.put(camera, datasetType, {"instrument": self.getName()}, run=run)
423 butler.registry.certify(collection, [ref], Timespan(begin=None, end=None))
425 def writeStandardTextCuratedCalibrations(self, butler: Butler, collection: Optional[str] = None,
426 suffixes: Sequence[str] = ()) -> None:
427 """Write the set of standardized curated text calibrations to
428 the repository.
430 Parameters
431 ----------
432 butler : `lsst.daf.butler.Butler`
433 Butler to receive these calibration datasets.
434 collection : `str`, optional
435 Name to use for the calibration collection that associates all
436 datasets with a validity range. If this collection already exists,
437 it must be a `~CollectionType.CALIBRATION` collection, and it must
438 not have any datasets that would conflict with those inserted by
439 this method. If `None`, a collection name is worked out
440 automatically from the instrument name and other metadata by
441 calling ``makeCalibrationCollectionName``, but this
442 default name may not work well for long-lived repositories unless
443 one or more ``suffixes`` are also provided (and changed every time
444 curated calibrations are ingested).
445 suffixes : `Sequence` [ `str` ], optional
446 Name suffixes to append to collection names, after concatenating
447 them with the standard collection name delimeter. If provided,
448 these are appended to the names of the `~CollectionType.RUN`
449 collections that datasets are inserted directly into, as well the
450 `~CollectionType.CALIBRATION` collection if it is generated
451 automatically (i.e. if ``collection is None``).
452 """
453 if collection is None:
454 collection = self.makeCalibrationCollectionName(*suffixes)
455 butler.registry.registerCollection(collection, type=CollectionType.CALIBRATION)
456 runs = set()
457 for datasetTypeName in self.standardCuratedDatasetTypes:
458 # We need to define the dataset types.
459 if datasetTypeName not in StandardCuratedCalibrationDatasetTypes:
460 raise ValueError(f"DatasetType {datasetTypeName} not in understood list"
461 f" [{'.'.join(StandardCuratedCalibrationDatasetTypes)}]")
462 definition = StandardCuratedCalibrationDatasetTypes[datasetTypeName]
463 datasetType = DatasetType(datasetTypeName,
464 universe=butler.registry.dimensions,
465 isCalibration=True,
466 **definition)
467 self._writeSpecificCuratedCalibrationDatasets(butler, datasetType, collection, runs=runs,
468 suffixes=suffixes)
470 @classmethod
471 def _getSpecificCuratedCalibrationPath(cls, datasetTypeName):
472 """Return the path of the curated calibration directory.
474 Parameters
475 ----------
476 datasetTypeName : `str`
477 The name of the standard dataset type to find.
479 Returns
480 -------
481 path : `str`
482 The path to the standard curated data directory. `None` if the
483 dataset type is not found or the obs data package is not
484 available.
485 """
486 if cls.getObsDataPackageDir() is None:
487 # if there is no data package then there can't be datasets
488 return None
490 calibPath = os.path.join(cls.getObsDataPackageDir(), cls.policyName,
491 datasetTypeName)
493 if os.path.exists(calibPath):
494 return calibPath
496 return None
498 def _writeSpecificCuratedCalibrationDatasets(self, butler: Butler, datasetType: DatasetType,
499 collection: str, runs: Set[str], suffixes: Sequence[str]):
500 """Write standardized curated calibration datasets for this specific
501 dataset type from an obs data package.
503 Parameters
504 ----------
505 butler : `lsst.daf.butler.Butler`
506 Gen3 butler in which to put the calibrations.
507 datasetType : `lsst.daf.butler.DatasetType`
508 Dataset type to be put.
509 collection : `str`
510 Name of the `~CollectionType.CALIBRATION` collection that
511 associates all datasets with validity ranges. Must have been
512 registered prior to this call.
513 runs : `set` [ `str` ]
514 Names of runs that have already been registered by previous calls
515 and need not be registered again. Should be updated by this
516 method as new runs are registered.
517 suffixes : `Sequence` [ `str` ]
518 Suffixes to append to run names when creating them from
519 ``CALIBDATE`` metadata, via calls to `makeCuratedCalibrationName`.
521 Notes
522 -----
523 This method scans the location defined in the ``obsDataPackageDir``
524 class attribute for curated calibrations corresponding to the
525 supplied dataset type. The directory name in the data package must
526 match the name of the dataset type. They are assumed to use the
527 standard layout and can be read by
528 `~lsst.pipe.tasks.read_curated_calibs.read_all` and provide standard
529 metadata.
530 """
531 calibPath = self._getSpecificCuratedCalibrationPath(datasetType.name)
532 if calibPath is None:
533 return
535 # Register the dataset type
536 butler.registry.registerDatasetType(datasetType)
538 # obs_base can't depend on pipe_tasks but concrete obs packages
539 # can -- we therefore have to defer import
540 from lsst.pipe.tasks.read_curated_calibs import read_all
542 # Read calibs, registering a new run for each CALIBDATE as needed.
543 # We try to avoid registering runs multiple times as an optimization
544 # by putting them in the ``runs`` set that was passed in.
545 camera = self.getCamera()
546 calibsDict = read_all(calibPath, camera)[0] # second return is calib type
547 datasetRecords = []
548 for det in calibsDict:
549 times = sorted([k for k in calibsDict[det]])
550 calibs = [calibsDict[det][time] for time in times]
551 times = [astropy.time.Time(t, format="datetime", scale="utc") for t in times]
552 times += [None]
553 for calib, beginTime, endTime in zip(calibs, times[:-1], times[1:]):
554 md = calib.getMetadata()
555 run = self.makeCuratedCalibrationRunName(md['CALIBDATE'], *suffixes)
556 if run not in runs:
557 butler.registry.registerRun(run)
558 runs.add(run)
559 dataId = DataCoordinate.standardize(
560 universe=butler.registry.dimensions,
561 instrument=self.getName(),
562 detector=md["DETECTOR"],
563 )
564 datasetRecords.append((calib, dataId, run, Timespan(beginTime, endTime)))
566 # Second loop actually does the inserts and filesystem writes. We
567 # first do a butler.put on each dataset, inserting it into the run for
568 # its calibDate. We remember those refs and group them by timespan, so
569 # we can vectorize the certify calls as much as possible.
570 refsByTimespan = defaultdict(list)
571 with butler.transaction():
572 for calib, dataId, run, timespan in datasetRecords:
573 refsByTimespan[timespan].append(butler.put(calib, datasetType, dataId, run=run))
574 for timespan, refs in refsByTimespan.items():
575 butler.registry.certify(collection, refs, timespan)
577 @abstractmethod
578 def makeDataIdTranslatorFactory(self) -> TranslatorFactory:
579 """Return a factory for creating Gen2->Gen3 data ID translators,
580 specialized for this instrument.
582 Derived class implementations should generally call
583 `TranslatorFactory.addGenericInstrumentRules` with appropriate
584 arguments, but are not required to (and may not be able to if their
585 Gen2 raw data IDs are sufficiently different from the HSC/DECam/CFHT
586 norm).
588 Returns
589 -------
590 factory : `TranslatorFactory`.
591 Factory for `Translator` objects.
592 """
593 raise NotImplementedError("Must be implemented by derived classes.")
595 @classmethod
596 def makeDefaultRawIngestRunName(cls) -> str:
597 """Make the default instrument-specific run collection string for raw
598 data ingest.
600 Returns
601 -------
602 coll : `str`
603 Run collection name to be used as the default for ingestion of
604 raws.
605 """
606 return cls.makeCollectionName("raw", "all")
608 @classmethod
609 def makeUnboundedCalibrationRunName(cls, *suffixes: str) -> str:
610 """Make a RUN collection name appropriate for inserting calibration
611 datasets whose validity ranges are unbounded.
613 Parameters
614 ----------
615 *suffixes : `str`
616 Strings to be appended to the base name, using the default
617 delimiter for collection names.
619 Returns
620 -------
621 name : `str`
622 Run collection name.
623 """
624 return cls.makeCollectionName("calib", "unbounded", *suffixes)
626 @classmethod
627 def makeCuratedCalibrationRunName(cls, calibDate: str, *suffixes: str) -> str:
628 """Make a RUN collection name appropriate for inserting curated
629 calibration datasets with the given ``CALIBDATE`` metadata value.
631 Parameters
632 ----------
633 calibDate : `str`
634 The ``CALIBDATE`` metadata value.
635 *suffixes : `str`
636 Strings to be appended to the base name, using the default
637 delimiter for collection names.
639 Returns
640 -------
641 name : `str`
642 Run collection name.
643 """
644 return cls.makeCollectionName("calib", "curated", calibDate, *suffixes)
646 @classmethod
647 def makeCalibrationCollectionName(cls, *suffixes: str) -> str:
648 """Make a CALIBRATION collection name appropriate for associating
649 calibration datasets with validity ranges.
651 Parameters
652 ----------
653 *suffixes : `str`
654 Strings to be appended to the base name, using the default
655 delimiter for collection names.
657 Returns
658 -------
659 name : `str`
660 Calibration collection name.
661 """
662 return cls.makeCollectionName("calib", *suffixes)
664 @classmethod
665 def makeCollectionName(cls, *labels: str) -> str:
666 """Get the instrument-specific collection string to use as derived
667 from the supplied labels.
669 Parameters
670 ----------
671 *labels : `str`
672 Strings to be combined with the instrument name to form a
673 collection name.
675 Returns
676 -------
677 name : `str`
678 Collection name to use that includes the instrument name.
679 """
680 return "/".join((cls.getName(),) + labels)
683def makeExposureRecordFromObsInfo(obsInfo, universe):
684 """Construct an exposure DimensionRecord from
685 `astro_metadata_translator.ObservationInfo`.
687 Parameters
688 ----------
689 obsInfo : `astro_metadata_translator.ObservationInfo`
690 A `~astro_metadata_translator.ObservationInfo` object corresponding to
691 the exposure.
692 universe : `DimensionUniverse`
693 Set of all known dimensions.
695 Returns
696 -------
697 record : `DimensionRecord`
698 A record containing exposure metadata, suitable for insertion into
699 a `Registry`.
700 """
701 dimension = universe["exposure"]
703 ra, dec, sky_angle, zenith_angle = (None, None, None, None)
704 if obsInfo.tracking_radec is not None:
705 icrs = obsInfo.tracking_radec.icrs
706 ra = icrs.ra.degree
707 dec = icrs.dec.degree
708 if obsInfo.boresight_rotation_coord == "sky":
709 sky_angle = obsInfo.boresight_rotation_angle.degree
710 if obsInfo.altaz_begin is not None:
711 zenith_angle = obsInfo.altaz_begin.zen.degree
713 return dimension.RecordClass(
714 instrument=obsInfo.instrument,
715 id=obsInfo.exposure_id,
716 name=obsInfo.observation_id,
717 group_name=obsInfo.exposure_group,
718 group_id=obsInfo.visit_id,
719 datetime_begin=obsInfo.datetime_begin,
720 datetime_end=obsInfo.datetime_end,
721 exposure_time=obsInfo.exposure_time.to_value("s"),
722 dark_time=obsInfo.dark_time.to_value("s"),
723 observation_type=obsInfo.observation_type,
724 physical_filter=obsInfo.physical_filter,
725 science_program=obsInfo.science_program,
726 target_name=obsInfo.object,
727 tracking_ra=ra,
728 tracking_dec=dec,
729 sky_angle=sky_angle,
730 zenith_angle=zenith_angle,
731 )
734def loadCamera(butler: Butler, dataId: DataId, *, collections: Any = None) -> Tuple[Camera, bool]:
735 """Attempt to load versioned camera geometry from a butler, but fall back
736 to obtaining a nominal camera from the `Instrument` class if that fails.
738 Parameters
739 ----------
740 butler : `lsst.daf.butler.Butler`
741 Butler instance to attempt to query for and load a ``camera`` dataset
742 from.
743 dataId : `dict` or `DataCoordinate`
744 Data ID that identifies at least the ``instrument`` and ``exposure``
745 dimensions.
746 collections : Any, optional
747 Collections to be searched, overriding ``self.butler.collections``.
748 Can be any of the types supported by the ``collections`` argument
749 to butler construction.
751 Returns
752 -------
753 camera : `lsst.afw.cameraGeom.Camera`
754 Camera object.
755 versioned : `bool`
756 If `True`, the camera was obtained from the butler and should represent
757 a versioned camera from a calibration repository. If `False`, no
758 camera datasets were found, and the returned camera was produced by
759 instantiating the appropriate `Instrument` class and calling
760 `Instrument.getCamera`.
761 """
762 if collections is None:
763 collections = butler.collections
764 # Registry would do data ID expansion internally if we didn't do it first,
765 # but we might want an expanded data ID ourselves later, so we do it here
766 # to ensure it only happens once.
767 # This will also catch problems with the data ID not having keys we need.
768 dataId = butler.registry.expandDataId(dataId, graph=butler.registry.dimensions["exposure"].graph)
769 cameraRefs = list(butler.registry.queryDatasets("camera", dataId=dataId, collections=collections,
770 deduplicate=True))
771 if cameraRefs:
772 assert len(cameraRefs) == 1, "Should be guaranteed by deduplicate=True above."
773 return butler.getDirect(cameraRefs[0]), True
774 instrument = Instrument.fromName(dataId["instrument"], butler.registry)
775 return instrument.getCamera(), False