lsst.pipe.tasks  16.0-23-ge8a9b866+4
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 import lsst.afw.table as afwTable
28 from lsst.meas.astrom import AstrometryTask, displayAstrometry, denormalizeMatches
29 from lsst.meas.extensions.astrometryNet import LoadAstrometryNetObjectsTask
30 from lsst.obs.base import ExposureIdInfo
31 import lsst.daf.base as dafBase
32 from lsst.afw.math import BackgroundList
33 from lsst.afw.table import IdFactory, SourceTable
34 from lsst.meas.algorithms import SourceDetectionTask
35 from lsst.meas.base import (SingleFrameMeasurementTask, ApplyApCorrTask,
36  CatalogCalculationTask)
37 from lsst.meas.deblender import SourceDeblendTask
38 from .fakes import BaseFakeSourcesTask
39 from .photoCal import PhotoCalTask
40 
41 __all__ = ["CalibrateConfig", "CalibrateTask"]
42 
43 
44 class CalibrateConfig(pexConfig.Config):
45  """Config for CalibrateTask"""
46  doWrite = pexConfig.Field(
47  dtype=bool,
48  default=True,
49  doc="Save calibration results?",
50  )
51  doWriteHeavyFootprintsInSources = pexConfig.Field(
52  dtype=bool,
53  default=True,
54  doc="Include HeavyFootprint data in source table? If false then heavy "
55  "footprints are saved as normal footprints, which saves some space"
56  )
57  doWriteMatches = pexConfig.Field(
58  dtype=bool,
59  default=True,
60  doc="Write reference matches (ignored if doWrite false)?",
61  )
62  doWriteMatchesDenormalized = pexConfig.Field(
63  dtype=bool,
64  default=False,
65  doc=("Write reference matches in denormalized format? "
66  "This format uses more disk space, but is more convenient to "
67  "read. Ignored if doWriteMatches=False or doWrite=False."),
68  )
69  doAstrometry = pexConfig.Field(
70  dtype=bool,
71  default=True,
72  doc="Perform astrometric calibration?",
73  )
74  astromRefObjLoader = pexConfig.ConfigurableField(
75  target=LoadAstrometryNetObjectsTask,
76  doc="reference object loader for astrometric calibration",
77  )
78  photoRefObjLoader = pexConfig.ConfigurableField(
79  target=LoadAstrometryNetObjectsTask,
80  doc="reference object loader for photometric calibration",
81  )
82  astrometry = pexConfig.ConfigurableField(
83  target=AstrometryTask,
84  doc="Perform astrometric calibration to refine the WCS",
85  )
86  requireAstrometry = pexConfig.Field(
87  dtype=bool,
88  default=True,
89  doc=("Raise an exception if astrometry fails? Ignored if doAstrometry "
90  "false."),
91  )
92  doPhotoCal = pexConfig.Field(
93  dtype=bool,
94  default=True,
95  doc="Perform phometric calibration?",
96  )
97  requirePhotoCal = pexConfig.Field(
98  dtype=bool,
99  default=True,
100  doc=("Raise an exception if photoCal fails? Ignored if doPhotoCal "
101  "false."),
102  )
103  photoCal = pexConfig.ConfigurableField(
104  target=PhotoCalTask,
105  doc="Perform photometric calibration",
106  )
107  icSourceFieldsToCopy = pexConfig.ListField(
108  dtype=str,
109  default=("calib_psf_candidate", "calib_psf_used", "calib_psf_reserved"),
110  doc=("Fields to copy from the icSource catalog to the output catalog "
111  "for matching sources Any missing fields will trigger a "
112  "RuntimeError exception. Ignored if icSourceCat is not provided.")
113  )
114  matchRadiusPix = pexConfig.Field(
115  dtype=float,
116  default=3,
117  doc=("Match radius for matching icSourceCat objects to sourceCat "
118  "objects (pixels)"),
119  )
120  checkUnitsParseStrict = pexConfig.Field(
121  doc=("Strictness of Astropy unit compatibility check, can be 'raise', "
122  "'warn' or 'silent'"),
123  dtype=str,
124  default="raise",
125  )
126  detection = pexConfig.ConfigurableField(
127  target=SourceDetectionTask,
128  doc="Detect sources"
129  )
130  doDeblend = pexConfig.Field(
131  dtype=bool,
132  default=True,
133  doc="Run deblender input exposure"
134  )
135  deblend = pexConfig.ConfigurableField(
136  target=SourceDeblendTask,
137  doc="Split blended sources into their components"
138  )
139  measurement = pexConfig.ConfigurableField(
140  target=SingleFrameMeasurementTask,
141  doc="Measure sources"
142  )
143  doApCorr = pexConfig.Field(
144  dtype=bool,
145  default=True,
146  doc="Run subtask to apply aperture correction"
147  )
148  applyApCorr = pexConfig.ConfigurableField(
149  target=ApplyApCorrTask,
150  doc="Subtask to apply aperture corrections"
151  )
152  # If doApCorr is False, and the exposure does not have apcorrections
153  # already applied, the active plugins in catalogCalculation almost
154  # certainly should not contain the characterization plugin
155  catalogCalculation = pexConfig.ConfigurableField(
156  target=CatalogCalculationTask,
157  doc="Subtask to run catalogCalculation plugins on catalog"
158  )
159  doInsertFakes = pexConfig.Field(
160  dtype=bool,
161  default=False,
162  doc="Run fake sources injection task"
163  )
164  insertFakes = pexConfig.ConfigurableField(
165  target=BaseFakeSourcesTask,
166  doc="Injection of fake sources for testing purposes (must be "
167  "retargeted)"
168  )
169 
170  def setDefaults(self):
171  pexConfig.Config.setDefaults(self)
172  # aperture correction should already be measured
173 
174 
175 
181 
182 class CalibrateTask(pipeBase.CmdLineTask):
183  """!Calibrate an exposure: measure sources and perform astrometric and
184  photometric calibration
185 
186  @anchor CalibrateTask_
187 
188  @section pipe_tasks_calibrate_Contents Contents
189 
190  - @ref pipe_tasks_calibrate_Purpose
191  - @ref pipe_tasks_calibrate_Initialize
192  - @ref pipe_tasks_calibrate_IO
193  - @ref pipe_tasks_calibrate_Config
194  - @ref pipe_tasks_calibrate_Metadata
195  - @ref pipe_tasks_calibrate_Debug
196 
197 
198  @section pipe_tasks_calibrate_Purpose Description
199 
200  Given an exposure with a good PSF model and aperture correction map
201  (e.g. as provided by @ref CharacterizeImageTask), perform the following
202  operations:
203  - Run detection and measurement
204  - Run astrometry subtask to fit an improved WCS
205  - Run photoCal subtask to fit the exposure's photometric zero-point
206 
207  @section pipe_tasks_calibrate_Initialize Task initialisation
208 
209  @copydoc \_\_init\_\_
210 
211  @section pipe_tasks_calibrate_IO Invoking the Task
212 
213  If you want this task to unpersist inputs or persist outputs, then call
214  the `runDataRef` method (a wrapper around the `run` method).
215 
216  If you already have the inputs unpersisted and do not want to persist the
217  output then it is more direct to call the `run` method:
218 
219  @section pipe_tasks_calibrate_Config Configuration parameters
220 
221  See @ref CalibrateConfig
222 
223  @section pipe_tasks_calibrate_Metadata Quantities set in exposure Metadata
224 
225  Exposure metadata
226  <dl>
227  <dt>MAGZERO_RMS <dd>MAGZERO's RMS == sigma reported by photoCal task
228  <dt>MAGZERO_NOBJ <dd>Number of stars used == ngood reported by photoCal
229  task
230  <dt>COLORTERM1 <dd>?? (always 0.0)
231  <dt>COLORTERM2 <dd>?? (always 0.0)
232  <dt>COLORTERM3 <dd>?? (always 0.0)
233  </dl>
234 
235  @section pipe_tasks_calibrate_Debug Debug variables
236 
237  The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink
238  interface supports a flag
239  `--debug` to import `debug.py` from your `$PYTHONPATH`; see @ref baseDebug
240  for more about `debug.py`.
241 
242  CalibrateTask has a debug dictionary containing one key:
243  <dl>
244  <dt>calibrate
245  <dd>frame (an int; <= 0 to not display) in which to display the exposure,
246  sources and matches. See @ref lsst.meas.astrom.displayAstrometry for
247  the meaning of the various symbols.
248  </dl>
249 
250  For example, put something like:
251  @code{.py}
252  import lsstDebug
253  def DebugInfo(name):
254  di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would
255  # call us recursively
256  if name == "lsst.pipe.tasks.calibrate":
257  di.display = dict(
258  calibrate = 1,
259  )
260 
261  return di
262 
263  lsstDebug.Info = DebugInfo
264  @endcode
265  into your `debug.py` file and run `calibrateTask.py` with the `--debug`
266  flag.
267 
268  Some subtasks may have their own debug variables; see individual Task
269  documentation.
270  """
271 
272  # Example description used to live here, removed 2-20-2017 as per
273  # https://jira.lsstcorp.org/browse/DM-9520
274 
275  ConfigClass = CalibrateConfig
276  _DefaultName = "calibrate"
277  RunnerClass = pipeBase.ButlerInitializedTaskRunner
278 
279  def __init__(self, butler=None, astromRefObjLoader=None,
280  photoRefObjLoader=None, icSourceSchema=None, **kwargs):
281  """!Construct a CalibrateTask
282 
283  @param[in] butler The butler is passed to the refObjLoader constructor
284  in case it is needed. Ignored if the refObjLoader argument
285  provides a loader directly.
286  @param[in] astromRefObjLoader An instance of LoadReferenceObjectsTasks
287  that supplies an external reference catalog for astrometric
288  calibration. May be None if the desired loader can be constructed
289  from the butler argument or all steps requiring a reference catalog
290  are disabled.
291  @param[in] photoRefObjLoader An instance of LoadReferenceObjectsTasks
292  that supplies an external reference catalog for photometric
293  calibration. May be None if the desired loader can be constructed
294  from the butler argument or all steps requiring a reference catalog
295  are disabled.
296  @param[in] icSourceSchema schema for icSource catalog, or None.
297  Schema values specified in config.icSourceFieldsToCopy will be
298  taken from this schema. If set to None, no values will be
299  propagated from the icSourceCatalog
300  @param[in,out] kwargs other keyword arguments for
301  lsst.pipe.base.CmdLineTask
302  """
303  pipeBase.CmdLineTask.__init__(self, **kwargs)
304 
305  if icSourceSchema is None and butler is not None:
306  # Use butler to read icSourceSchema from disk.
307  icSourceSchema = butler.get("icSrc_schema", immediate=True).schema
308 
309  if icSourceSchema is not None:
310  # use a schema mapper to avoid copying each field separately
311  self.schemaMapper = afwTable.SchemaMapper(icSourceSchema)
312  minimumSchema = afwTable.SourceTable.makeMinimalSchema()
313  self.schemaMapper.addMinimalSchema(minimumSchema, False)
314 
315  # Add fields to copy from an icSource catalog
316  # and a field to indicate that the source matched a source in that
317  # catalog. If any fields are missing then raise an exception, but
318  # first find all missing fields in order to make the error message
319  # more useful.
320  self.calibSourceKey = self.schemaMapper.addOutputField(
321  afwTable.Field["Flag"]("calib_detected",
322  "Source was detected as an icSource"))
323  missingFieldNames = []
324  for fieldName in self.config.icSourceFieldsToCopy:
325  try:
326  schemaItem = icSourceSchema.find(fieldName)
327  except Exception:
328  missingFieldNames.append(fieldName)
329  else:
330  # field found; if addMapping fails then raise an exception
331  self.schemaMapper.addMapping(schemaItem.getKey())
332 
333  if missingFieldNames:
334  raise RuntimeError("isSourceCat is missing fields {} "
335  "specified in icSourceFieldsToCopy"
336  .format(missingFieldNames))
337 
338  # produce a temporary schema to pass to the subtasks; finalize it
339  # later
340  self.schema = self.schemaMapper.editOutputSchema()
341  else:
342  self.schemaMapper = None
343  self.schema = afwTable.SourceTable.makeMinimalSchema()
344  self.makeSubtask('detection', schema=self.schema)
345 
346  self.algMetadata = dafBase.PropertyList()
347 
348  # Only create a subtask for fakes if configuration option is set
349  # N.B. the config for fake object task must be retargeted to a child
350  # of BaseFakeSourcesTask
351  if self.config.doInsertFakes:
352  self.makeSubtask("insertFakes")
353 
354  if self.config.doDeblend:
355  self.makeSubtask("deblend", schema=self.schema)
356  self.makeSubtask('measurement', schema=self.schema,
357  algMetadata=self.algMetadata)
358  if self.config.doApCorr:
359  self.makeSubtask('applyApCorr', schema=self.schema)
360  self.makeSubtask('catalogCalculation', schema=self.schema)
361 
362  if self.config.doAstrometry:
363  if astromRefObjLoader is None:
364  self.makeSubtask('astromRefObjLoader', butler=butler)
365  astromRefObjLoader = self.astromRefObjLoader
366  self.pixelMargin = astromRefObjLoader.config.pixelMargin
367  self.makeSubtask("astrometry", refObjLoader=astromRefObjLoader,
368  schema=self.schema)
369  if self.config.doPhotoCal:
370  if photoRefObjLoader is None:
371  self.makeSubtask('photoRefObjLoader', butler=butler)
372  photoRefObjLoader = self.photoRefObjLoader
373  self.pixelMargin = photoRefObjLoader.config.pixelMargin
374  self.makeSubtask("photoCal", refObjLoader=photoRefObjLoader,
375  schema=self.schema)
376 
377  if self.schemaMapper is not None:
378  # finalize the schema
379  self.schema = self.schemaMapper.getOutputSchema()
380  self.schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
381 
382  @pipeBase.timeMethod
383  def runDataRef(self, dataRef, exposure=None, background=None, icSourceCat=None,
384  doUnpersist=True):
385  """!Calibrate an exposure, optionally unpersisting inputs and
386  persisting outputs.
387 
388  This is a wrapper around the `run` method that unpersists inputs
389  (if `doUnpersist` true) and persists outputs (if `config.doWrite` true)
390 
391  @param[in] dataRef butler data reference corresponding to a science
392  image
393  @param[in,out] exposure characterized exposure (an
394  lsst.afw.image.ExposureF or similar), or None to unpersist existing
395  icExp and icBackground. See `run` method for details of what is
396  read and written.
397  @param[in,out] background initial model of background already
398  subtracted from exposure (an lsst.afw.math.BackgroundList). May be
399  None if no background has been subtracted, though that is unusual
400  for calibration. A refined background model is output. Ignored if
401  exposure is None.
402  @param[in] icSourceCat catalog from which to copy the fields specified
403  by icSourceKeys, or None;
404  @param[in] doUnpersist unpersist data:
405  - if True, exposure, background and icSourceCat are read from
406  dataRef and those three arguments must all be None;
407  - if False the exposure must be provided; background and
408  icSourceCat are optional. True is intended for running as a
409  command-line task, False for running as a subtask
410 
411  @return same data as the calibrate method
412  """
413  self.log.info("Processing %s" % (dataRef.dataId))
414 
415  if doUnpersist:
416  if any(item is not None for item in (exposure, background,
417  icSourceCat)):
418  raise RuntimeError("doUnpersist true; exposure, background "
419  "and icSourceCat must all be None")
420  exposure = dataRef.get("icExp", immediate=True)
421  background = dataRef.get("icExpBackground", immediate=True)
422  icSourceCat = dataRef.get("icSrc", immediate=True)
423  elif exposure is None:
424  raise RuntimeError("doUnpersist false; exposure must be provided")
425 
426  exposureIdInfo = dataRef.get("expIdInfo")
427 
428  calRes = self.run(
429  exposure=exposure,
430  exposureIdInfo=exposureIdInfo,
431  background=background,
432  icSourceCat=icSourceCat,
433  )
434 
435  if self.config.doWrite:
436  self.writeOutputs(
437  dataRef=dataRef,
438  exposure=calRes.exposure,
439  background=calRes.background,
440  sourceCat=calRes.sourceCat,
441  astromMatches=calRes.astromMatches,
442  matchMeta=calRes.matchMeta,
443  )
444 
445  return calRes
446 
447  def run(self, exposure, exposureIdInfo=None, background=None,
448  icSourceCat=None):
449  """!Calibrate an exposure (science image or coadd)
450 
451  @param[in,out] exposure exposure to calibrate (an
452  lsst.afw.image.ExposureF or similar);
453  in:
454  - MaskedImage
455  - Psf
456  out:
457  - MaskedImage has background subtracted
458  - Wcs is replaced
459  - Calib zero-point is set
460  @param[in] exposureIdInfo ID info for exposure (an
461  lsst.obs.base.ExposureIdInfo) If not provided, returned
462  SourceCatalog IDs will not be globally unique.
463  @param[in,out] background background model already subtracted from
464  exposure (an lsst.afw.math.BackgroundList). May be None if no
465  background has been subtracted, though that is unusual for
466  calibration. A refined background model is output.
467  @param[in] icSourceCat A SourceCatalog from CharacterizeImageTask
468  from which we can copy some fields.
469 
470  @return pipe_base Struct containing these fields:
471  - exposure calibrate science exposure with refined WCS and Calib
472  - background model of background subtracted from exposure (an
473  lsst.afw.math.BackgroundList)
474  - sourceCat catalog of measured sources
475  - astromMatches list of source/refObj matches from the astrometry
476  solver
477  """
478  # detect, deblend and measure sources
479  if exposureIdInfo is None:
480  exposureIdInfo = ExposureIdInfo()
481 
482  if background is None:
483  background = BackgroundList()
484  sourceIdFactory = IdFactory.makeSource(exposureIdInfo.expId,
485  exposureIdInfo.unusedBits)
486  table = SourceTable.make(self.schema, sourceIdFactory)
487  table.setMetadata(self.algMetadata)
488 
489  detRes = self.detection.run(table=table, exposure=exposure,
490  doSmooth=True)
491  sourceCat = detRes.sources
492  if detRes.fpSets.background:
493  for bg in detRes.fpSets.background:
494  background.append(bg)
495  if self.config.doDeblend:
496  self.deblend.run(exposure=exposure, sources=sourceCat)
497  self.measurement.run(
498  measCat=sourceCat,
499  exposure=exposure,
500  exposureId=exposureIdInfo.expId
501  )
502  if self.config.doApCorr:
503  self.applyApCorr.run(
504  catalog=sourceCat,
505  apCorrMap=exposure.getInfo().getApCorrMap()
506  )
507  self.catalogCalculation.run(sourceCat)
508 
509  if icSourceCat is not None and \
510  len(self.config.icSourceFieldsToCopy) > 0:
511  self.copyIcSourceFields(icSourceCat=icSourceCat,
512  sourceCat=sourceCat)
513 
514  # TODO DM-11568: this contiguous check-and-copy could go away if we
515  # reserve enough space during SourceDetection and/or SourceDeblend.
516  # NOTE: sourceSelectors require contiguous catalogs, so ensure
517  # contiguity now, so views are preserved from here on.
518  if not sourceCat.isContiguous():
519  sourceCat = sourceCat.copy(deep=True)
520 
521  # perform astrometry calibration:
522  # fit an improved WCS and update the exposure's WCS in place
523  astromMatches = None
524  matchMeta = None
525  if self.config.doAstrometry:
526  try:
527  astromRes = self.astrometry.run(
528  exposure=exposure,
529  sourceCat=sourceCat,
530  )
531  astromMatches = astromRes.matches
532  matchMeta = astromRes.matchMeta
533  except Exception as e:
534  if self.config.requireAstrometry:
535  raise
536  self.log.warn("Unable to perform astrometric calibration "
537  "(%s): attempting to proceed" % e)
538 
539  # compute photometric calibration
540  if self.config.doPhotoCal:
541  try:
542  photoRes = self.photoCal.run(exposure, sourceCat=sourceCat, expId=exposureIdInfo.expId)
543  exposure.getCalib().setFluxMag0(photoRes.calib.getFluxMag0())
544  self.log.info("Photometric zero-point: %f" %
545  photoRes.calib.getMagnitude(1.0))
546  self.setMetadata(exposure=exposure, photoRes=photoRes)
547  except Exception as e:
548  if self.config.requirePhotoCal:
549  raise
550  self.log.warn("Unable to perform photometric calibration "
551  "(%s): attempting to proceed" % e)
552  self.setMetadata(exposure=exposure, photoRes=None)
553 
554  if self.config.doInsertFakes:
555  self.insertFakes.run(exposure, background=background)
556 
557  table = SourceTable.make(self.schema, sourceIdFactory)
558  table.setMetadata(self.algMetadata)
559 
560  detRes = self.detection.run(table=table, exposure=exposure,
561  doSmooth=True)
562  sourceCat = detRes.sources
563  if detRes.fpSets.background:
564  for bg in detRes.fpSets.background:
565  background.append(bg)
566  if self.config.doDeblend:
567  self.deblend.run(exposure=exposure, sources=sourceCat)
568  self.measurement.run(
569  measCat=sourceCat,
570  exposure=exposure,
571  exposureId=exposureIdInfo.expId
572  )
573  if self.config.doApCorr:
574  self.applyApCorr.run(
575  catalog=sourceCat,
576  apCorrMap=exposure.getInfo().getApCorrMap()
577  )
578  self.catalogCalculation.run(sourceCat)
579 
580  if icSourceCat is not None and \
581  len(self.config.icSourceFieldsToCopy) > 0:
582  self.copyIcSourceFields(icSourceCat=icSourceCat,
583  sourceCat=sourceCat)
584 
585  frame = getDebugFrame(self._display, "calibrate")
586  if frame:
587  displayAstrometry(
588  sourceCat=sourceCat,
589  exposure=exposure,
590  matches=astromMatches,
591  frame=frame,
592  pause=False,
593  )
594 
595  return pipeBase.Struct(
596  exposure=exposure,
597  background=background,
598  sourceCat=sourceCat,
599  astromMatches=astromMatches,
600  matchMeta=matchMeta,
601  )
602 
603  def writeOutputs(self, dataRef, exposure, background, sourceCat,
604  astromMatches, matchMeta):
605  """Write output data to the output repository
606 
607  @param[in] dataRef butler data reference corresponding to a science
608  image
609  @param[in] exposure exposure to write
610  @param[in] background background model for exposure
611  @param[in] sourceCat catalog of measured sources
612  @param[in] astromMatches list of source/refObj matches from the
613  astrometry solver
614  """
615  dataRef.put(sourceCat, "src")
616  if self.config.doWriteMatches and astromMatches is not None:
617  normalizedMatches = afwTable.packMatches(astromMatches)
618  normalizedMatches.table.setMetadata(matchMeta)
619  dataRef.put(normalizedMatches, "srcMatch")
620  if self.config.doWriteMatchesDenormalized:
621  denormMatches = denormalizeMatches(astromMatches, matchMeta)
622  dataRef.put(denormMatches, "srcMatchFull")
623  dataRef.put(exposure, "calexp")
624  dataRef.put(background, "calexpBackground")
625 
626  def getSchemaCatalogs(self):
627  """Return a dict of empty catalogs for each catalog dataset produced
628  by this task.
629  """
630  sourceCat = afwTable.SourceCatalog(self.schema)
631  sourceCat.getTable().setMetadata(self.algMetadata)
632  return {"src": sourceCat}
633 
634  def setMetadata(self, exposure, photoRes=None):
635  """!Set task and exposure metadata
636 
637  Logs a warning and continues if needed data is missing.
638 
639  @param[in,out] exposure exposure whose metadata is to be set
640  @param[in] photoRes results of running photoCal; if None then it was
641  not run
642  """
643  if photoRes is None:
644  return
645 
646  # convert zero-point to (mag/sec/adu) for task MAGZERO metadata
647  try:
648  exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
649  magZero = photoRes.zp - 2.5*math.log10(exposureTime)
650  self.metadata.set('MAGZERO', magZero)
651  except Exception:
652  self.log.warn("Could not set normalized MAGZERO in header: no "
653  "exposure time")
654 
655  try:
656  metadata = exposure.getMetadata()
657  metadata.set('MAGZERO_RMS', photoRes.sigma)
658  metadata.set('MAGZERO_NOBJ', photoRes.ngood)
659  metadata.set('COLORTERM1', 0.0)
660  metadata.set('COLORTERM2', 0.0)
661  metadata.set('COLORTERM3', 0.0)
662  except Exception as e:
663  self.log.warn("Could not set exposure metadata: %s" % (e,))
664 
665  def copyIcSourceFields(self, icSourceCat, sourceCat):
666  """!Match sources in icSourceCat and sourceCat and copy the specified fields
667 
668  @param[in] icSourceCat catalog from which to copy fields
669  @param[in,out] sourceCat catalog to which to copy fields
670 
671  The fields copied are those specified by `config.icSourceFieldsToCopy`
672  that actually exist in the schema. This was set up by the constructor
673  using self.schemaMapper.
674  """
675  if self.schemaMapper is None:
676  raise RuntimeError("To copy icSource fields you must specify "
677  "icSourceSchema nd icSourceKeys when "
678  "constructing this task")
679  if icSourceCat is None or sourceCat is None:
680  raise RuntimeError("icSourceCat and sourceCat must both be "
681  "specified")
682  if len(self.config.icSourceFieldsToCopy) == 0:
683  self.log.warn("copyIcSourceFields doing nothing because "
684  "icSourceFieldsToCopy is empty")
685  return
686 
687  mc = afwTable.MatchControl()
688  mc.findOnlyClosest = False # return all matched objects
689  matches = afwTable.matchXy(icSourceCat, sourceCat,
690  self.config.matchRadiusPix, mc)
691  if self.config.doDeblend:
692  deblendKey = sourceCat.schema["deblend_nChild"].asKey()
693  # if deblended, keep children
694  matches = [m for m in matches if m[1].get(deblendKey) == 0]
695 
696  # Because we had to allow multiple matches to handle parents, we now
697  # need to prune to the best matches
698  # closest matches as a dict of icSourceCat source ID:
699  # (icSourceCat source, sourceCat source, distance in pixels)
700  bestMatches = {}
701  for m0, m1, d in matches:
702  id0 = m0.getId()
703  match = bestMatches.get(id0)
704  if match is None or d <= match[2]:
705  bestMatches[id0] = (m0, m1, d)
706  matches = list(bestMatches.values())
707 
708  # Check that no sourceCat sources are listed twice (we already know
709  # that each match has a unique icSourceCat source ID, due to using
710  # that ID as the key in bestMatches)
711  numMatches = len(matches)
712  numUniqueSources = len(set(m[1].getId() for m in matches))
713  if numUniqueSources != numMatches:
714  self.log.warn("{} icSourceCat sources matched only {} sourceCat "
715  "sources".format(numMatches, numUniqueSources))
716 
717  self.log.info("Copying flags from icSourceCat to sourceCat for "
718  "%s sources" % (numMatches,))
719 
720  # For each match: set the calibSourceKey flag and copy the desired
721  # fields
722  for icSrc, src, d in matches:
723  src.setFlag(self.calibSourceKey, True)
724  # src.assign copies the footprint from icSrc, which we don't want
725  # (DM-407)
726  # so set icSrc's footprint to src's footprint before src.assign,
727  # then restore it
728  icSrcFootprint = icSrc.getFootprint()
729  try:
730  icSrc.setFootprint(src.getFootprint())
731  src.assign(icSrc, self.schemaMapper)
732  finally:
733  icSrc.setFootprint(icSrcFootprint)
def copyIcSourceFields(self, icSourceCat, sourceCat)
Match sources in icSourceCat and sourceCat and copy the specified fields.
Definition: calibrate.py:665
def run(self, exposure, exposureIdInfo=None, background=None, icSourceCat=None)
Calibrate an exposure (science image or coadd)
Definition: calibrate.py:448
def writeOutputs(self, dataRef, exposure, background, sourceCat, astromMatches, matchMeta)
Definition: calibrate.py:604
def __init__(self, butler=None, astromRefObjLoader=None, photoRefObjLoader=None, icSourceSchema=None, kwargs)
Construct a CalibrateTask.
Definition: calibrate.py:280
def setMetadata(self, exposure, photoRes=None)
Set task and exposure metadata.
Definition: calibrate.py:634
def runDataRef(self, dataRef, exposure=None, background=None, icSourceCat=None, doUnpersist=True)
Calibrate an exposure, optionally unpersisting inputs and persisting outputs.
Definition: calibrate.py:384
Calibrate an exposure: measure sources and perform astrometric and photometric calibration.
Definition: calibrate.py:182