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