LSSTApplications  18.1.0
LSSTDataManagementBasePackage
calibrate.py
Go to the documentation of this file.
1 #
2 # LSST Data Management System
3 # Copyright 2008-2016 AURA/LSST.
4 #
5 # This product includes software developed by the
6 # LSST Project (http://www.lsst.org/).
7 #
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation, either version 3 of the License, or
11 # (at your option) any later version.
12 #
13 # This program is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
17 #
18 # You should have received a copy of the LSST License Statement and
19 # the GNU General Public License along with this program. If not,
20 # see <https://www.lsstcorp.org/LegalNotices/>.
21 #
22 import math
23 
24 from lsstDebug import getDebugFrame
25 import lsst.pex.config as pexConfig
26 import lsst.pipe.base as pipeBase
27 from lsst.pipe.base import (InitInputDatasetField, InitOutputDatasetField, InputDatasetField,
28  OutputDatasetField, PipelineTaskConfig, PipelineTask)
29 import lsst.afw.table as afwTable
30 from lsst.meas.astrom import AstrometryTask, displayAstrometry, denormalizeMatches
31 from lsst.meas.algorithms import LoadIndexedReferenceObjectsTask
32 from lsst.obs.base import ExposureIdInfo
33 import lsst.daf.base as dafBase
34 from lsst.afw.math import BackgroundList
35 from lsst.afw.table import IdFactory, SourceTable
36 from lsst.meas.algorithms import SourceDetectionTask, ReferenceObjectLoader
37 from lsst.meas.base import (SingleFrameMeasurementTask, ApplyApCorrTask,
38  CatalogCalculationTask)
39 from lsst.meas.deblender import SourceDeblendTask
40 from .fakes import BaseFakeSourcesTask
41 from .photoCal import PhotoCalTask
42 
43 __all__ = ["CalibrateConfig", "CalibrateTask"]
44 
45 
46 class CalibrateConfig(PipelineTaskConfig):
47  """Config for CalibrateTask"""
48  doWrite = pexConfig.Field(
49  dtype=bool,
50  default=True,
51  doc="Save calibration results?",
52  )
53  doWriteHeavyFootprintsInSources = pexConfig.Field(
54  dtype=bool,
55  default=True,
56  doc="Include HeavyFootprint data in source table? If false then heavy "
57  "footprints are saved as normal footprints, which saves some space"
58  )
59  doWriteMatches = pexConfig.Field(
60  dtype=bool,
61  default=True,
62  doc="Write reference matches (ignored if doWrite false)?",
63  )
64  doWriteMatchesDenormalized = pexConfig.Field(
65  dtype=bool,
66  default=False,
67  doc=("Write reference matches in denormalized format? "
68  "This format uses more disk space, but is more convenient to "
69  "read. Ignored if doWriteMatches=False or doWrite=False."),
70  )
71  doAstrometry = pexConfig.Field(
72  dtype=bool,
73  default=True,
74  doc="Perform astrometric calibration?",
75  )
76  astromRefObjLoader = pexConfig.ConfigurableField(
77  target=LoadIndexedReferenceObjectsTask,
78  doc="reference object loader for astrometric calibration",
79  )
80  photoRefObjLoader = pexConfig.ConfigurableField(
81  target=LoadIndexedReferenceObjectsTask,
82  doc="reference object loader for photometric calibration",
83  )
84  astrometry = pexConfig.ConfigurableField(
85  target=AstrometryTask,
86  doc="Perform astrometric calibration to refine the WCS",
87  )
88  requireAstrometry = pexConfig.Field(
89  dtype=bool,
90  default=True,
91  doc=("Raise an exception if astrometry fails? Ignored if doAstrometry "
92  "false."),
93  )
94  doPhotoCal = pexConfig.Field(
95  dtype=bool,
96  default=True,
97  doc="Perform phometric calibration?",
98  )
99  requirePhotoCal = pexConfig.Field(
100  dtype=bool,
101  default=True,
102  doc=("Raise an exception if photoCal fails? Ignored if doPhotoCal "
103  "false."),
104  )
105  photoCal = pexConfig.ConfigurableField(
106  target=PhotoCalTask,
107  doc="Perform photometric calibration",
108  )
109  icSourceFieldsToCopy = pexConfig.ListField(
110  dtype=str,
111  default=("calib_psf_candidate", "calib_psf_used", "calib_psf_reserved"),
112  doc=("Fields to copy from the icSource catalog to the output catalog "
113  "for matching sources Any missing fields will trigger a "
114  "RuntimeError exception. Ignored if icSourceCat is not provided.")
115  )
116  matchRadiusPix = pexConfig.Field(
117  dtype=float,
118  default=3,
119  doc=("Match radius for matching icSourceCat objects to sourceCat "
120  "objects (pixels)"),
121  )
122  checkUnitsParseStrict = pexConfig.Field(
123  doc=("Strictness of Astropy unit compatibility check, can be 'raise', "
124  "'warn' or 'silent'"),
125  dtype=str,
126  default="raise",
127  )
128  detection = pexConfig.ConfigurableField(
129  target=SourceDetectionTask,
130  doc="Detect sources"
131  )
132  doDeblend = pexConfig.Field(
133  dtype=bool,
134  default=True,
135  doc="Run deblender input exposure"
136  )
137  deblend = pexConfig.ConfigurableField(
138  target=SourceDeblendTask,
139  doc="Split blended sources into their components"
140  )
141  measurement = pexConfig.ConfigurableField(
142  target=SingleFrameMeasurementTask,
143  doc="Measure sources"
144  )
145  doApCorr = pexConfig.Field(
146  dtype=bool,
147  default=True,
148  doc="Run subtask to apply aperture correction"
149  )
150  applyApCorr = pexConfig.ConfigurableField(
151  target=ApplyApCorrTask,
152  doc="Subtask to apply aperture corrections"
153  )
154  # If doApCorr is False, and the exposure does not have apcorrections
155  # already applied, the active plugins in catalogCalculation almost
156  # certainly should not contain the characterization plugin
157  catalogCalculation = pexConfig.ConfigurableField(
158  target=CatalogCalculationTask,
159  doc="Subtask to run catalogCalculation plugins on catalog"
160  )
161  doInsertFakes = pexConfig.Field(
162  dtype=bool,
163  default=False,
164  doc="Run fake sources injection task"
165  )
166  insertFakes = pexConfig.ConfigurableField(
167  target=BaseFakeSourcesTask,
168  doc="Injection of fake sources for testing purposes (must be "
169  "retargeted)"
170  )
171  icSourceSchema = InitInputDatasetField(
172  doc="Schema produced by characterize image task, used to initialize this task",
173  name="icSrc_schema",
174  storageClass="SourceCatalog",
175  )
176  outputSchema = InitOutputDatasetField(
177  doc="Schema after CalibrateTask has been initialized",
178  name="src_schema",
179  storageClass="SourceCatalog",
180  )
181  exposure = InputDatasetField(
182  doc="Input image to calibrate",
183  name="icExp",
184  storageClass="ExposureF",
185  dimensions=("instrument", "visit", "detector"),
186  scalar=True
187  )
188  background = InputDatasetField(
189  doc="Backgrounds determined by characterize task",
190  name="icExpBackground",
191  storageClass="Background",
192  dimensions=("instrument", "visit", "detector"),
193  scalar=True
194  )
195  icSourceCat = InputDatasetField(
196  doc="Source catalog created by characterize task",
197  name="icSrc",
198  storageClass="SourceCatalog",
199  dimensions=("instrument", "visit", "detector"),
200  scalar=True
201  )
202  astromRefCat = InputDatasetField(
203  doc="Reference catalog to use for astrometry",
204  name="ref_cat",
205  storageClass="SimpleCatalog",
206  dimensions=("skypix",),
207  manualLoad=True
208  )
209  photoRefCat = InputDatasetField(
210  doc="Reference catalog to use for photometric calibration",
211  name="ref_cat",
212  storageClass="SimpleCatalog",
213  dimensions=("skypix",),
214  manualLoad=True
215  )
216  outputExposure = OutputDatasetField(
217  doc="Exposure after running calibration task",
218  name="calexp",
219  storageClass="ExposureF",
220  dimensions=("instrument", "visit", "detector"),
221  scalar=True
222  )
223  outputCat = OutputDatasetField(
224  doc="Source catalog produced in calibrate task",
225  name="src",
226  storageClass="SourceCatalog",
227  dimensions=("instrument", "visit", "detector"),
228  scalar=True
229  )
230  outputBackground = OutputDatasetField(
231  doc="Background models estimated in calibration task",
232  name="calexpBackground",
233  storageClass="Background",
234  dimensions=("instrument", "visit", "detector"),
235  scalar=True
236  )
238  doc="Source/refObj matches from the astrometry solver",
239  name="srcMatch",
240  storageClass="Catalog",
241  dimensions=("instrument", "visit", "detector"),
242  scalar=True
243  )
244  matchesDenormalized = OutputDatasetField(
245  doc="Denormalized matches from astrometry solver",
246  name="srcMatchFull",
247  storageClass="Catalog",
248  dimensions=("instrument", "visit", "detector"),
249  scalar=True
250  )
251 
252  doWriteExposure = pexConfig.Field(
253  dtype=bool,
254  default=True,
255  doc="Write the calexp? If fakes have been added then we do not want to write out the calexp as a "
256  "normal calexp but as a fakes_calexp."
257  )
258 
259  def setDefaults(self):
260  super().setDefaults()
261  self.quantum.dimensions = ("instrument", "visit", "detector")
262  # aperture correction should already be measured
263 
264 
265 
271 
272 class CalibrateTask(PipelineTask, pipeBase.CmdLineTask):
273  r"""!Calibrate an exposure: measure sources and perform astrometric and
274  photometric calibration
275 
276  @anchor CalibrateTask_
277 
278  @section pipe_tasks_calibrate_Contents Contents
279 
280  - @ref pipe_tasks_calibrate_Purpose
281  - @ref pipe_tasks_calibrate_Initialize
282  - @ref pipe_tasks_calibrate_IO
283  - @ref pipe_tasks_calibrate_Config
284  - @ref pipe_tasks_calibrate_Metadata
285  - @ref pipe_tasks_calibrate_Debug
286 
287 
288  @section pipe_tasks_calibrate_Purpose Description
289 
290  Given an exposure with a good PSF model and aperture correction map
291  (e.g. as provided by @ref CharacterizeImageTask), perform the following
292  operations:
293  - Run detection and measurement
294  - Run astrometry subtask to fit an improved WCS
295  - Run photoCal subtask to fit the exposure's photometric zero-point
296 
297  @section pipe_tasks_calibrate_Initialize Task initialisation
298 
299  @copydoc \_\_init\_\_
300 
301  @section pipe_tasks_calibrate_IO Invoking the Task
302 
303  If you want this task to unpersist inputs or persist outputs, then call
304  the `runDataRef` method (a wrapper around the `run` method).
305 
306  If you already have the inputs unpersisted and do not want to persist the
307  output then it is more direct to call the `run` method:
308 
309  @section pipe_tasks_calibrate_Config Configuration parameters
310 
311  See @ref CalibrateConfig
312 
313  @section pipe_tasks_calibrate_Metadata Quantities set in exposure Metadata
314 
315  Exposure metadata
316  <dl>
317  <dt>MAGZERO_RMS <dd>MAGZERO's RMS == sigma reported by photoCal task
318  <dt>MAGZERO_NOBJ <dd>Number of stars used == ngood reported by photoCal
319  task
320  <dt>COLORTERM1 <dd>?? (always 0.0)
321  <dt>COLORTERM2 <dd>?? (always 0.0)
322  <dt>COLORTERM3 <dd>?? (always 0.0)
323  </dl>
324 
325  @section pipe_tasks_calibrate_Debug Debug variables
326 
327  The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink
328  interface supports a flag
329  `--debug` to import `debug.py` from your `$PYTHONPATH`; see @ref baseDebug
330  for more about `debug.py`.
331 
332  CalibrateTask has a debug dictionary containing one key:
333  <dl>
334  <dt>calibrate
335  <dd>frame (an int; <= 0 to not display) in which to display the exposure,
336  sources and matches. See @ref lsst.meas.astrom.displayAstrometry for
337  the meaning of the various symbols.
338  </dl>
339 
340  For example, put something like:
341  @code{.py}
342  import lsstDebug
343  def DebugInfo(name):
344  di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would
345  # call us recursively
346  if name == "lsst.pipe.tasks.calibrate":
347  di.display = dict(
348  calibrate = 1,
349  )
350 
351  return di
352 
353  lsstDebug.Info = DebugInfo
354  @endcode
355  into your `debug.py` file and run `calibrateTask.py` with the `--debug`
356  flag.
357 
358  Some subtasks may have their own debug variables; see individual Task
359  documentation.
360  """
361 
362  # Example description used to live here, removed 2-20-2017 as per
363  # https://jira.lsstcorp.org/browse/DM-9520
364 
365  ConfigClass = CalibrateConfig
366  _DefaultName = "calibrate"
367  RunnerClass = pipeBase.ButlerInitializedTaskRunner
368 
369  def __init__(self, butler=None, astromRefObjLoader=None,
370  photoRefObjLoader=None, icSourceSchema=None,
371  initInputs=None, **kwargs):
372  """!Construct a CalibrateTask
373 
374  @param[in] butler The butler is passed to the refObjLoader constructor
375  in case it is needed. Ignored if the refObjLoader argument
376  provides a loader directly.
377  @param[in] astromRefObjLoader An instance of LoadReferenceObjectsTasks
378  that supplies an external reference catalog for astrometric
379  calibration. May be None if the desired loader can be constructed
380  from the butler argument or all steps requiring a reference catalog
381  are disabled.
382  @param[in] photoRefObjLoader An instance of LoadReferenceObjectsTasks
383  that supplies an external reference catalog for photometric
384  calibration. May be None if the desired loader can be constructed
385  from the butler argument or all steps requiring a reference catalog
386  are disabled.
387  @param[in] icSourceSchema schema for icSource catalog, or None.
388  Schema values specified in config.icSourceFieldsToCopy will be
389  taken from this schema. If set to None, no values will be
390  propagated from the icSourceCatalog
391  @param[in,out] kwargs other keyword arguments for
392  lsst.pipe.base.CmdLineTask
393  """
394  super().__init__(**kwargs)
395 
396  if icSourceSchema is None and butler is not None:
397  # Use butler to read icSourceSchema from disk.
398  icSourceSchema = butler.get("icSrc_schema", immediate=True).schema
399 
400  if icSourceSchema is None and butler is None and initInputs is not None:
401  icSourceSchema = initInputs['icSourceSchema'].schema
402 
403  if icSourceSchema is not None:
404  # use a schema mapper to avoid copying each field separately
405  self.schemaMapper = afwTable.SchemaMapper(icSourceSchema)
406  minimumSchema = afwTable.SourceTable.makeMinimalSchema()
407  self.schemaMapper.addMinimalSchema(minimumSchema, False)
408 
409  # Add fields to copy from an icSource catalog
410  # and a field to indicate that the source matched a source in that
411  # catalog. If any fields are missing then raise an exception, but
412  # first find all missing fields in order to make the error message
413  # more useful.
414  self.calibSourceKey = self.schemaMapper.addOutputField(
415  afwTable.Field["Flag"]("calib_detected",
416  "Source was detected as an icSource"))
417  missingFieldNames = []
418  for fieldName in self.config.icSourceFieldsToCopy:
419  try:
420  schemaItem = icSourceSchema.find(fieldName)
421  except Exception:
422  missingFieldNames.append(fieldName)
423  else:
424  # field found; if addMapping fails then raise an exception
425  self.schemaMapper.addMapping(schemaItem.getKey())
426 
427  if missingFieldNames:
428  raise RuntimeError("isSourceCat is missing fields {} "
429  "specified in icSourceFieldsToCopy"
430  .format(missingFieldNames))
431 
432  # produce a temporary schema to pass to the subtasks; finalize it
433  # later
434  self.schema = self.schemaMapper.editOutputSchema()
435  else:
436  self.schemaMapper = None
437  self.schema = afwTable.SourceTable.makeMinimalSchema()
438  self.makeSubtask('detection', schema=self.schema)
439 
441 
442  # Only create a subtask for fakes if configuration option is set
443  # N.B. the config for fake object task must be retargeted to a child
444  # of BaseFakeSourcesTask
445  if self.config.doInsertFakes:
446  self.makeSubtask("insertFakes")
447 
448  if self.config.doDeblend:
449  self.makeSubtask("deblend", schema=self.schema)
450  self.makeSubtask('measurement', schema=self.schema,
451  algMetadata=self.algMetadata)
452  if self.config.doApCorr:
453  self.makeSubtask('applyApCorr', schema=self.schema)
454  self.makeSubtask('catalogCalculation', schema=self.schema)
455 
456  if self.config.doAstrometry:
457  if astromRefObjLoader is None and butler is not None:
458  self.makeSubtask('astromRefObjLoader', butler=butler)
459  astromRefObjLoader = self.astromRefObjLoader
460  self.pixelMargin = astromRefObjLoader.config.pixelMargin
461  self.makeSubtask("astrometry", refObjLoader=astromRefObjLoader,
462  schema=self.schema)
463  if self.config.doPhotoCal:
464  if photoRefObjLoader is None and butler is not None:
465  self.makeSubtask('photoRefObjLoader', butler=butler)
466  photoRefObjLoader = self.photoRefObjLoader
467  self.pixelMargin = photoRefObjLoader.config.pixelMargin
468  self.makeSubtask("photoCal", refObjLoader=photoRefObjLoader,
469  schema=self.schema)
470 
471  if initInputs is not None and (astromRefObjLoader is not None or photoRefObjLoader is not None):
472  raise RuntimeError("PipelineTask form of this task should not be initialized with "
473  "reference object loaders.")
474 
475  if self.schemaMapper is not None:
476  # finalize the schema
477  self.schema = self.schemaMapper.getOutputSchema()
478  self.schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
479 
481  sourceCatSchema = afwTable.SourceCatalog(self.schema)
482  sourceCatSchema.getTable().setMetadata(self.algMetadata)
483  return {'outputSchema': sourceCatSchema}
484 
485  @classmethod
486  def getOutputDatasetTypes(cls, config):
487  outputTypesDict = super().getOutputDatasetTypes(config)
488  if config.doWriteMatches is False:
489  outputTypesDict.pop("matches")
490  if config.doWriteMatchesDenormalized is False:
491  outputTypesDict.pop("matchesDenormalized")
492  return outputTypesDict
493 
494  @classmethod
495  def getPrerequisiteDatasetTypes(cls, config):
496  return frozenset(["astromRefCat", "photoRefCat"])
497 
498  @pipeBase.timeMethod
499  def runDataRef(self, dataRef, exposure=None, background=None, icSourceCat=None,
500  doUnpersist=True):
501  """!Calibrate an exposure, optionally unpersisting inputs and
502  persisting outputs.
503 
504  This is a wrapper around the `run` method that unpersists inputs
505  (if `doUnpersist` true) and persists outputs (if `config.doWrite` true)
506 
507  @param[in] dataRef butler data reference corresponding to a science
508  image
509  @param[in,out] exposure characterized exposure (an
510  lsst.afw.image.ExposureF or similar), or None to unpersist existing
511  icExp and icBackground. See `run` method for details of what is
512  read and written.
513  @param[in,out] background initial model of background already
514  subtracted from exposure (an lsst.afw.math.BackgroundList). May be
515  None if no background has been subtracted, though that is unusual
516  for calibration. A refined background model is output. Ignored if
517  exposure is None.
518  @param[in] icSourceCat catalog from which to copy the fields specified
519  by icSourceKeys, or None;
520  @param[in] doUnpersist unpersist data:
521  - if True, exposure, background and icSourceCat are read from
522  dataRef and those three arguments must all be None;
523  - if False the exposure must be provided; background and
524  icSourceCat are optional. True is intended for running as a
525  command-line task, False for running as a subtask
526  @return same data as the calibrate method
527  """
528  self.log.info("Processing %s" % (dataRef.dataId))
529 
530  if doUnpersist:
531  if any(item is not None for item in (exposure, background,
532  icSourceCat)):
533  raise RuntimeError("doUnpersist true; exposure, background "
534  "and icSourceCat must all be None")
535  exposure = dataRef.get("icExp", immediate=True)
536  background = dataRef.get("icExpBackground", immediate=True)
537  icSourceCat = dataRef.get("icSrc", immediate=True)
538  elif exposure is None:
539  raise RuntimeError("doUnpersist false; exposure must be provided")
540 
541  exposureIdInfo = dataRef.get("expIdInfo")
542 
543  calRes = self.run(
544  exposure=exposure,
545  exposureIdInfo=exposureIdInfo,
546  background=background,
547  icSourceCat=icSourceCat,
548  )
549 
550  if self.config.doWrite:
551  self.writeOutputs(
552  dataRef=dataRef,
553  exposure=calRes.exposure,
554  background=calRes.background,
555  sourceCat=calRes.sourceCat,
556  astromMatches=calRes.astromMatches,
557  matchMeta=calRes.matchMeta,
558  )
559 
560  return calRes
561 
562  def adaptArgsAndRun(self, inputData, inputDataIds, outputDataIds, butler):
563  expId, expBits = butler.registry.packDataId("visit_detector",
564  inputDataIds['exposure'],
565  returnMaxBits=True)
566  inputData['exposureIdInfo'] = ExposureIdInfo(expId, expBits)
567 
568  if self.config.doAstrometry:
569  refObjLoader = ReferenceObjectLoader(dataIds=inputDataIds['astromRefCat'],
570  butler=butler,
571  config=self.config.astromRefObjLoader,
572  log=self.log)
573  self.pixelMargin = refObjLoader.config.pixelMargin
574  self.astrometry.setRefObjLoader(refObjLoader)
575 
576  if self.config.doPhotoCal:
577  photoRefObjLoader = ReferenceObjectLoader(inputDataIds['photoRefCat'],
578  butler,
579  self.config.photoRefObjLoader,
580  self.log)
581  self.pixelMargin = photoRefObjLoader.config.pixelMargin
582  self.photoCal.match.setRefObjLoader(photoRefObjLoader)
583 
584  results = self.run(**inputData)
585 
586  if self.config.doWriteMatches:
587  normalizedMatches = afwTable.packMatches(results.astromMatches)
588  normalizedMatches.table.setMetadata(results.matchMeta)
589  if self.config.doWriteMatchesDenormalized:
590  denormMatches = denormalizeMatches(results.astromMatches, results.matchMeta)
591  results.matchesDenormalized = denormMatches
592  results.matches = normalizedMatches
593  return results
594 
595  def run(self, exposure, exposureIdInfo=None, background=None,
596  icSourceCat=None):
597  """!Calibrate an exposure (science image or coadd)
598 
599  @param[in,out] exposure exposure to calibrate (an
600  lsst.afw.image.ExposureF or similar);
601  in:
602  - MaskedImage
603  - Psf
604  out:
605  - MaskedImage has background subtracted
606  - Wcs is replaced
607  - PhotoCalib is replaced
608  @param[in] exposureIdInfo ID info for exposure (an
609  lsst.obs.base.ExposureIdInfo) If not provided, returned
610  SourceCatalog IDs will not be globally unique.
611  @param[in,out] background background model already subtracted from
612  exposure (an lsst.afw.math.BackgroundList). May be None if no
613  background has been subtracted, though that is unusual for
614  calibration. A refined background model is output.
615  @param[in] icSourceCat A SourceCatalog from CharacterizeImageTask
616  from which we can copy some fields.
617 
618  @return pipe_base Struct containing these fields:
619  - exposure calibrate science exposure with refined WCS and PhotoCalib
620  - background model of background subtracted from exposure (an
621  lsst.afw.math.BackgroundList)
622  - sourceCat catalog of measured sources
623  - astromMatches list of source/refObj matches from the astrometry
624  solver
625  """
626  # detect, deblend and measure sources
627  if exposureIdInfo is None:
628  exposureIdInfo = ExposureIdInfo()
629 
630  if background is None:
631  background = BackgroundList()
632  sourceIdFactory = IdFactory.makeSource(exposureIdInfo.expId,
633  exposureIdInfo.unusedBits)
634  table = SourceTable.make(self.schema, sourceIdFactory)
635  table.setMetadata(self.algMetadata)
636 
637  detRes = self.detection.run(table=table, exposure=exposure,
638  doSmooth=True)
639  sourceCat = detRes.sources
640  if detRes.fpSets.background:
641  for bg in detRes.fpSets.background:
642  background.append(bg)
643  if self.config.doDeblend:
644  self.deblend.run(exposure=exposure, sources=sourceCat)
645  self.measurement.run(
646  measCat=sourceCat,
647  exposure=exposure,
648  exposureId=exposureIdInfo.expId
649  )
650  if self.config.doApCorr:
651  self.applyApCorr.run(
652  catalog=sourceCat,
653  apCorrMap=exposure.getInfo().getApCorrMap()
654  )
655  self.catalogCalculation.run(sourceCat)
656 
657  if icSourceCat is not None and \
658  len(self.config.icSourceFieldsToCopy) > 0:
659  self.copyIcSourceFields(icSourceCat=icSourceCat,
660  sourceCat=sourceCat)
661 
662  # TODO DM-11568: this contiguous check-and-copy could go away if we
663  # reserve enough space during SourceDetection and/or SourceDeblend.
664  # NOTE: sourceSelectors require contiguous catalogs, so ensure
665  # contiguity now, so views are preserved from here on.
666  if not sourceCat.isContiguous():
667  sourceCat = sourceCat.copy(deep=True)
668 
669  # perform astrometry calibration:
670  # fit an improved WCS and update the exposure's WCS in place
671  astromMatches = None
672  matchMeta = None
673  if self.config.doAstrometry:
674  try:
675  astromRes = self.astrometry.run(
676  exposure=exposure,
677  sourceCat=sourceCat,
678  )
679  astromMatches = astromRes.matches
680  matchMeta = astromRes.matchMeta
681  except Exception as e:
682  if self.config.requireAstrometry:
683  raise
684  self.log.warn("Unable to perform astrometric calibration "
685  "(%s): attempting to proceed" % e)
686 
687  # compute photometric calibration
688  if self.config.doPhotoCal:
689  try:
690  photoRes = self.photoCal.run(exposure, sourceCat=sourceCat, expId=exposureIdInfo.expId)
691  exposure.setPhotoCalib(photoRes.photoCalib)
692  # TODO: reword this to phrase it in terms of the calibration factor?
693  self.log.info("Photometric zero-point: %f" %
694  photoRes.photoCalib.instFluxToMagnitude(1.0))
695  self.setMetadata(exposure=exposure, photoRes=photoRes)
696  except Exception as e:
697  if self.config.requirePhotoCal:
698  raise
699  self.log.warn("Unable to perform photometric calibration "
700  "(%s): attempting to proceed" % e)
701  self.setMetadata(exposure=exposure, photoRes=None)
702 
703  if self.config.doInsertFakes:
704  self.insertFakes.run(exposure, background=background)
705 
706  table = SourceTable.make(self.schema, sourceIdFactory)
707  table.setMetadata(self.algMetadata)
708 
709  detRes = self.detection.run(table=table, exposure=exposure,
710  doSmooth=True)
711  sourceCat = detRes.sources
712  if detRes.fpSets.background:
713  for bg in detRes.fpSets.background:
714  background.append(bg)
715  if self.config.doDeblend:
716  self.deblend.run(exposure=exposure, sources=sourceCat)
717  self.measurement.run(
718  measCat=sourceCat,
719  exposure=exposure,
720  exposureId=exposureIdInfo.expId
721  )
722  if self.config.doApCorr:
723  self.applyApCorr.run(
724  catalog=sourceCat,
725  apCorrMap=exposure.getInfo().getApCorrMap()
726  )
727  self.catalogCalculation.run(sourceCat)
728 
729  if icSourceCat is not None and \
730  len(self.config.icSourceFieldsToCopy) > 0:
731  self.copyIcSourceFields(icSourceCat=icSourceCat,
732  sourceCat=sourceCat)
733 
734  frame = getDebugFrame(self._display, "calibrate")
735  if frame:
737  sourceCat=sourceCat,
738  exposure=exposure,
739  matches=astromMatches,
740  frame=frame,
741  pause=False,
742  )
743 
744  return pipeBase.Struct(
745  exposure=exposure,
746  background=background,
747  sourceCat=sourceCat,
748  astromMatches=astromMatches,
749  matchMeta=matchMeta,
750  # These are duplicate entries with different names for use with
751  # gen3 middleware
752  outputExposure=exposure,
753  outputCat=sourceCat,
754  outputBackground=background,
755  )
756 
757  def writeOutputs(self, dataRef, exposure, background, sourceCat,
758  astromMatches, matchMeta):
759  """Write output data to the output repository
760 
761  @param[in] dataRef butler data reference corresponding to a science
762  image
763  @param[in] exposure exposure to write
764  @param[in] background background model for exposure
765  @param[in] sourceCat catalog of measured sources
766  @param[in] astromMatches list of source/refObj matches from the
767  astrometry solver
768  """
769  dataRef.put(sourceCat, "src")
770  if self.config.doWriteMatches and astromMatches is not None:
771  normalizedMatches = afwTable.packMatches(astromMatches)
772  normalizedMatches.table.setMetadata(matchMeta)
773  dataRef.put(normalizedMatches, "srcMatch")
774  if self.config.doWriteMatchesDenormalized:
775  denormMatches = denormalizeMatches(astromMatches, matchMeta)
776  dataRef.put(denormMatches, "srcMatchFull")
777  if self.config.doWriteExposure:
778  dataRef.put(exposure, "calexp")
779  dataRef.put(background, "calexpBackground")
780 
781  def getSchemaCatalogs(self):
782  """Return a dict of empty catalogs for each catalog dataset produced
783  by this task.
784  """
785  sourceCat = afwTable.SourceCatalog(self.schema)
786  sourceCat.getTable().setMetadata(self.algMetadata)
787  return {"src": sourceCat}
788 
789  def setMetadata(self, exposure, photoRes=None):
790  """!Set task and exposure metadata
791 
792  Logs a warning and continues if needed data is missing.
793 
794  @param[in,out] exposure exposure whose metadata is to be set
795  @param[in] photoRes results of running photoCal; if None then it was
796  not run
797  """
798  if photoRes is None:
799  return
800 
801  # convert zero-point to (mag/sec/adu) for task MAGZERO metadata
802  try:
803  exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
804  magZero = photoRes.zp - 2.5*math.log10(exposureTime)
805  self.metadata.set('MAGZERO', magZero)
806  except Exception:
807  self.log.warn("Could not set normalized MAGZERO in header: no "
808  "exposure time")
809 
810  try:
811  metadata = exposure.getMetadata()
812  metadata.set('MAGZERO_RMS', photoRes.sigma)
813  metadata.set('MAGZERO_NOBJ', photoRes.ngood)
814  metadata.set('COLORTERM1', 0.0)
815  metadata.set('COLORTERM2', 0.0)
816  metadata.set('COLORTERM3', 0.0)
817  except Exception as e:
818  self.log.warn("Could not set exposure metadata: %s" % (e,))
819 
820  def copyIcSourceFields(self, icSourceCat, sourceCat):
821  """!Match sources in icSourceCat and sourceCat and copy the specified fields
822 
823  @param[in] icSourceCat catalog from which to copy fields
824  @param[in,out] sourceCat catalog to which to copy fields
825 
826  The fields copied are those specified by `config.icSourceFieldsToCopy`
827  that actually exist in the schema. This was set up by the constructor
828  using self.schemaMapper.
829  """
830  if self.schemaMapper is None:
831  raise RuntimeError("To copy icSource fields you must specify "
832  "icSourceSchema nd icSourceKeys when "
833  "constructing this task")
834  if icSourceCat is None or sourceCat is None:
835  raise RuntimeError("icSourceCat and sourceCat must both be "
836  "specified")
837  if len(self.config.icSourceFieldsToCopy) == 0:
838  self.log.warn("copyIcSourceFields doing nothing because "
839  "icSourceFieldsToCopy is empty")
840  return
841 
842  mc = afwTable.MatchControl()
843  mc.findOnlyClosest = False # return all matched objects
844  matches = afwTable.matchXy(icSourceCat, sourceCat,
845  self.config.matchRadiusPix, mc)
846  if self.config.doDeblend:
847  deblendKey = sourceCat.schema["deblend_nChild"].asKey()
848  # if deblended, keep children
849  matches = [m for m in matches if m[1].get(deblendKey) == 0]
850 
851  # Because we had to allow multiple matches to handle parents, we now
852  # need to prune to the best matches
853  # closest matches as a dict of icSourceCat source ID:
854  # (icSourceCat source, sourceCat source, distance in pixels)
855  bestMatches = {}
856  for m0, m1, d in matches:
857  id0 = m0.getId()
858  match = bestMatches.get(id0)
859  if match is None or d <= match[2]:
860  bestMatches[id0] = (m0, m1, d)
861  matches = list(bestMatches.values())
862 
863  # Check that no sourceCat sources are listed twice (we already know
864  # that each match has a unique icSourceCat source ID, due to using
865  # that ID as the key in bestMatches)
866  numMatches = len(matches)
867  numUniqueSources = len(set(m[1].getId() for m in matches))
868  if numUniqueSources != numMatches:
869  self.log.warn("{} icSourceCat sources matched only {} sourceCat "
870  "sources".format(numMatches, numUniqueSources))
871 
872  self.log.info("Copying flags from icSourceCat to sourceCat for "
873  "%s sources" % (numMatches,))
874 
875  # For each match: set the calibSourceKey flag and copy the desired
876  # fields
877  for icSrc, src, d in matches:
878  src.setFlag(self.calibSourceKey, True)
879  # src.assign copies the footprint from icSrc, which we don't want
880  # (DM-407)
881  # so set icSrc's footprint to src's footprint before src.assign,
882  # then restore it
883  icSrcFootprint = icSrc.getFootprint()
884  try:
885  icSrc.setFootprint(src.getFootprint())
886  src.assign(icSrc, self.schemaMapper)
887  finally:
888  icSrc.setFootprint(icSrcFootprint)
def copyIcSourceFields(self, icSourceCat, sourceCat)
Match sources in icSourceCat and sourceCat and copy the specified fields.
Definition: calibrate.py:820
def __init__(self, butler=None, astromRefObjLoader=None, photoRefObjLoader=None, icSourceSchema=None, initInputs=None, kwargs)
Construct a CalibrateTask.
Definition: calibrate.py:371
def InitOutputDatasetField
Definition: config.py:339
def getPrerequisiteDatasetTypes(cls, config)
Definition: calibrate.py:495
def run(self, exposure, exposureIdInfo=None, background=None, icSourceCat=None)
Calibrate an exposure (science image or coadd)
Definition: calibrate.py:596
Class for storing ordered metadata with comments.
Definition: PropertyList.h:68
A mapping between the keys of two Schemas, used to copy data between them.
Definition: SchemaMapper.h:21
def denormalizeMatches(matches, matchMeta=None)
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations...
daf::base::PropertySet * set
Definition: fits.cc:884
def writeOutputs(self, dataRef, exposure, background, sourceCat, astromMatches, matchMeta)
Definition: calibrate.py:758
bool any(CoordinateExpr< N > const &expr) noexcept
Return true if any elements are true.
def adaptArgsAndRun(self, inputData, inputDataIds, outputDataIds, butler)
Definition: calibrate.py:562
Pass parameters to algorithms that match list of sources.
Definition: Match.h:45
def format(config, name=None, writeSourceLine=True, prefix="", verbose=False)
Definition: history.py:168
template BaseCatalog packMatches(SourceMatchVector const &)
A description of a field in a table.
Definition: Field.h:24
def setMetadata(self, exposure, photoRes=None)
Set task and exposure metadata.
Definition: calibrate.py:789
def displayAstrometry(refCat=None, sourceCat=None, distortedCentroidKey=None, bbox=None, exposure=None, matches=None, frame=1, title="", pause=True)
Definition: display.py:34
def getDebugFrame(debugDisplay, name)
Definition: lsstDebug.py:90
SourceMatchVector matchXy(SourceCatalog const &cat, double radius, bool symmetric)
Compute all tuples (s1,s2,d) where s1 != s2, s1 and s2 both belong to cat, and d, the distance betwee...
Definition: Match.cc:383
def runDataRef(self, dataRef, exposure=None, background=None, icSourceCat=None, doUnpersist=True)
Calibrate an exposure, optionally unpersisting inputs and persisting outputs.
Definition: calibrate.py:500
daf::base::PropertyList * list
Definition: fits.cc:885
Calibrate an exposure: measure sources and perform astrometric and photometric calibration.
Definition: calibrate.py:272