lsst.obs.base  20.0.0-32-ge89cb00+c14a4dfd40
defineVisits.py
Go to the documentation of this file.
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/>.
21 
22 from __future__ import annotations
23 
24 __all__ = [
25  "DefineVisitsConfig",
26  "DefineVisitsTask",
27  "GroupExposuresConfig",
28  "GroupExposuresTask",
29  "VisitDefinitionData",
30 ]
31 
32 from abc import ABCMeta, abstractmethod
33 from collections import defaultdict
34 import dataclasses
35 from typing import Any, Dict, Iterable, List, Optional, Tuple
36 from multiprocessing import Pool
37 
38 from lsst.daf.butler import (
39  Butler,
40  DataId,
41  DimensionGraph,
42  DimensionRecord,
43  Timespan,
44  TIMESPAN_FIELD_SPECS,
45 )
46 
47 import lsst.geom
48 from lsst.geom import Box2D
49 from lsst.pex.config import Config, Field, makeRegistry, registerConfigurable
50 from lsst.afw.cameraGeom import FOCAL_PLANE, PIXELS
51 from lsst.pipe.base import Task
52 from lsst.sphgeom import ConvexPolygon, Region, UnitVector3d
53 from ._instrument import loadCamera, Instrument
54 
55 
56 @dataclasses.dataclass
58  """Struct representing a group of exposures that will be used to define a
59  visit.
60  """
61 
62  instrument: str
63  """Name of the instrument this visit will be associated with.
64  """
65 
66  id: int
67  """Integer ID of the visit.
68 
69  This must be unique across all visit systems for the instrument.
70  """
71 
72  name: str
73  """String name for the visit.
74 
75  This must be unique across all visit systems for the instrument.
76  """
77 
78  exposures: List[DimensionRecord] = dataclasses.field(default_factory=list)
79  """Dimension records for the exposures that are part of this visit.
80  """
81 
82 
83 @dataclasses.dataclass
85  """Struct containing the dimension records associated with a visit.
86  """
87 
88  visit: DimensionRecord
89  """Record for the 'visit' dimension itself.
90  """
91 
92  visit_definition: List[DimensionRecord]
93  """Records for 'visit_definition', which relates 'visit' to 'exposure'.
94  """
95 
96  visit_detector_region: List[DimensionRecord]
97  """Records for 'visit_detector_region', which associates the combination
98  of a 'visit' and a 'detector' with a region on the sky.
99  """
100 
101 
102 class GroupExposuresConfig(Config):
103  pass
104 
105 
106 class GroupExposuresTask(Task, metaclass=ABCMeta):
107  """Abstract base class for the subtask of `DefineVisitsTask` that is
108  responsible for grouping exposures into visits.
109 
110  Subclasses should be registered with `GroupExposuresTask.registry` to
111  enable use by `DefineVisitsTask`, and should generally correspond to a
112  particular 'visit_system' dimension value. They are also responsible for
113  defining visit IDs and names that are unique across all visit systems in
114  use by an instrument.
115 
116  Parameters
117  ----------
118  config : `GroupExposuresConfig`
119  Configuration information.
120  **kwargs
121  Additional keyword arguments forwarded to the `Task` constructor.
122  """
123  def __init__(self, config: GroupExposuresConfig, **kwargs: Any):
124  Task.__init__(self, config=config, **kwargs)
125 
126  ConfigClass = GroupExposuresConfig
127 
128  _DefaultName = "groupExposures"
129 
130  registry = makeRegistry(
131  doc="Registry of algorithms for grouping exposures into visits.",
132  configBaseType=GroupExposuresConfig,
133  )
134 
135  @abstractmethod
136  def group(self, exposures: List[DimensionRecord]) -> Iterable[VisitDefinitionData]:
137  """Group the given exposures into visits.
138 
139  Parameters
140  ----------
141  exposures : `list` [ `DimensionRecord` ]
142  DimensionRecords (for the 'exposure' dimension) describing the
143  exposures to group.
144 
145  Returns
146  -------
147  visits : `Iterable` [ `VisitDefinitionData` ]
148  Structs identifying the visits and the exposures associated with
149  them. This may be an iterator or a container.
150  """
151  raise NotImplementedError()
152 
153  @abstractmethod
154  def getVisitSystem(self) -> Tuple[int, str]:
155  """Return identifiers for the 'visit_system' dimension this
156  algorithm implements.
157 
158  Returns
159  -------
160  id : `int`
161  Integer ID for the visit system (given an instrument).
162  name : `str`
163  Unique string identifier for the visit system (given an
164  instrument).
165  """
166  raise NotImplementedError()
167 
168 
170  padding = Field(
171  dtype=int,
172  default=0,
173  doc=("Pad raw image bounding boxes with specified number of pixels "
174  "when calculating their (conservatively large) region on the "
175  "sky."),
176  )
177 
178 
179 class ComputeVisitRegionsTask(Task, metaclass=ABCMeta):
180  """Abstract base class for the subtask of `DefineVisitsTask` that is
181  responsible for extracting spatial regions for visits and visit+detector
182  combinations.
183 
184  Subclasses should be registered with `ComputeVisitRegionsTask.registry` to
185  enable use by `DefineVisitsTask`.
186 
187  Parameters
188  ----------
189  config : `ComputeVisitRegionsConfig`
190  Configuration information.
191  butler : `lsst.daf.butler.Butler`
192  The butler to use.
193  **kwargs
194  Additional keyword arguments forwarded to the `Task` constructor.
195  """
196  def __init__(self, config: ComputeVisitRegionsConfig, *, butler: Butler, **kwargs: Any):
197  Task.__init__(self, config=config, **kwargs)
198  self.butler = butler
199  self.instrumentMap = {}
200 
201  ConfigClass = ComputeVisitRegionsConfig
202 
203  _DefaultName = "computeVisitRegions"
204 
205  registry = makeRegistry(
206  doc=("Registry of algorithms for computing on-sky regions for visits "
207  "and visit+detector combinations."),
208  configBaseType=ComputeVisitRegionsConfig,
209  )
210 
211  def getInstrument(self, instrumentName) -> Instrument:
212  """Retrieve an `~lsst.obs.base.Instrument` associated with this
213  instrument name.
214 
215  Parameters
216  ----------
217  instrumentName : `str`
218  The name of the instrument.
219 
220  Returns
221  -------
222  instrument : `~lsst.obs.base.Instrument`
223  The associated instrument object.
224 
225  Notes
226  -----
227  The result is cached.
228  """
229  instrument = self.instrumentMap.get(instrumentName)
230  if instrument is None:
231  instrument = Instrument.fromName(instrumentName, self.butler.registry)
232  self.instrumentMap[instrumentName] = instrument
233  return instrument
234 
235  @abstractmethod
236  def compute(self, visit: VisitDefinitionData, *, collections: Any = None
237  ) -> Tuple[Region, Dict[int, Region]]:
238  """Compute regions for the given visit and all detectors in that visit.
239 
240  Parameters
241  ----------
242  visit : `VisitDefinitionData`
243  Struct describing the visit and the exposures associated with it.
244  collections : Any, optional
245  Collections to be searched for raws and camera geometry, overriding
246  ``self.butler.collections``.
247  Can be any of the types supported by the ``collections`` argument
248  to butler construction.
249 
250  Returns
251  -------
252  visitRegion : `lsst.sphgeom.Region`
253  Region for the full visit.
254  visitDetectorRegions : `dict` [ `int`, `lsst.sphgeom.Region` ]
255  Dictionary mapping detector ID to the region for that detector.
256  Should include all detectors in the visit.
257  """
258  raise NotImplementedError()
259 
260 
261 class DefineVisitsConfig(Config):
262  groupExposures = GroupExposuresTask.registry.makeField(
263  doc="Algorithm for grouping exposures into visits.",
264  default="one-to-one",
265  )
266  computeVisitRegions = ComputeVisitRegionsTask.registry.makeField(
267  doc="Algorithm from computing visit and visit+detector regions.",
268  default="single-raw-wcs",
269  )
270  ignoreNonScienceExposures = Field(
271  doc=("If True, silently ignore input exposures that do not have "
272  "observation_type=SCIENCE. If False, raise an exception if one "
273  "encountered."),
274  dtype=bool,
275  optional=False,
276  default=True,
277  )
278 
279 
280 class DefineVisitsTask(Task):
281  """Driver Task for defining visits (and their spatial regions) in Gen3
282  Butler repositories.
283 
284  Parameters
285  ----------
286  config : `DefineVisitsConfig`
287  Configuration for the task.
288  butler : `~lsst.daf.butler.Butler`
289  Writeable butler instance. Will be used to read `raw.wcs` and `camera`
290  datasets and insert/sync dimension data.
291  **kwargs
292  Additional keyword arguments are forwarded to the `lsst.pipe.base.Task`
293  constructor.
294 
295  Notes
296  -----
297  Each instance of `DefineVisitsTask` reads from / writes to the same Butler.
298  Each invocation of `DefineVisitsTask.run` processes an independent group of
299  exposures into one or more new vists, all belonging to the same visit
300  system and instrument.
301 
302  The actual work of grouping exposures and computing regions is delegated
303  to pluggable subtasks (`GroupExposuresTask` and `ComputeVisitRegionsTask`),
304  respectively. The defaults are to create one visit for every exposure,
305  and to use exactly one (arbitrary) detector-level raw dataset's WCS along
306  with camera geometry to compute regions for all detectors. Other
307  implementations can be created and configured for instruments for which
308  these choices are unsuitable (e.g. because visits and exposures are not
309  one-to-one, or because ``raw.wcs`` datasets for different detectors may not
310  be consistent with camera geomery).
311 
312  It is not necessary in general to ingest all raws for an exposure before
313  defining a visit that includes the exposure; this depends entirely on the
314  `ComputeVisitRegionTask` subclass used. For the default configuration,
315  a single raw for each exposure is sufficient.
316  """
317  def __init__(self, config: Optional[DefineVisitsConfig] = None, *, butler: Butler, **kwargs: Any):
318  config.validate() # Not a CmdlineTask nor PipelineTask, so have to validate the config here.
319  super().__init__(config, **kwargs)
320  self.butler = butler
321  self.universe = self.butler.registry.dimensions
322  self.makeSubtask("groupExposures")
323  self.makeSubtask("computeVisitRegions", butler=self.butler)
324 
325  ConfigClass = DefineVisitsConfig
326 
327  _DefaultName = "defineVisits"
328 
329  def _buildVisitRecords(self, definition: VisitDefinitionData, *,
330  collections: Any = None) -> _VisitRecords:
331  """Build the DimensionRecords associated with a visit.
332 
333  Parameters
334  ----------
335  definition : `VisitDefinition`
336  Struct with identifiers for the visit and records for its
337  constituent exposures.
338  collections : Any, optional
339  Collections to be searched for raws and camera geometry, overriding
340  ``self.butler.collections``.
341  Can be any of the types supported by the ``collections`` argument
342  to butler construction.
343 
344  Results
345  -------
346  records : `_VisitRecords`
347  Struct containing DimensionRecords for the visit, including
348  associated dimension elements.
349  """
350  # Compute all regions.
351  visitRegion, visitDetectorRegions = self.computeVisitRegions.compute(definition,
352  collections=collections)
353  # Aggregate other exposure quantities.
354  timespan = Timespan(
355  begin=_reduceOrNone(min, (e.timespan.begin for e in definition.exposures)),
356  end=_reduceOrNone(max, (e.timespan.end for e in definition.exposures)),
357  )
358  exposure_time = _reduceOrNone(sum, (e.exposure_time for e in definition.exposures))
359  physical_filter = _reduceOrNone(lambda a, b: a if a == b else None,
360  (e.physical_filter for e in definition.exposures))
361  target_name = _reduceOrNone(lambda a, b: a if a == b else None,
362  (e.target_name for e in definition.exposures))
363  science_program = _reduceOrNone(lambda a, b: a if a == b else None,
364  (e.science_program for e in definition.exposures))
365 
366  # Use the mean zenith angle as an approximation
367  zenith_angle = _reduceOrNone(sum, (e.zenith_angle for e in definition.exposures))
368  if zenith_angle is not None:
369  zenith_angle /= len(definition.exposures)
370 
371  # Construct the actual DimensionRecords.
372  return _VisitRecords(
373  visit=self.universe["visit"].RecordClass.fromDict({
374  "instrument": definition.instrument,
375  "id": definition.id,
376  "name": definition.name,
377  "physical_filter": physical_filter,
378  "target_name": target_name,
379  "science_program": science_program,
380  "zenith_angle": zenith_angle,
381  "visit_system": self.groupExposures.getVisitSystem()[0],
382  "exposure_time": exposure_time,
383  TIMESPAN_FIELD_SPECS.begin.name: timespan.begin,
384  TIMESPAN_FIELD_SPECS.end.name: timespan.end,
385  "region": visitRegion,
386  # TODO: no seeing value in exposure dimension records, so we can't
387  # set that here. But there are many other columns that both
388  # dimensions should probably have as well.
389  }),
390  visit_definition=[
391  self.universe["visit_definition"].RecordClass.fromDict({
392  "instrument": definition.instrument,
393  "visit": definition.id,
394  "exposure": exposure.id,
395  "visit_system": self.groupExposures.getVisitSystem()[0],
396  })
397  for exposure in definition.exposures
398  ],
399  visit_detector_region=[
400  self.universe["visit_detector_region"].RecordClass.fromDict({
401  "instrument": definition.instrument,
402  "visit": definition.id,
403  "detector": detectorId,
404  "region": detectorRegion,
405  })
406  for detectorId, detectorRegion in visitDetectorRegions.items()
407  ]
408  )
409 
410  def run(self, dataIds: Iterable[DataId], *,
411  pool: Optional[Pool] = None,
412  processes: int = 1,
413  collections: Optional[str] = None):
414  """Add visit definitions to the registry for the given exposures.
415 
416  Parameters
417  ----------
418  dataIds : `Iterable` [ `dict` or `DataCoordinate` ]
419  Exposure-level data IDs. These must all correspond to the same
420  instrument, and are expected to be on-sky science exposures.
421  pool : `multiprocessing.Pool`, optional
422  If not `None`, a process pool with which to parallelize some
423  operations.
424  processes : `int`, optional
425  The number of processes to use. Ignored if ``pool`` is not `None`.
426  collections : Any, optional
427  Collections to be searched for raws and camera geometry, overriding
428  ``self.butler.collections``.
429  Can be any of the types supported by the ``collections`` argument
430  to butler construction.
431  """
432  # Set up multiprocessing, if desired.
433  if pool is None and processes > 1:
434  pool = Pool(processes)
435  mapFunc = map if pool is None else pool.imap_unordered
436  # Normalize, expand, and deduplicate data IDs.
437  self.log.info("Preprocessing data IDs.")
438  dimensions = DimensionGraph(self.universe, names=["exposure"])
439  dataIds = set(mapFunc(lambda d: self.butler.registry.expandDataId(d, graph=dimensions), dataIds))
440  if not dataIds:
441  raise RuntimeError("No exposures given.")
442  # Extract exposure DimensionRecords, check that there's only one
443  # instrument in play, and check for non-science exposures.
444  exposures = []
445  instruments = set()
446  for dataId in dataIds:
447  record = dataId.records["exposure"]
448  if record.observation_type != "science":
449  if self.config.ignoreNonScienceExposures:
450  continue
451  else:
452  raise RuntimeError(f"Input exposure {dataId} has observation_type "
453  f"{record.observation_type}, not 'science'.")
454  instruments.add(dataId["instrument"])
455  exposures.append(record)
456  if not exposures:
457  self.log.info("No science exposures found after filtering.")
458  return
459  if len(instruments) > 1:
460  raise RuntimeError(
461  f"All data IDs passed to DefineVisitsTask.run must be "
462  f"from the same instrument; got {instruments}."
463  )
464  instrument, = instruments
465  # Ensure the visit_system our grouping algorithm uses is in the
466  # registry, if it wasn't already.
467  visitSystemId, visitSystemName = self.groupExposures.getVisitSystem()
468  self.log.info("Registering visit_system %d: %s.", visitSystemId, visitSystemName)
469  self.butler.registry.syncDimensionData(
470  "visit_system",
471  {"instrument": instrument, "id": visitSystemId, "name": visitSystemName}
472  )
473  # Group exposures into visits, delegating to subtask.
474  self.log.info("Grouping %d exposure(s) into visits.", len(exposures))
475  definitions = list(self.groupExposures.group(exposures))
476  # Compute regions and build DimensionRecords for each visit.
477  # This is the only parallel step, but it _should_ be the most expensive
478  # one (unless DB operations are slow).
479  self.log.info("Computing regions and other metadata for %d visit(s).", len(definitions))
480  allRecords = mapFunc(lambda d: self._buildVisitRecords(d, collections=collections), definitions)
481  # Iterate over visits and insert dimension data, one transaction per
482  # visit.
483  for visitRecords in allRecords:
484  with self.butler.registry.transaction():
485  self.butler.registry.insertDimensionData("visit", visitRecords.visit)
486  self.butler.registry.insertDimensionData("visit_definition",
487  *visitRecords.visit_definition)
488  self.butler.registry.insertDimensionData("visit_detector_region",
489  *visitRecords.visit_detector_region)
490 
491 
492 def _reduceOrNone(func, iterable):
493  """Apply a binary function to pairs of elements in an iterable until a
494  single value is returned, but return `None` if any element is `None` or
495  there are no elements.
496  """
497  r = None
498  for v in iterable:
499  if v is None:
500  return None
501  if r is None:
502  r = v
503  else:
504  r = func(r, v)
505  return r
506 
507 
509  visitSystemId = Field(
510  doc=("Integer ID of the visit_system implemented by this grouping "
511  "algorithm."),
512  dtype=int,
513  default=0,
514  )
515  visitSystemName = Field(
516  doc=("String name of the visit_system implemented by this grouping "
517  "algorithm."),
518  dtype=str,
519  default="one-to-one",
520  )
521 
522 
523 @registerConfigurable("one-to-one", GroupExposuresTask.registry)
525  """An exposure grouping algorithm that simply defines one visit for each
526  exposure, reusing the exposures identifiers for the visit.
527  """
528 
529  ConfigClass = _GroupExposuresOneToOneConfig
530 
531  def group(self, exposures: List[DimensionRecord]) -> Iterable[VisitDefinitionData]:
532  # Docstring inherited from GroupExposuresTask.
533  for exposure in exposures:
534  yield VisitDefinitionData(
535  instrument=exposure.instrument,
536  id=exposure.id,
537  name=exposure.name,
538  exposures=[exposure],
539  )
540 
541  def getVisitSystem(self) -> Tuple[int, str]:
542  # Docstring inherited from GroupExposuresTask.
543  return (self.config.visitSystemId, self.config.visitSystemName)
544 
545 
547  visitSystemId = Field(
548  doc=("Integer ID of the visit_system implemented by this grouping "
549  "algorithm."),
550  dtype=int,
551  default=1,
552  )
553  visitSystemName = Field(
554  doc=("String name of the visit_system implemented by this grouping "
555  "algorithm."),
556  dtype=str,
557  default="by-group-metadata",
558  )
559 
560 
561 @registerConfigurable("by-group-metadata", GroupExposuresTask.registry)
563  """An exposure grouping algorithm that uses exposure.group_name and
564  exposure.group_id.
565 
566  This algorithm _assumes_ exposure.group_id (generally populated from
567  `astro_metadata_translator.ObservationInfo.visit_id`) is not just unique,
568  but disjoint from all `ObservationInfo.exposure_id` values - if it isn't,
569  it will be impossible to ever use both this grouping algorithm and the
570  one-to-one algorithm for a particular camera in the same data repository.
571  """
572 
573  ConfigClass = _GroupExposuresByGroupMetadataConfig
574 
575  def group(self, exposures: List[DimensionRecord]) -> Iterable[VisitDefinitionData]:
576  # Docstring inherited from GroupExposuresTask.
577  groups = defaultdict(list)
578  for exposure in exposures:
579  groups[exposure.group_name].append(exposure)
580  for visitName, exposuresInGroup in groups.items():
581  instrument = exposuresInGroup[0].instrument
582  visitId = exposuresInGroup[0].group_id
583  assert all(e.group_id == visitId for e in exposuresInGroup), \
584  "Grouping by exposure.group_name does not yield consistent group IDs"
585  yield VisitDefinitionData(instrument=instrument, id=visitId, name=visitName,
586  exposures=exposuresInGroup)
587 
588  def getVisitSystem(self) -> Tuple[int, str]:
589  # Docstring inherited from GroupExposuresTask.
590  return (self.config.visitSystemId, self.config.visitSystemName)
591 
592 
594  mergeExposures = Field(
595  doc=("If True, merge per-detector regions over all exposures in a "
596  "visit (via convex hull) instead of using the first exposure and "
597  "assuming its regions are valid for all others."),
598  dtype=bool,
599  default=False,
600  )
601  detectorId = Field(
602  doc=("Load the WCS for the detector with this ID. If None, use an "
603  "arbitrary detector (the first found in a query of the data "
604  "repository for each exposure (or all exposures, if "
605  "mergeExposures is True)."),
606  dtype=int,
607  optional=True,
608  default=None
609  )
610  requireVersionedCamera = Field(
611  doc=("If True, raise LookupError if version camera geometry cannot be "
612  "loaded for an exposure. If False, use the nominal camera from "
613  "the Instrument class instead."),
614  dtype=bool,
615  optional=False,
616  default=False,
617  )
618 
619 
620 @registerConfigurable("single-raw-wcs", ComputeVisitRegionsTask.registry)
622  """A visit region calculator that uses a single raw WCS and a camera to
623  project the bounding boxes of all detectors onto the sky, relating
624  different detectors by their positions in focal plane coordinates.
625 
626  Notes
627  -----
628  Most instruments should have their raw WCSs determined from a combination
629  of boresight angle, rotator angle, and camera geometry, and hence this
630  algorithm should produce stable results regardless of which detector the
631  raw corresponds to. If this is not the case (e.g. because a per-file FITS
632  WCS is used instead), either the ID of the detector should be fixed (see
633  the ``detectorId`` config parameter) or a different algorithm used.
634  """
635 
636  ConfigClass = _ComputeVisitRegionsFromSingleRawWcsConfig
637 
638  def computeExposureBounds(self, exposure: DimensionRecord, *, collections: Any = None
639  ) -> Dict[int, List[UnitVector3d]]:
640  """Compute the lists of unit vectors on the sphere that correspond to
641  the sky positions of detector corners.
642 
643  Parameters
644  ----------
645  exposure : `DimensionRecord`
646  Dimension record for the exposure.
647  collections : Any, optional
648  Collections to be searched for raws and camera geometry, overriding
649  ``self.butler.collections``.
650  Can be any of the types supported by the ``collections`` argument
651  to butler construction.
652 
653  Returns
654  -------
655  bounds : `dict`
656  Dictionary mapping detector ID to a list of unit vectors on the
657  sphere representing that detector's corners projected onto the sky.
658  """
659  if collections is None:
660  collections = self.butler.collections
661  camera, versioned = loadCamera(self.butler, exposure.dataId, collections=collections)
662  if not versioned and self.config.requireVersionedCamera:
663  raise LookupError(f"No versioned camera found for exposure {exposure.dataId}.")
664 
665  # Derive WCS from boresight information -- if available in registry
666  use_registry = True
667  try:
668  orientation = lsst.geom.Angle(exposure.sky_angle, lsst.geom.degrees)
669  radec = lsst.geom.SpherePoint(lsst.geom.Angle(exposure.tracking_ra, lsst.geom.degrees),
670  lsst.geom.Angle(exposure.tracking_dec, lsst.geom.degrees))
671  except AttributeError:
672  use_registry = False
673 
674  if use_registry:
675  if self.config.detectorId is None:
676  detectorId = next(camera.getIdIter())
677  else:
678  detectorId = self.config.detectorId
679  wcsDetector = camera[detectorId]
680 
681  # Ask the raw formatter to create the relevant WCS
682  # This allows flips to be taken into account
683  instrument = self.getInstrument(exposure.instrument)
684  rawFormatter = instrument.getRawFormatter({"detector": detectorId})
685  wcs = rawFormatter.makeRawSkyWcsFromBoresight(radec, orientation, wcsDetector)
686 
687  else:
688  if self.config.detectorId is None:
689  wcsRefsIter = self.butler.registry.queryDatasets("raw.wcs", dataId=exposure.dataId,
690  collections=collections)
691  if not wcsRefsIter:
692  raise LookupError(f"No raw.wcs datasets found for data ID {exposure.dataId} "
693  f"in collections {collections}.")
694  wcsRef = next(iter(wcsRefsIter))
695  wcsDetector = camera[wcsRef.dataId["detector"]]
696  wcs = self.butler.getDirect(wcsRef)
697  else:
698  wcsDetector = camera[self.config.detectorId]
699  wcs = self.butler.get("raw.wcs", dataId=exposure.dataId, detector=self.config.detectorId,
700  collections=collections)
701  fpToSky = wcsDetector.getTransform(FOCAL_PLANE, PIXELS).then(wcs.getTransform())
702  bounds = {}
703  for detector in camera:
704  pixelsToSky = detector.getTransform(PIXELS, FOCAL_PLANE).then(fpToSky)
705  pixCorners = Box2D(detector.getBBox().dilatedBy(self.config.padding)).getCorners()
706  bounds[detector.getId()] = [
707  skyCorner.getVector() for skyCorner in pixelsToSky.applyForward(pixCorners)
708  ]
709  return bounds
710 
711  def compute(self, visit: VisitDefinitionData, *, collections: Any = None
712  ) -> Tuple[Region, Dict[int, Region]]:
713  # Docstring inherited from ComputeVisitRegionsTask.
714  if self.config.mergeExposures:
715  detectorBounds = defaultdict(list)
716  for exposure in visit.exposures:
717  exposureDetectorBounds = self.computeExposureBounds(exposure, collections=collections)
718  for detectorId, bounds in exposureDetectorBounds.items():
719  detectorBounds[detectorId].extend(bounds)
720  else:
721  detectorBounds = self.computeExposureBounds(visit.exposures[0], collections=collections)
722  visitBounds = []
723  detectorRegions = {}
724  for detectorId, bounds in detectorBounds.items():
725  detectorRegions[detectorId] = ConvexPolygon.convexHull(bounds)
726  visitBounds.extend(bounds)
727  return ConvexPolygon.convexHull(visitBounds), detectorRegions
lsst.obs.base.defineVisits.ComputeVisitRegionsConfig
Definition: defineVisits.py:169
lsst.obs.base._instrument.loadCamera
Tuple[Camera, bool] loadCamera(Butler butler, DataId dataId, *Any collections=None)
Definition: _instrument.py:561
lsst.obs.base.defineVisits.DefineVisitsTask._buildVisitRecords
_VisitRecords _buildVisitRecords(self, VisitDefinitionData definition, *Any collections=None)
Definition: defineVisits.py:329
lsst.obs.base.defineVisits._ComputeVisitRegionsFromSingleRawWcsConfig
Definition: defineVisits.py:593
lsst.obs.base.defineVisits._GroupExposuresByGroupMetadataTask
Definition: defineVisits.py:562
lsst.obs.base.defineVisits.DefineVisitsTask.butler
butler
Definition: defineVisits.py:320
lsst.obs.base.defineVisits.ComputeVisitRegionsTask.instrumentMap
instrumentMap
Definition: defineVisits.py:199
lsst.obs.base.defineVisits.DefineVisitsTask.universe
universe
Definition: defineVisits.py:321
lsst.obs.base.defineVisits.ComputeVisitRegionsTask
Definition: defineVisits.py:179
lsst.obs.base.defineVisits.ComputeVisitRegionsTask.compute
Tuple[Region, Dict[int, Region]] compute(self, VisitDefinitionData visit, *Any collections=None)
Definition: defineVisits.py:236
lsst.obs.base.defineVisits._ComputeVisitRegionsFromSingleRawWcsTask
Definition: defineVisits.py:621
lsst.obs.base.defineVisits.ComputeVisitRegionsTask.getInstrument
Instrument getInstrument(self, instrumentName)
Definition: defineVisits.py:211
lsst.obs.base.defineVisits.DefineVisitsTask.__init__
def __init__(self, Optional[DefineVisitsConfig] config=None, *Butler butler, **Any kwargs)
Definition: defineVisits.py:317
lsst.obs.base.defineVisits._GroupExposuresByGroupMetadataTask.getVisitSystem
Tuple[int, str] getVisitSystem(self)
Definition: defineVisits.py:588
lsst.obs.base.defineVisits.GroupExposuresTask.__init__
def __init__(self, GroupExposuresConfig config, **Any kwargs)
Definition: defineVisits.py:123
lsst.obs.base.defineVisits._GroupExposuresOneToOneTask.group
Iterable[VisitDefinitionData] group(self, List[DimensionRecord] exposures)
Definition: defineVisits.py:531
lsst.obs.base.defineVisits._GroupExposuresByGroupMetadataTask.group
Iterable[VisitDefinitionData] group(self, List[DimensionRecord] exposures)
Definition: defineVisits.py:575
lsst.obs.base.defineVisits._GroupExposuresByGroupMetadataConfig
Definition: defineVisits.py:546
lsst.obs.base.defineVisits.GroupExposuresConfig
Definition: defineVisits.py:102
lsst.obs.base.defineVisits.ComputeVisitRegionsTask.__init__
def __init__(self, ComputeVisitRegionsConfig config, *Butler butler, **Any kwargs)
Definition: defineVisits.py:196
lsst.obs.base.defineVisits._ComputeVisitRegionsFromSingleRawWcsTask.computeExposureBounds
Dict[int, List[UnitVector3d]] computeExposureBounds(self, DimensionRecord exposure, *Any collections=None)
Definition: defineVisits.py:638
lsst.obs.base.defineVisits.GroupExposuresTask.group
Iterable[VisitDefinitionData] group(self, List[DimensionRecord] exposures)
Definition: defineVisits.py:136
lsst.obs.base.defineVisits._ComputeVisitRegionsFromSingleRawWcsTask.compute
Tuple[Region, Dict[int, Region]] compute(self, VisitDefinitionData visit, *Any collections=None)
Definition: defineVisits.py:711
lsst.obs.base.defineVisits.ComputeVisitRegionsTask.butler
butler
Definition: defineVisits.py:198
lsst.obs.base.defineVisits.DefineVisitsTask
Definition: defineVisits.py:280
lsst.obs.base.defineVisits.GroupExposuresTask
Definition: defineVisits.py:106
lsst.obs.base.defineVisits.VisitDefinitionData
Definition: defineVisits.py:57
lsst.obs.base.defineVisits.GroupExposuresTask.getVisitSystem
Tuple[int, str] getVisitSystem(self)
Definition: defineVisits.py:154
lsst.obs.base.defineVisits._GroupExposuresOneToOneTask
Definition: defineVisits.py:524
lsst.obs.base.defineVisits._VisitRecords
Definition: defineVisits.py:84
lsst.obs.base.defineVisits.DefineVisitsConfig
Definition: defineVisits.py:261
lsst.obs.base.defineVisits.DefineVisitsTask.run
def run(self, Iterable[DataId] dataIds, *Optional[Pool] pool=None, int processes=1, Optional[str] collections=None)
Definition: defineVisits.py:410
lsst.obs.base.defineVisits._GroupExposuresOneToOneConfig
Definition: defineVisits.py:508
lsst.obs.base.defineVisits._GroupExposuresOneToOneTask.getVisitSystem
Tuple[int, str] getVisitSystem(self)
Definition: defineVisits.py:541