lsst.meas.base  21.0.0-14-g3bd782b+3c43d270e9
forcedPhotCcd.py
Go to the documentation of this file.
1 # This file is part of meas_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 <https://www.gnu.org/licenses/>.
21 
22 import collections
23 
24 import lsst.pex.config
26 from lsst.log import Log
27 import lsst.pipe.base
28 import lsst.geom
29 import lsst.afw.geom
30 import lsst.afw.image
31 import lsst.afw.table
32 import lsst.sphgeom
33 
34 from lsst.pipe.base import PipelineTaskConnections
35 import lsst.pipe.base.connectionTypes as cT
36 
37 import lsst.pipe.base as pipeBase
38 from lsst.skymap import BaseSkyMap
39 
40 from .references import MultiBandReferencesTask
41 from .forcedMeasurement import ForcedMeasurementTask
42 from .applyApCorr import ApplyApCorrTask
43 from .catalogCalculation import CatalogCalculationTask
44 
45 try:
46  from lsst.meas.mosaic import applyMosaicResults
47 except ImportError:
48  applyMosaicResults = None
49 
50 __all__ = ("PerTractCcdDataIdContainer", "ForcedPhotCcdConfig", "ForcedPhotCcdTask", "imageOverlapsTract")
51 
52 
53 class PerTractCcdDataIdContainer(pipeBase.DataIdContainer):
54  """A data ID container which combines raw data IDs with a tract.
55 
56  Notes
57  -----
58  Required because we need to add "tract" to the raw data ID keys (defined as
59  whatever we use for ``src``) when no tract is provided (so that the user is
60  not required to know which tracts are spanned by the raw data ID).
61 
62  This subclass of `~lsst.pipe.base.DataIdContainer` assumes that a calexp is
63  being measured using the detection information, a set of reference
64  catalogs, from the set of coadds which intersect with the calexp. It needs
65  the calexp id (e.g. visit, raft, sensor), but is also uses the tract to
66  decide what set of coadds to use. The references from the tract whose
67  patches intersect with the calexp are used.
68  """
69 
70  def makeDataRefList(self, namespace):
71  """Make self.refList from self.idList
72  """
73  if self.datasetType is None:
74  raise RuntimeError("Must call setDatasetType first")
75  log = Log.getLogger("meas.base.forcedPhotCcd.PerTractCcdDataIdContainer")
76  skymap = None
77  visitTract = collections.defaultdict(set) # Set of tracts for each visit
78  visitRefs = collections.defaultdict(list) # List of data references for each visit
79  for dataId in self.idList:
80  if "tract" not in dataId:
81  # Discover which tracts the data overlaps
82  log.info("Reading WCS for components of dataId=%s to determine tracts", dict(dataId))
83  if skymap is None:
84  skymap = namespace.butler.get(namespace.config.coaddName + "Coadd_skyMap")
85 
86  for ref in namespace.butler.subset("calexp", dataId=dataId):
87  if not ref.datasetExists("calexp"):
88  continue
89 
90  visit = ref.dataId["visit"]
91  visitRefs[visit].append(ref)
92 
93  md = ref.get("calexp_md", immediate=True)
94  wcs = lsst.afw.geom.makeSkyWcs(md)
96  # Going with just the nearest tract. Since we're throwing all tracts for the visit
97  # together, this shouldn't be a problem unless the tracts are much smaller than a CCD.
98  tract = skymap.findTract(wcs.pixelToSky(box.getCenter()))
99  if imageOverlapsTract(tract, wcs, box):
100  visitTract[visit].add(tract.getId())
101  else:
102  self.refList.extend(ref for ref in namespace.butler.subset(self.datasetType, dataId=dataId))
103 
104  # Ensure all components of a visit are kept together by putting them all in the same set of tracts
105  for visit, tractSet in visitTract.items():
106  for ref in visitRefs[visit]:
107  for tract in tractSet:
108  self.refList.append(namespace.butler.dataRef(datasetType=self.datasetType,
109  dataId=ref.dataId, tract=tract))
110  if visitTract:
111  tractCounter = collections.Counter()
112  for tractSet in visitTract.values():
113  tractCounter.update(tractSet)
114  log.info("Number of visits for each tract: %s", dict(tractCounter))
115 
116 
117 def imageOverlapsTract(tract, imageWcs, imageBox):
118  """Return whether the given bounding box overlaps the tract given a WCS.
119 
120  Parameters
121  ----------
122  tract : `lsst.skymap.TractInfo`
123  TractInfo specifying a tract.
124  imageWcs : `lsst.afw.geom.SkyWcs`
125  World coordinate system for the image.
126  imageBox : `lsst.geom.Box2I`
127  Bounding box for the image.
128 
129  Returns
130  -------
131  overlap : `bool`
132  `True` if the bounding box overlaps the tract; `False` otherwise.
133  """
134  tractPoly = tract.getOuterSkyPolygon()
135 
136  imagePixelCorners = lsst.geom.Box2D(imageBox).getCorners()
137  try:
138  imageSkyCorners = imageWcs.pixelToSky(imagePixelCorners)
139  except lsst.pex.exceptions.LsstCppException as e:
140  # Protecting ourselves from awful Wcs solutions in input images
141  if (not isinstance(e.message, lsst.pex.exceptions.DomainErrorException)
142  and not isinstance(e.message, lsst.pex.exceptions.RuntimeErrorException)):
143  raise
144  return False
145 
146  imagePoly = lsst.sphgeom.ConvexPolygon.convexHull([coord.getVector() for coord in imageSkyCorners])
147  return tractPoly.intersects(imagePoly) # "intersects" also covers "contains" or "is contained by"
148 
149 
150 class ForcedPhotCcdConnections(PipelineTaskConnections,
151  dimensions=("instrument", "visit", "detector", "skymap", "tract"),
152  defaultTemplates={"inputCoaddName": "deep",
153  "inputName": "calexp"}):
154  inputSchema = cT.InitInput(
155  doc="Schema for the input measurement catalogs.",
156  name="{inputCoaddName}Coadd_ref_schema",
157  storageClass="SourceCatalog",
158  )
159  outputSchema = cT.InitOutput(
160  doc="Schema for the output forced measurement catalogs.",
161  name="forced_src_schema",
162  storageClass="SourceCatalog",
163  )
164  exposure = cT.Input(
165  doc="Input exposure to perform photometry on.",
166  name="{inputName}",
167  storageClass="ExposureF",
168  dimensions=["instrument", "visit", "detector"],
169  )
170  refCat = cT.Input(
171  doc="Catalog of shapes and positions at which to force photometry.",
172  name="{inputCoaddName}Coadd_ref",
173  storageClass="SourceCatalog",
174  dimensions=["skymap", "tract", "patch"],
175  multiple=True,
176  deferLoad=True,
177  )
178  skyMap = cT.Input(
179  doc="SkyMap dataset that defines the coordinate system of the reference catalog.",
180  name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
181  storageClass="SkyMap",
182  dimensions=["skymap"],
183  )
184  measCat = cT.Output(
185  doc="Output forced photometry catalog.",
186  name="forced_src",
187  storageClass="SourceCatalog",
188  dimensions=["instrument", "visit", "detector", "skymap", "tract"],
189  )
190 
191 
192 class ForcedPhotCcdConfig(pipeBase.PipelineTaskConfig,
193  pipelineConnections=ForcedPhotCcdConnections):
194  """Config class for forced measurement driver task."""
195  references = lsst.pex.config.ConfigurableField(
196  target=MultiBandReferencesTask,
197  doc="subtask to retrieve reference source catalog"
198  )
199  measurement = lsst.pex.config.ConfigurableField(
200  target=ForcedMeasurementTask,
201  doc="subtask to do forced measurement"
202  )
203  coaddName = lsst.pex.config.Field(
204  doc="coadd name: typically one of deep or goodSeeing",
205  dtype=str,
206  default="deep",
207  )
208  doApCorr = lsst.pex.config.Field(
209  dtype=bool,
210  default=True,
211  doc="Run subtask to apply aperture corrections"
212  )
213  applyApCorr = lsst.pex.config.ConfigurableField(
214  target=ApplyApCorrTask,
215  doc="Subtask to apply aperture corrections"
216  )
217  catalogCalculation = lsst.pex.config.ConfigurableField(
218  target=CatalogCalculationTask,
219  doc="Subtask to run catalogCalculation plugins on catalog"
220  )
221  doApplyUberCal = lsst.pex.config.Field(
222  dtype=bool,
223  doc="Apply meas_mosaic ubercal results to input calexps?",
224  default=False,
225  deprecated="Deprecated by DM-23352; use doApplyExternalPhotoCalib and doApplyExternalSkyWcs instead",
226  )
227  doApplyExternalPhotoCalib = lsst.pex.config.Field(
228  dtype=bool,
229  default=False,
230  doc=("Whether to apply external photometric calibration via an "
231  "`lsst.afw.image.PhotoCalib` object. Uses the "
232  "``externalPhotoCalibName`` field to determine which calibration "
233  "to load."),
234  )
235  doApplyExternalSkyWcs = lsst.pex.config.Field(
236  dtype=bool,
237  default=False,
238  doc=("Whether to apply external astrometric calibration via an "
239  "`lsst.afw.geom.SkyWcs` object. Uses ``externalSkyWcsName`` "
240  "field to determine which calibration to load."),
241  )
242  doApplySkyCorr = lsst.pex.config.Field(
243  dtype=bool,
244  default=False,
245  doc="Apply sky correction?",
246  )
247  includePhotoCalibVar = lsst.pex.config.Field(
248  dtype=bool,
249  default=False,
250  doc="Add photometric calibration variance to warp variance plane?",
251  )
252  externalPhotoCalibName = lsst.pex.config.ChoiceField(
253  dtype=str,
254  doc=("Type of external PhotoCalib if ``doApplyExternalPhotoCalib`` is True. "
255  "Unused for Gen3 middleware."),
256  default="jointcal",
257  allowed={
258  "jointcal": "Use jointcal_photoCalib",
259  "fgcm": "Use fgcm_photoCalib",
260  "fgcm_tract": "Use fgcm_tract_photoCalib"
261  },
262  )
263  externalSkyWcsName = lsst.pex.config.ChoiceField(
264  dtype=str,
265  doc="Type of external SkyWcs if ``doApplyExternalSkyWcs`` is True. Unused for Gen3 middleware.",
266  default="jointcal",
267  allowed={
268  "jointcal": "Use jointcal_wcs"
269  },
270  )
271 
272  def setDefaults(self):
273  # Docstring inherited.
274  # Make catalogCalculation a no-op by default as no modelFlux is setup by default in
275  # ForcedMeasurementTask
276  super().setDefaults()
277 
278  self.catalogCalculation.plugins.names = []
279 
280 
281 class ForcedPhotCcdTask(pipeBase.PipelineTask, pipeBase.CmdLineTask):
282  """A command-line driver for performing forced measurement on CCD images.
283 
284  Parameters
285  ----------
286  butler : `lsst.daf.persistence.butler.Butler`, optional
287  A Butler which will be passed to the references subtask to allow it to
288  load its schema from disk. Optional, but must be specified if
289  ``refSchema`` is not; if both are specified, ``refSchema`` takes
290  precedence.
291  refSchema : `lsst.afw.table.Schema`, optional
292  The schema of the reference catalog, passed to the constructor of the
293  references subtask. Optional, but must be specified if ``butler`` is
294  not; if both are specified, ``refSchema`` takes precedence.
295  **kwds
296  Keyword arguments are passed to the supertask constructor.
297 
298  Notes
299  -----
300  The `runDataRef` method takes a `~lsst.daf.persistence.ButlerDataRef` argument
301  that corresponds to a single CCD. This should contain the data ID keys that
302  correspond to the ``forced_src`` dataset (the output dataset for this
303  task), which are typically all those used to specify the ``calexp`` dataset
304  (``visit``, ``raft``, ``sensor`` for LSST data) as well as a coadd tract.
305  The tract is used to look up the appropriate coadd measurement catalogs to
306  use as references (e.g. ``deepCoadd_src``; see
307  :lsst-task:`lsst.meas.base.references.CoaddSrcReferencesTask` for more
308  information). While the tract must be given as part of the dataRef, the
309  patches are determined automatically from the bounding box and WCS of the
310  calexp to be measured, and the filter used to fetch references is set via
311  the ``filter`` option in the configuration of
312  :lsst-task:`lsst.meas.base.references.BaseReferencesTask`).
313  """
314 
315  ConfigClass = ForcedPhotCcdConfig
316  RunnerClass = pipeBase.ButlerInitializedTaskRunner
317  _DefaultName = "forcedPhotCcd"
318  dataPrefix = ""
319 
320  def __init__(self, butler=None, refSchema=None, initInputs=None, **kwds):
321  super().__init__(**kwds)
322 
323  if initInputs is not None:
324  refSchema = initInputs['inputSchema'].schema
325 
326  self.makeSubtask("references", butler=butler, schema=refSchema)
327  if refSchema is None:
328  refSchema = self.references.schema
329  self.makeSubtask("measurement", refSchema=refSchema)
330  # It is necessary to get the schema internal to the forced measurement task until such a time
331  # that the schema is not owned by the measurement task, but is passed in by an external caller
332  if self.config.doApCorr:
333  self.makeSubtask("applyApCorr", schema=self.measurement.schema)
334  self.makeSubtask('catalogCalculation', schema=self.measurement.schema)
335  self.outputSchema = lsst.afw.table.SourceCatalog(self.measurement.schema)
336 
337  def runQuantum(self, butlerQC, inputRefs, outputRefs):
338  inputs = butlerQC.get(inputRefs)
339 
340  tract = butlerQC.quantum.dataId['tract']
341  skyMap = inputs.pop("skyMap")
342  inputs['refWcs'] = skyMap[tract].getWcs()
343 
344  inputs['refCat'] = self.mergeAndFilterReferences(inputs['exposure'], inputs['refCat'],
345  inputs['refWcs'])
346 
347  inputs['measCat'], inputs['exposureId'] = self.generateMeasCat(inputRefs.exposure.dataId,
348  inputs['exposure'],
349  inputs['refCat'], inputs['refWcs'],
350  "visit_detector")
351  self.attachFootprints(inputs['measCat'], inputs['refCat'], inputs['exposure'], inputs['refWcs'])
352  # TODO: apply external calibrations (DM-17062)
353  outputs = self.run(**inputs)
354  butlerQC.put(outputs, outputRefs)
355 
356  def mergeAndFilterReferences(self, exposure, refCats, refWcs):
357  """Filter reference catalog so that all sources are within the
358  boundaries of the exposure.
359 
360  Parameters
361  ----------
362  exposure : `lsst.afw.image.exposure.Exposure`
363  Exposure to generate the catalog for.
364  refCats : sequence of `lsst.daf.butler.DeferredDatasetHandle`
365  Handles for catalogs of shapes and positions at which to force
366  photometry.
367  refWcs : `lsst.afw.image.SkyWcs`
368  Reference world coordinate system.
369 
370  Returns
371  -------
372  refSources : `lsst.afw.table.SourceCatalog`
373  Filtered catalog of forced sources to measure.
374 
375  Notes
376  -----
377  Filtering the reference catalog is currently handled by Gen2
378  specific methods. To function for Gen3, this method copies
379  code segments to do the filtering and transformation. The
380  majority of this code is based on the methods of
381  lsst.meas.algorithms.loadReferenceObjects.ReferenceObjectLoader
382 
383  """
384 
385  # Step 1: Determine bounds of the exposure photometry will
386  # be performed on.
387  expWcs = exposure.getWcs()
388  expRegion = exposure.getBBox(lsst.afw.image.PARENT)
389  expBBox = lsst.geom.Box2D(expRegion)
390  expBoxCorners = expBBox.getCorners()
391  expSkyCorners = [expWcs.pixelToSky(corner).getVector() for
392  corner in expBoxCorners]
393  expPolygon = lsst.sphgeom.ConvexPolygon(expSkyCorners)
394 
395  # Step 2: Filter out reference catalog sources that are
396  # not contained within the exposure boundaries, or whose
397  # parents are not within the exposure boundaries. Note
398  # that within a single input refCat, the parents always
399  # appear before the children.
400  mergedRefCat = None
401  for refCat in refCats:
402  refCat = refCat.get()
403  if mergedRefCat is None:
404  mergedRefCat = lsst.afw.table.SourceCatalog(refCat.table)
405  containedIds = {0} # zero as a parent ID means "this is a parent"
406  for record in refCat:
407  if expPolygon.contains(record.getCoord().getVector()) and record.getParent() in containedIds:
408  record.setFootprint(record.getFootprint().transform(refWcs, expWcs, expRegion))
409  mergedRefCat.append(record)
410  containedIds.add(record.getId())
411  if mergedRefCat is None:
412  raise RuntimeError("No reference objects for forced photometry.")
413  mergedRefCat.sort(lsst.afw.table.SourceTable.getParentKey())
414  return mergedRefCat
415 
416  def generateMeasCat(self, exposureDataId, exposure, refCat, refWcs, idPackerName):
417  """Generate a measurement catalog for Gen3.
418 
419  Parameters
420  ----------
421  exposureDataId : `DataId`
422  Butler dataId for this exposure.
423  exposure : `lsst.afw.image.exposure.Exposure`
424  Exposure to generate the catalog for.
425  refCat : `lsst.afw.table.SourceCatalog`
426  Catalog of shapes and positions at which to force photometry.
427  refWcs : `lsst.afw.image.SkyWcs`
428  Reference world coordinate system.
429  idPackerName : `str`
430  Type of ID packer to construct from the registry.
431 
432  Returns
433  -------
434  measCat : `lsst.afw.table.SourceCatalog`
435  Catalog of forced sources to measure.
436  expId : `int`
437  Unique binary id associated with the input exposure
438  """
439  expId, expBits = exposureDataId.pack(idPackerName, returnMaxBits=True)
440  idFactory = lsst.afw.table.IdFactory.makeSource(expId, 64 - expBits)
441 
442  measCat = self.measurement.generateMeasCat(exposure, refCat, refWcs,
443  idFactory=idFactory)
444  return measCat, expId
445 
446  def runDataRef(self, dataRef, psfCache=None):
447  """Perform forced measurement on a single exposure.
448 
449  Parameters
450  ----------
451  dataRef : `lsst.daf.persistence.ButlerDataRef`
452  Passed to the ``references`` subtask to obtain the reference WCS,
453  the ``getExposure`` method (implemented by derived classes) to
454  read the measurment image, and the ``fetchReferences`` method to
455  get the exposure and load the reference catalog (see
456  :lsst-task`lsst.meas.base.references.CoaddSrcReferencesTask`).
457  Refer to derived class documentation for details of the datasets
458  and data ID keys which are used.
459  psfCache : `int`, optional
460  Size of PSF cache, or `None`. The size of the PSF cache can have
461  a significant effect upon the runtime for complicated PSF models.
462 
463  Notes
464  -----
465  Sources are generated with ``generateMeasCat`` in the ``measurement``
466  subtask. These are passed to ``measurement``'s ``run`` method, which
467  fills the source catalog with the forced measurement results. The
468  sources are then passed to the ``writeOutputs`` method (implemented by
469  derived classes) which writes the outputs.
470  """
471  refWcs = self.references.getWcs(dataRef)
472  exposure = self.getExposure(dataRef)
473  if psfCache is not None:
474  exposure.getPsf().setCacheSize(psfCache)
475  refCat = self.fetchReferences(dataRef, exposure)
476 
477  measCat = self.measurement.generateMeasCat(exposure, refCat, refWcs,
478  idFactory=self.makeIdFactory(dataRef))
479  self.log.info("Performing forced measurement on %s" % (dataRef.dataId,))
480  self.attachFootprints(measCat, refCat, exposure, refWcs)
481 
482  exposureId = self.getExposureId(dataRef)
483 
484  forcedPhotResult = self.run(measCat, exposure, refCat, refWcs, exposureId=exposureId)
485 
486  self.writeOutput(dataRef, forcedPhotResult.measCat)
487 
488  def run(self, measCat, exposure, refCat, refWcs, exposureId=None):
489  """Perform forced measurement on a single exposure.
490 
491  Parameters
492  ----------
493  measCat : `lsst.afw.table.SourceCatalog`
494  The measurement catalog, based on the sources listed in the
495  reference catalog.
496  exposure : `lsst.afw.image.Exposure`
497  The measurement image upon which to perform forced detection.
498  refCat : `lsst.afw.table.SourceCatalog`
499  The reference catalog of sources to measure.
500  refWcs : `lsst.afw.image.SkyWcs`
501  The WCS for the references.
502  exposureId : `int`
503  Optional unique exposureId used for random seed in measurement
504  task.
505 
506  Returns
507  -------
508  result : `lsst.pipe.base.Struct`
509  Structure with fields:
510 
511  ``measCat``
512  Catalog of forced measurement results
513  (`lsst.afw.table.SourceCatalog`).
514  """
515  self.measurement.run(measCat, exposure, refCat, refWcs, exposureId=exposureId)
516  if self.config.doApCorr:
517  self.applyApCorr.run(
518  catalog=measCat,
519  apCorrMap=exposure.getInfo().getApCorrMap()
520  )
521  self.catalogCalculation.run(measCat)
522 
523  return pipeBase.Struct(measCat=measCat)
524 
525  def makeIdFactory(self, dataRef):
526  """Create an object that generates globally unique source IDs.
527 
528  Source IDs are created based on a per-CCD ID and the ID of the CCD
529  itself.
530 
531  Parameters
532  ----------
533  dataRef : `lsst.daf.persistence.ButlerDataRef`
534  Butler data reference. The ``ccdExposureId_bits`` and
535  ``ccdExposureId`` datasets are accessed. The data ID must have the
536  keys that correspond to ``ccdExposureId``, which are generally the
537  same as those that correspond to ``calexp`` (``visit``, ``raft``,
538  ``sensor`` for LSST data).
539  """
540  expBits = dataRef.get("ccdExposureId_bits")
541  expId = int(dataRef.get("ccdExposureId"))
542  return lsst.afw.table.IdFactory.makeSource(expId, 64 - expBits)
543 
544  def getExposureId(self, dataRef):
545  return int(dataRef.get("ccdExposureId", immediate=True))
546 
547  def fetchReferences(self, dataRef, exposure):
548  """Get sources that overlap the exposure.
549 
550  Parameters
551  ----------
552  dataRef : `lsst.daf.persistence.ButlerDataRef`
553  Butler data reference corresponding to the image to be measured;
554  should have ``tract``, ``patch``, and ``filter`` keys.
555  exposure : `lsst.afw.image.Exposure`
556  The image to be measured (used only to obtain a WCS and bounding
557  box).
558 
559  Returns
560  -------
561  referencs : `lsst.afw.table.SourceCatalog`
562  Catalog of sources that overlap the exposure
563 
564  Notes
565  -----
566  The returned catalog is sorted by ID and guarantees that all included
567  children have their parent included and that all Footprints are valid.
568 
569  All work is delegated to the references subtask; see
570  :lsst-task:`lsst.meas.base.references.CoaddSrcReferencesTask`
571  for information about the default behavior.
572  """
573  references = lsst.afw.table.SourceCatalog(self.references.schema)
574  badParents = set()
575  unfiltered = self.references.fetchInBox(dataRef, exposure.getBBox(), exposure.getWcs())
576  for record in unfiltered:
577  if record.getFootprint() is None or record.getFootprint().getArea() == 0:
578  if record.getParent() != 0:
579  self.log.warn("Skipping reference %s (child of %s) with bad Footprint",
580  record.getId(), record.getParent())
581  else:
582  self.log.warn("Skipping reference parent %s with bad Footprint", record.getId())
583  badParents.add(record.getId())
584  elif record.getParent() not in badParents:
585  references.append(record)
586  # catalog must be sorted by parent ID for lsst.afw.table.getChildren to work
587  references.sort(lsst.afw.table.SourceTable.getParentKey())
588  return references
589 
590  def attachFootprints(self, sources, refCat, exposure, refWcs):
591  r"""Attach footprints to blank sources prior to measurements.
592 
593  Notes
594  -----
595  `~lsst.afw.detection.Footprint`\ s for forced photometry must be in the
596  pixel coordinate system of the image being measured, while the actual
597  detections may start out in a different coordinate system.
598 
599  Subclasses of this class must implement this method to define how
600  those `~lsst.afw.detection.Footprint`\ s should be generated.
601 
602  This default implementation transforms the
603  `~lsst.afw.detection.Footprint`\ s from the reference catalog from the
604  reference WCS to the exposure's WcS, which downgrades
605  `lsst.afw.detection.heavyFootprint.HeavyFootprint`\ s into regular
606  `~lsst.afw.detection.Footprint`\ s, destroying deblend information.
607  """
608  return self.measurement.attachTransformedFootprints(sources, refCat, exposure, refWcs)
609 
610  def getExposure(self, dataRef):
611  """Read input exposure for measurement.
612 
613  Parameters
614  ----------
615  dataRef : `lsst.daf.persistence.ButlerDataRef`
616  Butler data reference.
617  """
618  exposure = dataRef.get(self.dataPrefix + "calexp", immediate=True)
619 
620  if self.config.doApplyExternalPhotoCalib:
621  source = f"{self.config.externalPhotoCalibName}_photoCalib"
622  self.log.info("Applying external photoCalib from %s", source)
623  photoCalib = dataRef.get(source)
624  exposure.setPhotoCalib(photoCalib) # No need for calibrateImage; having the photoCalib suffices
625 
626  if self.config.doApplyExternalSkyWcs:
627  source = f"{self.config.externalSkyWcsName}_wcs"
628  self.log.info("Applying external skyWcs from %s", source)
629  skyWcs = dataRef.get(source)
630  exposure.setWcs(skyWcs)
631 
632  if self.config.doApplySkyCorr:
633  self.log.info("Apply sky correction")
634  skyCorr = dataRef.get("skyCorr")
635  exposure.maskedImage -= skyCorr.getImage()
636 
637  return exposure
638 
639  def writeOutput(self, dataRef, sources):
640  """Write forced source table
641 
642  Parameters
643  ----------
644  dataRef : `lsst.daf.persistence.ButlerDataRef`
645  Butler data reference. The forced_src dataset (with
646  self.dataPrefix prepended) is all that will be modified.
647  sources : `lsst.afw.table.SourceCatalog`
648  Catalog of sources to save.
649  """
650  dataRef.put(sources, self.dataPrefix + "forced_src", flags=lsst.afw.table.SOURCE_IO_NO_FOOTPRINTS)
651 
652  def getSchemaCatalogs(self):
653  """The schema catalogs that will be used by this task.
654 
655  Returns
656  -------
657  schemaCatalogs : `dict`
658  Dictionary mapping dataset type to schema catalog.
659 
660  Notes
661  -----
662  There is only one schema for each type of forced measurement. The
663  dataset type for this measurement is defined in the mapper.
664  """
665  catalog = lsst.afw.table.SourceCatalog(self.measurement.schema)
666  catalog.getTable().setMetadata(self.measurement.algMetadata)
667  datasetType = self.dataPrefix + "forced_src"
668  return {datasetType: catalog}
669 
670  def _getConfigName(self):
671  # Documented in superclass.
672  return self.dataPrefix + "forcedPhotCcd_config"
673 
674  def _getMetadataName(self):
675  # Documented in superclass
676  return self.dataPrefix + "forcedPhotCcd_metadata"
677 
678  @classmethod
679  def _makeArgumentParser(cls):
680  parser = pipeBase.ArgumentParser(name=cls._DefaultName)
681  parser.add_id_argument("--id", "forced_src", help="data ID with raw CCD keys [+ tract optionally], "
682  "e.g. --id visit=12345 ccd=1,2 [tract=0]",
683  ContainerClass=PerTractCcdDataIdContainer)
684  return parser
static std::shared_ptr< IdFactory > makeSource(RecordId expId, int reserved)
static Key< RecordId > getParentKey()
static ConvexPolygon convexHull(std::vector< UnitVector3d > const &points)
std::shared_ptr< SkyWcs > makeSkyWcs(daf::base::PropertySet &metadata, bool strip=false)
lsst::geom::Box2I bboxFromMetadata(daf::base::PropertySet &metadata)
def imageOverlapsTract(tract, imageWcs, imageBox)
def attachFootprints(self, sources, refCat, exposure, refWcs, dataRef)
def writeOutput(self, dataRef, sources)
def fetchReferences(self, dataRef, exposure)