lsst.pipe.tasks  13.0-57-geba51f8
 All Classes Namespaces Files Functions Variables Groups Pages
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 from __future__ import absolute_import, division, print_function
23 import math
24 
25 from lsstDebug import getDebugFrame
26 import lsst.pex.config as pexConfig
27 import lsst.pipe.base as pipeBase
28 import lsst.afw.table as afwTable
29 from lsst.meas.astrom import (AstrometryTask, displayAstrometry,
30  createMatchMetadata, denormalizeMatches)
31 from lsst.meas.extensions.astrometryNet import LoadAstrometryNetObjectsTask
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
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(pexConfig.Config):
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=LoadAstrometryNetObjectsTask,
78  doc="reference object loader for astrometric calibration",
79  )
80  photoRefObjLoader = pexConfig.ConfigurableField(
81  target=LoadAstrometryNetObjectsTask,
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_psfCandidate", "calib_psfUsed", "calib_psfReserved"),
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 
172  def setDefaults(self):
173  pexConfig.Config.setDefaults(self)
174  # aperture correction should already be measured
175 
176 
177 ## \addtogroup LSST_task_documentation
178 ## \{
179 ## \page CalibrateTask
180 ## \ref CalibrateTask_ "CalibrateTask"
181 ## \copybrief CalibrateTask
182 ## \}
183 
184 class CalibrateTask(pipeBase.CmdLineTask):
185  """!Calibrate an exposure: measure sources and perform astrometric and
186  photometric calibration
187 
188  @anchor CalibrateTask_
189 
190  @section pipe_tasks_calibrate_Contents Contents
191 
192  - @ref pipe_tasks_calibrate_Purpose
193  - @ref pipe_tasks_calibrate_Initialize
194  - @ref pipe_tasks_calibrate_IO
195  - @ref pipe_tasks_calibrate_Config
196  - @ref pipe_tasks_calibrate_Metadata
197  - @ref pipe_tasks_calibrate_Debug
198 
199 
200  @section pipe_tasks_calibrate_Purpose Description
201 
202  Given an exposure with a good PSF model and aperture correction map
203  (e.g. as provided by @ref CharacterizeImageTask), perform the following
204  operations:
205  - Run detection and measurement
206  - Run astrometry subtask to fit an improved WCS
207  - Run photoCal subtask to fit the exposure's photometric zero-point
208 
209  @section pipe_tasks_calibrate_Initialize Task initialisation
210 
211  @copydoc \_\_init\_\_
212 
213  @section pipe_tasks_calibrate_IO Invoking the Task
214 
215  If you want this task to unpersist inputs or persist outputs, then call
216  the `run` method (a wrapper around the `calibrate` method).
217 
218  If you already have the inputs unpersisted and do not want to persist the
219  output then it is more direct to call the `calibrate` method:
220 
221  @section pipe_tasks_calibrate_Config Configuration parameters
222 
223  See @ref CalibrateConfig
224 
225  @section pipe_tasks_calibrate_Metadata Quantities set in exposure Metadata
226 
227  Exposure metadata
228  <dl>
229  <dt>MAGZERO_RMS <dd>MAGZERO's RMS == sigma reported by photoCal task
230  <dt>MAGZERO_NOBJ <dd>Number of stars used == ngood reported by photoCal
231  task
232  <dt>COLORTERM1 <dd>?? (always 0.0)
233  <dt>COLORTERM2 <dd>?? (always 0.0)
234  <dt>COLORTERM3 <dd>?? (always 0.0)
235  </dl>
236 
237  @section pipe_tasks_calibrate_Debug Debug variables
238 
239  The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink
240  interface supports a flag
241  `--debug` to import `debug.py` from your `$PYTHONPATH`; see @ref baseDebug
242  for more about `debug.py`.
243 
244  CalibrateTask has a debug dictionary containing one key:
245  <dl>
246  <dt>calibrate
247  <dd>frame (an int; <= 0 to not display) in which to display the exposure,
248  sources and matches. See @ref lsst.meas.astrom.displayAstrometry for
249  the meaning of the various symbols.
250  </dl>
251 
252  For example, put something like:
253  @code{.py}
254  import lsstDebug
255  def DebugInfo(name):
256  di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would
257  # call us recursively
258  if name == "lsst.pipe.tasks.calibrate":
259  di.display = dict(
260  calibrate = 1,
261  )
262 
263  return di
264 
265  lsstDebug.Info = DebugInfo
266  @endcode
267  into your `debug.py` file and run `calibrateTask.py` with the `--debug`
268  flag.
269 
270  Some subtasks may have their own debug variables; see individual Task
271  documentation.
272  """
273 
274  # Example description used to live here, removed 2-20-2017 as per
275  # https://jira.lsstcorp.org/browse/DM-9520
276 
277  ConfigClass = CalibrateConfig
278  _DefaultName = "calibrate"
279  RunnerClass = pipeBase.ButlerInitializedTaskRunner
280 
281  def __init__(self, butler=None, astromRefObjLoader=None,
282  photoRefObjLoader=None, icSourceSchema=None, **kwargs):
283  """!Construct a CalibrateTask
284 
285  @param[in] butler The butler is passed to the refObjLoader constructor
286  in case it is needed. Ignored if the refObjLoader argument
287  provides a loader directly.
288  @param[in] astromRefObjLoader An instance of LoadReferenceObjectsTasks
289  that supplies an external reference catalog for astrometric
290  calibration. May be None if the desired loader can be constructed
291  from the butler argument or all steps requiring a reference catalog
292  are disabled.
293  @param[in] photoRefObjLoader An instance of LoadReferenceObjectsTasks
294  that supplies an external reference catalog for photometric
295  calibration. May be None if the desired loader can be constructed
296  from the butler argument or all steps requiring a reference catalog
297  are disabled.
298  @param[in] icSourceSchema schema for icSource catalog, or None.
299  Schema values specified in config.icSourceFieldsToCopy will be
300  taken from this schema. If set to None, no values will be
301  propagated from the icSourceCatalog
302  @param[in,out] kwargs other keyword arguments for
303  lsst.pipe.base.CmdLineTask
304  """
305  pipeBase.CmdLineTask.__init__(self, **kwargs)
306 
307  if icSourceSchema is None and butler is not None:
308  # Use butler to read icSourceSchema from disk.
309  icSourceSchema = butler.get("icSrc_schema", immediate=True).schema
310 
311  if icSourceSchema is not None:
312  # use a schema mapper to avoid copying each field separately
313  self.schemaMapper = afwTable.SchemaMapper(icSourceSchema)
314  minimumSchema = afwTable.SourceTable.makeMinimalSchema()
315  self.schemaMapper.addMinimalSchema(minimumSchema, False)
316 
317  # Add fields to copy from an icSource catalog
318  # and a field to indicate that the source matched a source in that
319  # catalog. If any fields are missing then raise an exception, but
320  # first find all missing fields in order to make the error message
321  # more useful.
322  self.calibSourceKey = self.schemaMapper.addOutputField(
323  afwTable.Field["Flag"]("calib_detected",
324  "Source was detected as an icSource"))
325  missingFieldNames = []
326  for fieldName in self.config.icSourceFieldsToCopy:
327  try:
328  schemaItem = icSourceSchema.find(fieldName)
329  except Exception:
330  missingFieldNames.append(fieldName)
331  else:
332  # field found; if addMapping fails then raise an exception
333  self.schemaMapper.addMapping(schemaItem.getKey())
334 
335  if missingFieldNames:
336  raise RuntimeError("isSourceCat is missing fields {} "
337  "specified in icSourceFieldsToCopy"
338  .format(missingFieldNames))
339 
340  # produce a temporary schema to pass to the subtasks; finalize it
341  # later
342  self.schema = self.schemaMapper.editOutputSchema()
343  else:
344  self.schemaMapper = None
345  self.schema = afwTable.SourceTable.makeMinimalSchema()
346  self.makeSubtask('detection', schema=self.schema)
347 
348  self.algMetadata = dafBase.PropertyList()
349 
350  # Only create a subtask for fakes if configuration option is set
351  # N.B. the config for fake object task must be retargeted to a child
352  # of BaseFakeSourcesTask
353  if self.config.doInsertFakes:
354  self.makeSubtask("insertFakes")
355 
356  if self.config.doDeblend:
357  self.makeSubtask("deblend", schema=self.schema)
358  self.makeSubtask('measurement', schema=self.schema,
359  algMetadata=self.algMetadata)
360  if self.config.doApCorr:
361  self.makeSubtask('applyApCorr', schema=self.schema)
362  self.makeSubtask('catalogCalculation', schema=self.schema)
363 
364  if self.config.doAstrometry:
365  if astromRefObjLoader is None:
366  self.makeSubtask('astromRefObjLoader', butler=butler)
367  astromRefObjLoader = self.astromRefObjLoader
368  self.pixelMargin = astromRefObjLoader.config.pixelMargin
369  self.makeSubtask("astrometry", refObjLoader=astromRefObjLoader,
370  schema=self.schema)
371  if self.config.doPhotoCal:
372  if photoRefObjLoader is None:
373  self.makeSubtask('photoRefObjLoader', butler=butler)
374  photoRefObjLoader = self.photoRefObjLoader
375  self.pixelMargin = photoRefObjLoader.config.pixelMargin
376  self.makeSubtask("photoCal", refObjLoader=photoRefObjLoader,
377  schema=self.schema)
378 
379  if self.schemaMapper is not None:
380  # finalize the schema
381  self.schema = self.schemaMapper.getOutputSchema()
382  self.schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
383 
384  @pipeBase.timeMethod
385  def run(self, dataRef, exposure=None, background=None, icSourceCat=None,
386  doUnpersist=True):
387  """!Calibrate an exposure, optionally unpersisting inputs and
388  persisting outputs.
389 
390  This is a wrapper around the `calibrate` method that unpersists inputs
391  (if `doUnpersist` true) and persists outputs (if `config.doWrite` true)
392 
393  @param[in] dataRef butler data reference corresponding to a science
394  image
395  @param[in,out] exposure characterized exposure (an
396  lsst.afw.image.ExposureF or similar), or None to unpersist existing
397  icExp and icBackground. See calibrate method for details of what is
398  read and written.
399  @param[in,out] background initial model of background already
400  subtracted from exposure (an lsst.afw.math.BackgroundList). May be
401  None if no background has been subtracted, though that is unusual
402  for calibration. A refined background model is output. Ignored if
403  exposure is None.
404  @param[in] icSourceCat catalog from which to copy the fields specified
405  by icSourceKeys, or None;
406  @param[in] doUnpersist unpersist data:
407  - if True, exposure, background and icSourceCat are read from
408  dataRef and those three arguments must all be None;
409  - if False the exposure must be provided; background and
410  icSourceCat are optional. True is intended for running as a
411  command-line task, False for running as a subtask
412 
413  @return same data as the calibrate method
414  """
415  self.log.info("Processing %s" % (dataRef.dataId))
416 
417  if doUnpersist:
418  if any(item is not None for item in (exposure, background,
419  icSourceCat)):
420  raise RuntimeError("doUnpersist true; exposure, background "
421  "and icSourceCat must all be None")
422  exposure = dataRef.get("icExp", immediate=True)
423  background = dataRef.get("icExpBackground", immediate=True)
424  icSourceCat = dataRef.get("icSrc", immediate=True)
425  elif exposure is None:
426  raise RuntimeError("doUnpersist false; exposure must be provided")
427 
428  if self.config.doWrite and self.config.doAstrometry:
429  matchMeta = createMatchMetadata(exposure, border=self.pixelMargin)
430  else:
431  matchMeta = None
432 
433  exposureIdInfo = dataRef.get("expIdInfo")
434 
435  calRes = self.calibrate(
436  exposure=exposure,
437  exposureIdInfo=exposureIdInfo,
438  background=background,
439  icSourceCat=icSourceCat,
440  )
441 
442  if self.config.doWrite:
443  self.writeOutputs(
444  dataRef=dataRef,
445  exposure=calRes.exposure,
446  background=calRes.background,
447  sourceCat=calRes.sourceCat,
448  astromMatches=calRes.astromMatches,
449  matchMeta=matchMeta,
450  )
451 
452  return calRes
453 
454  def calibrate(self, exposure, exposureIdInfo=None, background=None,
455  icSourceCat=None):
456  """!Calibrate an exposure (science image or coadd)
457 
458  @param[in,out] exposure exposure to calibrate (an
459  lsst.afw.image.ExposureF or similar);
460  in:
461  - MaskedImage
462  - Psf
463  out:
464  - MaskedImage has background subtracted
465  - Wcs is replaced
466  - Calib zero-point is set
467  @param[in] exposureIdInfo ID info for exposure (an
468  lsst.obs.base.ExposureIdInfo) If not provided, returned
469  SourceCatalog IDs will not be globally unique.
470  @param[in,out] background background model already subtracted from
471  exposure (an lsst.afw.math.BackgroundList). May be None if no
472  background has been subtracted, though that is unusual for
473  calibration. A refined background model is output.
474  @param[in] icSourceCat A SourceCatalog from CharacterizeImageTask
475  from which we can copy some fields.
476 
477  @return pipe_base Struct containing these fields:
478  - exposure calibrate science exposure with refined WCS and Calib
479  - background model of background subtracted from exposure (an
480  lsst.afw.math.BackgroundList)
481  - sourceCat catalog of measured sources
482  - astromMatches list of source/refObj matches from the astrometry
483  solver
484  """
485  # detect, deblend and measure sources
486  if exposureIdInfo is None:
487  exposureIdInfo = ExposureIdInfo()
488 
489  if background is None:
490  background = BackgroundList()
491  sourceIdFactory = IdFactory.makeSource(exposureIdInfo.expId,
492  exposureIdInfo.unusedBits)
493  table = SourceTable.make(self.schema, sourceIdFactory)
494  table.setMetadata(self.algMetadata)
495 
496  if self.config.doInsertFakes:
497  self.insertFakes.run(exposure, background=background)
498 
499  detRes = self.detection.run(table=table, exposure=exposure,
500  doSmooth=True)
501  sourceCat = detRes.sources
502  if detRes.fpSets.background:
503  background.append(detRes.fpSets.background)
504  if self.config.doDeblend:
505  self.deblend.run(exposure=exposure, sources=sourceCat)
506  self.measurement.run(
507  measCat=sourceCat,
508  exposure=exposure,
509  exposureId=exposureIdInfo.expId
510  )
511  if self.config.doApCorr:
512  self.applyApCorr.run(
513  catalog=sourceCat,
514  apCorrMap=exposure.getInfo().getApCorrMap()
515  )
516  self.catalogCalculation.run(sourceCat)
517 
518  if icSourceCat is not None and \
519  len(self.config.icSourceFieldsToCopy) > 0:
520  self.copyIcSourceFields(icSourceCat=icSourceCat,
521  sourceCat=sourceCat)
522 
523  # perform astrometry calibration:
524  # fit an improved WCS and update the exposure's WCS in place
525  astromMatches = None
526  if self.config.doAstrometry:
527  try:
528  astromRes = self.astrometry.run(
529  exposure=exposure,
530  sourceCat=sourceCat,
531  )
532  astromMatches = astromRes.matches
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)
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  frame = getDebugFrame(self._display, "calibrate")
555  if frame:
556  displayAstrometry(
557  sourceCat=sourceCat,
558  exposure=exposure,
559  matches=astromMatches,
560  frame=frame,
561  pause=False,
562  )
563 
564  return pipeBase.Struct(
565  exposure=exposure,
566  background=background,
567  sourceCat=sourceCat,
568  astromMatches=astromMatches,
569  )
570 
571  def writeOutputs(self, dataRef, exposure, background, sourceCat,
572  astromMatches, matchMeta):
573  """Write output data to the output repository
574 
575  @param[in] dataRef butler data reference corresponding to a science
576  image
577  @param[in] exposure exposure to write
578  @param[in] background background model for exposure
579  @param[in] sourceCat catalog of measured sources
580  @param[in] astromMatches list of source/refObj matches from the
581  astrometry solver
582  """
583  sourceWriteFlags = 0 if self.config.doWriteHeavyFootprintsInSources \
584  else afwTable.SOURCE_IO_NO_HEAVY_FOOTPRINTS
585  dataRef.put(sourceCat, "src")
586  if self.config.doWriteMatches and astromMatches is not None:
587  normalizedMatches = afwTable.packMatches(astromMatches)
588  normalizedMatches.table.setMetadata(matchMeta)
589  dataRef.put(normalizedMatches, "srcMatch")
590  if self.config.doWriteMatchesDenormalized:
591  denormMatches = denormalizeMatches(astromMatches, matchMeta)
592  dataRef.put(denormMatches, "srcMatchFull")
593  dataRef.put(exposure, "calexp")
594  dataRef.put(background, "calexpBackground")
595 
596  def getSchemaCatalogs(self):
597  """Return a dict of empty catalogs for each catalog dataset produced
598  by this task.
599  """
600  sourceCat = afwTable.SourceCatalog(self.schema)
601  sourceCat.getTable().setMetadata(self.algMetadata)
602  return {"src": sourceCat}
603 
604  def setMetadata(self, exposure, photoRes=None):
605  """!Set task and exposure metadata
606 
607  Logs a warning and continues if needed data is missing.
608 
609  @param[in,out] exposure exposure whose metadata is to be set
610  @param[in] photoRes results of running photoCal; if None then it was
611  not run
612  """
613  if photoRes is None:
614  return
615 
616  # convert zero-point to (mag/sec/adu) for task MAGZERO metadata
617  try:
618  exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
619  magZero = photoRes.zp - 2.5*math.log10(exposureTime)
620  self.metadata.set('MAGZERO', magZero)
621  except Exception:
622  self.log.warn("Could not set normalized MAGZERO in header: no "
623  "exposure time")
624 
625  try:
626  metadata = exposure.getMetadata()
627  metadata.set('MAGZERO_RMS', photoRes.sigma)
628  metadata.set('MAGZERO_NOBJ', photoRes.ngood)
629  metadata.set('COLORTERM1', 0.0)
630  metadata.set('COLORTERM2', 0.0)
631  metadata.set('COLORTERM3', 0.0)
632  except Exception as e:
633  self.log.warn("Could not set exposure metadata: %s" % (e,))
634 
635  def copyIcSourceFields(self, icSourceCat, sourceCat):
636  """!Match sources in icSourceCat and sourceCat and copy the specified fields
637 
638  @param[in] icSourceCat catalog from which to copy fields
639  @param[in,out] sourceCat catalog to which to copy fields
640 
641  The fields copied are those specified by `config.icSourceFieldsToCopy`
642  that actually exist in the schema. This was set up by the constructor
643  using self.schemaMapper.
644  """
645  if self.schemaMapper is None:
646  raise RuntimeError("To copy icSource fields you must specify "
647  "icSourceSchema nd icSourceKeys when "
648  "constructing this task")
649  if icSourceCat is None or sourceCat is None:
650  raise RuntimeError("icSourceCat and sourceCat must both be "
651  "specified")
652  if len(self.config.icSourceFieldsToCopy) == 0:
653  self.log.warn("copyIcSourceFields doing nothing because "
654  "icSourceFieldsToCopy is empty")
655  return
656 
657  mc = afwTable.MatchControl()
658  mc.findOnlyClosest = False # return all matched objects
659  matches = afwTable.matchXy(icSourceCat, sourceCat,
660  self.config.matchRadiusPix, mc)
661  if self.config.doDeblend:
662  deblendKey = sourceCat.schema["deblend_nChild"].asKey()
663  # if deblended, keep children
664  matches = [m for m in matches if m[1].get(deblendKey) == 0]
665 
666  # Because we had to allow multiple matches to handle parents, we now
667  # need to prune to the best matches
668  # closest matches as a dict of icSourceCat source ID:
669  # (icSourceCat source, sourceCat source, distance in pixels)
670  bestMatches = {}
671  for m0, m1, d in matches:
672  id0 = m0.getId()
673  match = bestMatches.get(id0)
674  if match is None or d <= match[2]:
675  bestMatches[id0] = (m0, m1, d)
676  matches = list(bestMatches.values())
677 
678  # Check that no sourceCat sources are listed twice (we already know
679  # that each match has a unique icSourceCat source ID, due to using
680  # that ID as the key in bestMatches)
681  numMatches = len(matches)
682  numUniqueSources = len(set(m[1].getId() for m in matches))
683  if numUniqueSources != numMatches:
684  self.log.warn("{} icSourceCat sources matched only {} sourceCat "
685  "sources".format(numMatches, numUniqueSources))
686 
687  self.log.info("Copying flags from icSourceCat to sourceCat for "
688  "%s sources" % (numMatches,))
689 
690  # For each match: set the calibSourceKey flag and copy the desired
691  # fields
692  for icSrc, src, d in matches:
693  src.setFlag(self.calibSourceKey, True)
694  # src.assign copies the footprint from icSrc, which we don't want
695  # (DM-407)
696  # so set icSrc's footprint to src's footprint before src.assign,
697  # then restore it
698  icSrcFootprint = icSrc.getFootprint()
699  try:
700  icSrc.setFootprint(src.getFootprint())
701  src.assign(icSrc, self.schemaMapper)
702  finally:
703  icSrc.setFootprint(icSrcFootprint)
def __init__
Construct a CalibrateTask.
Definition: calibrate.py:282
def setMetadata
Set task and exposure metadata.
Definition: calibrate.py:604
def calibrate
Calibrate an exposure (science image or coadd)
Definition: calibrate.py:455
def copyIcSourceFields
Match sources in icSourceCat and sourceCat and copy the specified fields.
Definition: calibrate.py:635
def run
Calibrate an exposure, optionally unpersisting inputs and persisting outputs.
Definition: calibrate.py:386
Calibrate an exposure: measure sources and perform astrometric and photometric calibration.
Definition: calibrate.py:184