LSSTApplications  16.0-10-g0ee56ad+5,16.0-11-ga33d1f2+5,16.0-12-g3ef5c14+3,16.0-12-g71e5ef5+18,16.0-12-gbdf3636+3,16.0-13-g118c103+3,16.0-13-g8f68b0a+3,16.0-15-gbf5c1cb+4,16.0-16-gfd17674+3,16.0-17-g7c01f5c+3,16.0-18-g0a50484+1,16.0-20-ga20f992+8,16.0-21-g0e05fd4+6,16.0-21-g15e2d33+4,16.0-22-g62d8060+4,16.0-22-g847a80f+4,16.0-25-gf00d9b8+1,16.0-28-g3990c221+4,16.0-3-gf928089+3,16.0-32-g88a4f23+5,16.0-34-gd7987ad+3,16.0-37-gc7333cb+2,16.0-4-g10fc685+2,16.0-4-g18f3627+26,16.0-4-g5f3a788+26,16.0-5-gaf5c3d7+4,16.0-5-gcc1f4bb+1,16.0-6-g3b92700+4,16.0-6-g4412fcd+3,16.0-6-g7235603+4,16.0-69-g2562ce1b+2,16.0-8-g14ebd58+4,16.0-8-g2df868b+1,16.0-8-g4cec79c+6,16.0-8-gadf6c7a+1,16.0-8-gfc7ad86,16.0-82-g59ec2a54a+1,16.0-9-g5400cdc+2,16.0-9-ge6233d7+5,master-g2880f2d8cf+3,v17.0.rc1
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.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, 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=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_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  def setDefaults(self):
253  super().setDefaults()
254  self.quantum.dimensions = ("Instrument", "Visit", "Detector")
255  # aperture correction should already be measured
256 
257 
258 
264 
265 class CalibrateTask(PipelineTask, pipeBase.CmdLineTask):
266  r"""!Calibrate an exposure: measure sources and perform astrometric and
267  photometric calibration
268 
269  @anchor CalibrateTask_
270 
271  @section pipe_tasks_calibrate_Contents Contents
272 
273  - @ref pipe_tasks_calibrate_Purpose
274  - @ref pipe_tasks_calibrate_Initialize
275  - @ref pipe_tasks_calibrate_IO
276  - @ref pipe_tasks_calibrate_Config
277  - @ref pipe_tasks_calibrate_Metadata
278  - @ref pipe_tasks_calibrate_Debug
279 
280 
281  @section pipe_tasks_calibrate_Purpose Description
282 
283  Given an exposure with a good PSF model and aperture correction map
284  (e.g. as provided by @ref CharacterizeImageTask), perform the following
285  operations:
286  - Run detection and measurement
287  - Run astrometry subtask to fit an improved WCS
288  - Run photoCal subtask to fit the exposure's photometric zero-point
289 
290  @section pipe_tasks_calibrate_Initialize Task initialisation
291 
292  @copydoc \_\_init\_\_
293 
294  @section pipe_tasks_calibrate_IO Invoking the Task
295 
296  If you want this task to unpersist inputs or persist outputs, then call
297  the `runDataRef` method (a wrapper around the `run` method).
298 
299  If you already have the inputs unpersisted and do not want to persist the
300  output then it is more direct to call the `run` method:
301 
302  @section pipe_tasks_calibrate_Config Configuration parameters
303 
304  See @ref CalibrateConfig
305 
306  @section pipe_tasks_calibrate_Metadata Quantities set in exposure Metadata
307 
308  Exposure metadata
309  <dl>
310  <dt>MAGZERO_RMS <dd>MAGZERO's RMS == sigma reported by photoCal task
311  <dt>MAGZERO_NOBJ <dd>Number of stars used == ngood reported by photoCal
312  task
313  <dt>COLORTERM1 <dd>?? (always 0.0)
314  <dt>COLORTERM2 <dd>?? (always 0.0)
315  <dt>COLORTERM3 <dd>?? (always 0.0)
316  </dl>
317 
318  @section pipe_tasks_calibrate_Debug Debug variables
319 
320  The @link lsst.pipe.base.cmdLineTask.CmdLineTask command line task@endlink
321  interface supports a flag
322  `--debug` to import `debug.py` from your `$PYTHONPATH`; see @ref baseDebug
323  for more about `debug.py`.
324 
325  CalibrateTask has a debug dictionary containing one key:
326  <dl>
327  <dt>calibrate
328  <dd>frame (an int; <= 0 to not display) in which to display the exposure,
329  sources and matches. See @ref lsst.meas.astrom.displayAstrometry for
330  the meaning of the various symbols.
331  </dl>
332 
333  For example, put something like:
334  @code{.py}
335  import lsstDebug
336  def DebugInfo(name):
337  di = lsstDebug.getInfo(name) # N.b. lsstDebug.Info(name) would
338  # call us recursively
339  if name == "lsst.pipe.tasks.calibrate":
340  di.display = dict(
341  calibrate = 1,
342  )
343 
344  return di
345 
346  lsstDebug.Info = DebugInfo
347  @endcode
348  into your `debug.py` file and run `calibrateTask.py` with the `--debug`
349  flag.
350 
351  Some subtasks may have their own debug variables; see individual Task
352  documentation.
353  """
354 
355  # Example description used to live here, removed 2-20-2017 as per
356  # https://jira.lsstcorp.org/browse/DM-9520
357 
358  ConfigClass = CalibrateConfig
359  _DefaultName = "calibrate"
360  RunnerClass = pipeBase.ButlerInitializedTaskRunner
361 
362  def __init__(self, butler=None, astromRefObjLoader=None,
363  photoRefObjLoader=None, icSourceSchema=None,
364  initInputs=None, **kwargs):
365  """!Construct a CalibrateTask
366 
367  @param[in] butler The butler is passed to the refObjLoader constructor
368  in case it is needed. Ignored if the refObjLoader argument
369  provides a loader directly.
370  @param[in] astromRefObjLoader An instance of LoadReferenceObjectsTasks
371  that supplies an external reference catalog for astrometric
372  calibration. May be None if the desired loader can be constructed
373  from the butler argument or all steps requiring a reference catalog
374  are disabled.
375  @param[in] photoRefObjLoader An instance of LoadReferenceObjectsTasks
376  that supplies an external reference catalog for photometric
377  calibration. May be None if the desired loader can be constructed
378  from the butler argument or all steps requiring a reference catalog
379  are disabled.
380  @param[in] icSourceSchema schema for icSource catalog, or None.
381  Schema values specified in config.icSourceFieldsToCopy will be
382  taken from this schema. If set to None, no values will be
383  propagated from the icSourceCatalog
384  @param[in,out] kwargs other keyword arguments for
385  lsst.pipe.base.CmdLineTask
386  """
387  super().__init__(**kwargs)
388 
389  if icSourceSchema is None and butler is not None:
390  # Use butler to read icSourceSchema from disk.
391  icSourceSchema = butler.get("icSrc_schema", immediate=True).schema
392 
393  if icSourceSchema is None and butler is None and initInputs is not None:
394  icSourceSchema = initInputs['icSourceSchema'].schema
395 
396  if icSourceSchema is not None:
397  # use a schema mapper to avoid copying each field separately
398  self.schemaMapper = afwTable.SchemaMapper(icSourceSchema)
399  minimumSchema = afwTable.SourceTable.makeMinimalSchema()
400  self.schemaMapper.addMinimalSchema(minimumSchema, False)
401 
402  # Add fields to copy from an icSource catalog
403  # and a field to indicate that the source matched a source in that
404  # catalog. If any fields are missing then raise an exception, but
405  # first find all missing fields in order to make the error message
406  # more useful.
407  self.calibSourceKey = self.schemaMapper.addOutputField(
408  afwTable.Field["Flag"]("calib_detected",
409  "Source was detected as an icSource"))
410  missingFieldNames = []
411  for fieldName in self.config.icSourceFieldsToCopy:
412  try:
413  schemaItem = icSourceSchema.find(fieldName)
414  except Exception:
415  missingFieldNames.append(fieldName)
416  else:
417  # field found; if addMapping fails then raise an exception
418  self.schemaMapper.addMapping(schemaItem.getKey())
419 
420  if missingFieldNames:
421  raise RuntimeError("isSourceCat is missing fields {} "
422  "specified in icSourceFieldsToCopy"
423  .format(missingFieldNames))
424 
425  # produce a temporary schema to pass to the subtasks; finalize it
426  # later
427  self.schema = self.schemaMapper.editOutputSchema()
428  else:
429  self.schemaMapper = None
430  self.schema = afwTable.SourceTable.makeMinimalSchema()
431  self.makeSubtask('detection', schema=self.schema)
432 
434 
435  # Only create a subtask for fakes if configuration option is set
436  # N.B. the config for fake object task must be retargeted to a child
437  # of BaseFakeSourcesTask
438  if self.config.doInsertFakes:
439  self.makeSubtask("insertFakes")
440 
441  if self.config.doDeblend:
442  self.makeSubtask("deblend", schema=self.schema)
443  self.makeSubtask('measurement', schema=self.schema,
444  algMetadata=self.algMetadata)
445  if self.config.doApCorr:
446  self.makeSubtask('applyApCorr', schema=self.schema)
447  self.makeSubtask('catalogCalculation', schema=self.schema)
448 
449  if self.config.doAstrometry:
450  if astromRefObjLoader is None and butler is not None:
451  self.makeSubtask('astromRefObjLoader', butler=butler)
452  astromRefObjLoader = self.astromRefObjLoader
453  self.pixelMargin = astromRefObjLoader.config.pixelMargin
454  self.makeSubtask("astrometry", refObjLoader=astromRefObjLoader,
455  schema=self.schema)
456  if self.config.doPhotoCal:
457  if photoRefObjLoader is None and butler is not None:
458  self.makeSubtask('photoRefObjLoader', butler=butler)
459  photoRefObjLoader = self.photoRefObjLoader
460  self.pixelMargin = photoRefObjLoader.config.pixelMargin
461  # If this is gen 3 don't allow LoadAstometryNet loader. If this is a gen3 run
462  # then initInputs will be populated with data used to initialize the task from
463  # a pipeline activator. This verifies that LoadAstrometryNetObjects task is not
464  # used for photo calibration, and that the reference object loader is actually
465  # None in gen3 world. It will be added in adaptArgsAndRun as a refObjLoader class
466  # object.
467  if initInputs is not None and photoRefObjLoader is LoadAstrometryNetObjectsTask:
468  raise RuntimeError("Astrometry Net tasks are not compatible with gen 3 middleware")
469  self.makeSubtask("photoCal", refObjLoader=photoRefObjLoader,
470  schema=self.schema)
471 
472  if self.schemaMapper is not None:
473  # finalize the schema
474  self.schema = self.schemaMapper.getOutputSchema()
475  self.schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
476 
478  sourceCatSchema = afwTable.SourceCatalog(self.schema)
479  sourceCatSchema.getTable().setMetadata(self.algMetadata)
480  return {'outputSchema': sourceCatSchema}
481 
482  @classmethod
483  def getOutputDatasetTypes(cls, config):
484  outputTypesDict = super().getOutputDatasetTypes(config)
485  if config.doWriteMatches is False:
486  outputTypesDict.pop("matches")
487  if config.doWriteMatchesDenormalized is False:
488  outputTypesDict.pop("matchesDenormalized")
489  return outputTypesDict
490 
491  @pipeBase.timeMethod
492  def runDataRef(self, dataRef, exposure=None, background=None, icSourceCat=None,
493  doUnpersist=True):
494  """!Calibrate an exposure, optionally unpersisting inputs and
495  persisting outputs.
496 
497  This is a wrapper around the `run` method that unpersists inputs
498  (if `doUnpersist` true) and persists outputs (if `config.doWrite` true)
499 
500  @param[in] dataRef butler data reference corresponding to a science
501  image
502  @param[in,out] exposure characterized exposure (an
503  lsst.afw.image.ExposureF or similar), or None to unpersist existing
504  icExp and icBackground. See `run` method for details of what is
505  read and written.
506  @param[in,out] background initial model of background already
507  subtracted from exposure (an lsst.afw.math.BackgroundList). May be
508  None if no background has been subtracted, though that is unusual
509  for calibration. A refined background model is output. Ignored if
510  exposure is None.
511  @param[in] icSourceCat catalog from which to copy the fields specified
512  by icSourceKeys, or None;
513  @param[in] doUnpersist unpersist data:
514  - if True, exposure, background and icSourceCat are read from
515  dataRef and those three arguments must all be None;
516  - if False the exposure must be provided; background and
517  icSourceCat are optional. True is intended for running as a
518  command-line task, False for running as a subtask
519  @return same data as the calibrate method
520  """
521  self.log.info("Processing %s" % (dataRef.dataId))
522 
523  if doUnpersist:
524  if any(item is not None for item in (exposure, background,
525  icSourceCat)):
526  raise RuntimeError("doUnpersist true; exposure, background "
527  "and icSourceCat must all be None")
528  exposure = dataRef.get("icExp", immediate=True)
529  background = dataRef.get("icExpBackground", immediate=True)
530  icSourceCat = dataRef.get("icSrc", immediate=True)
531  elif exposure is None:
532  raise RuntimeError("doUnpersist false; exposure must be provided")
533 
534  exposureIdInfo = dataRef.get("expIdInfo")
535 
536  calRes = self.run(
537  exposure=exposure,
538  exposureIdInfo=exposureIdInfo,
539  background=background,
540  icSourceCat=icSourceCat,
541  )
542 
543  if self.config.doWrite:
544  self.writeOutputs(
545  dataRef=dataRef,
546  exposure=calRes.exposure,
547  background=calRes.background,
548  sourceCat=calRes.sourceCat,
549  astromMatches=calRes.astromMatches,
550  matchMeta=calRes.matchMeta,
551  )
552 
553  return calRes
554 
555  def adaptArgsAndRun(self, inputData, inputDataIds, outputDataIds, butler):
556  expId, expBits = butler.registry.packDataId("VisitDetector",
557  inputDataIds['exposure'],
558  returnMaxBits=True)
559  inputData['exposureIdInfo'] = ExposureIdInfo(expId, expBits)
560 
561  if self.config.doAstrometry:
562  refObjLoader = ReferenceObjectLoader(dataIds=inputDataIds['astromRefCat'],
563  butler=butler,
564  config=self.config.astromRefObjLoader,
565  log=self.log)
566  self.pixelMargin = refObjLoader.config.pixelMargin
567  self.astrometry.setRefObjLoader(refObjLoader)
568 
569  if self.config.doPhotoCal:
570  photoRefObjLoader = ReferenceObjectLoader(inputDataIds['photoRefCat'],
571  butler,
572  self.config.photoRefObjLoader,
573  self.log)
574  self.pixelMargin = photoRefObjLoader.config.pixelMargin
575  self.photoCal.match.setRefObjLoader(photoRefObjLoader)
576 
577  results = self.run(**inputData)
578 
579  if self.config.doWriteMatches:
580  normalizedMatches = afwTable.packMatches(results.astromMatches)
581  normalizedMatches.table.setMetadata(results.matchMeta)
582  if self.config.doWriteMatchesDenormalized:
583  denormMatches = denormalizeMatches(results.astromMatches, results.matchMeta)
584  results.matchesDenormalized = denormMatches
585  results.matches = normalizedMatches
586  return results
587 
588  def run(self, exposure, exposureIdInfo=None, background=None,
589  icSourceCat=None):
590  """!Calibrate an exposure (science image or coadd)
591 
592  @param[in,out] exposure exposure to calibrate (an
593  lsst.afw.image.ExposureF or similar);
594  in:
595  - MaskedImage
596  - Psf
597  out:
598  - MaskedImage has background subtracted
599  - Wcs is replaced
600  - Calib zero-point is set
601  @param[in] exposureIdInfo ID info for exposure (an
602  lsst.obs.base.ExposureIdInfo) If not provided, returned
603  SourceCatalog IDs will not be globally unique.
604  @param[in,out] background background model already subtracted from
605  exposure (an lsst.afw.math.BackgroundList). May be None if no
606  background has been subtracted, though that is unusual for
607  calibration. A refined background model is output.
608  @param[in] icSourceCat A SourceCatalog from CharacterizeImageTask
609  from which we can copy some fields.
610 
611  @return pipe_base Struct containing these fields:
612  - exposure calibrate science exposure with refined WCS and Calib
613  - background model of background subtracted from exposure (an
614  lsst.afw.math.BackgroundList)
615  - sourceCat catalog of measured sources
616  - astromMatches list of source/refObj matches from the astrometry
617  solver
618  """
619  # detect, deblend and measure sources
620  if exposureIdInfo is None:
621  exposureIdInfo = ExposureIdInfo()
622 
623  if background is None:
624  background = BackgroundList()
625  sourceIdFactory = IdFactory.makeSource(exposureIdInfo.expId,
626  exposureIdInfo.unusedBits)
627  table = SourceTable.make(self.schema, sourceIdFactory)
628  table.setMetadata(self.algMetadata)
629 
630  detRes = self.detection.run(table=table, exposure=exposure,
631  doSmooth=True)
632  sourceCat = detRes.sources
633  if detRes.fpSets.background:
634  for bg in detRes.fpSets.background:
635  background.append(bg)
636  if self.config.doDeblend:
637  self.deblend.run(exposure=exposure, sources=sourceCat)
638  self.measurement.run(
639  measCat=sourceCat,
640  exposure=exposure,
641  exposureId=exposureIdInfo.expId
642  )
643  if self.config.doApCorr:
644  self.applyApCorr.run(
645  catalog=sourceCat,
646  apCorrMap=exposure.getInfo().getApCorrMap()
647  )
648  self.catalogCalculation.run(sourceCat)
649 
650  if icSourceCat is not None and \
651  len(self.config.icSourceFieldsToCopy) > 0:
652  self.copyIcSourceFields(icSourceCat=icSourceCat,
653  sourceCat=sourceCat)
654 
655  # TODO DM-11568: this contiguous check-and-copy could go away if we
656  # reserve enough space during SourceDetection and/or SourceDeblend.
657  # NOTE: sourceSelectors require contiguous catalogs, so ensure
658  # contiguity now, so views are preserved from here on.
659  if not sourceCat.isContiguous():
660  sourceCat = sourceCat.copy(deep=True)
661 
662  # perform astrometry calibration:
663  # fit an improved WCS and update the exposure's WCS in place
664  astromMatches = None
665  matchMeta = None
666  if self.config.doAstrometry:
667  try:
668  astromRes = self.astrometry.run(
669  exposure=exposure,
670  sourceCat=sourceCat,
671  )
672  astromMatches = astromRes.matches
673  matchMeta = astromRes.matchMeta
674  except Exception as e:
675  if self.config.requireAstrometry:
676  raise
677  self.log.warn("Unable to perform astrometric calibration "
678  "(%s): attempting to proceed" % e)
679 
680  # compute photometric calibration
681  if self.config.doPhotoCal:
682  try:
683  photoRes = self.photoCal.run(exposure, sourceCat=sourceCat, expId=exposureIdInfo.expId)
684  exposure.getCalib().setFluxMag0(photoRes.calib.getFluxMag0())
685  self.log.info("Photometric zero-point: %f" %
686  photoRes.calib.getMagnitude(1.0))
687  self.setMetadata(exposure=exposure, photoRes=photoRes)
688  except Exception as e:
689  if self.config.requirePhotoCal:
690  raise
691  self.log.warn("Unable to perform photometric calibration "
692  "(%s): attempting to proceed" % e)
693  self.setMetadata(exposure=exposure, photoRes=None)
694 
695  if self.config.doInsertFakes:
696  self.insertFakes.run(exposure, background=background)
697 
698  table = SourceTable.make(self.schema, sourceIdFactory)
699  table.setMetadata(self.algMetadata)
700 
701  detRes = self.detection.run(table=table, exposure=exposure,
702  doSmooth=True)
703  sourceCat = detRes.sources
704  if detRes.fpSets.background:
705  for bg in detRes.fpSets.background:
706  background.append(bg)
707  if self.config.doDeblend:
708  self.deblend.run(exposure=exposure, sources=sourceCat)
709  self.measurement.run(
710  measCat=sourceCat,
711  exposure=exposure,
712  exposureId=exposureIdInfo.expId
713  )
714  if self.config.doApCorr:
715  self.applyApCorr.run(
716  catalog=sourceCat,
717  apCorrMap=exposure.getInfo().getApCorrMap()
718  )
719  self.catalogCalculation.run(sourceCat)
720 
721  if icSourceCat is not None and \
722  len(self.config.icSourceFieldsToCopy) > 0:
723  self.copyIcSourceFields(icSourceCat=icSourceCat,
724  sourceCat=sourceCat)
725 
726  frame = getDebugFrame(self._display, "calibrate")
727  if frame:
729  sourceCat=sourceCat,
730  exposure=exposure,
731  matches=astromMatches,
732  frame=frame,
733  pause=False,
734  )
735 
736  return pipeBase.Struct(
737  exposure=exposure,
738  background=background,
739  sourceCat=sourceCat,
740  astromMatches=astromMatches,
741  matchMeta=matchMeta,
742  # These are duplicate entries with different names for use with
743  # gen3 middleware
744  outputExposure=exposure,
745  outputCat=sourceCat,
746  outputBackground=background,
747  )
748 
749  def writeOutputs(self, dataRef, exposure, background, sourceCat,
750  astromMatches, matchMeta):
751  """Write output data to the output repository
752 
753  @param[in] dataRef butler data reference corresponding to a science
754  image
755  @param[in] exposure exposure to write
756  @param[in] background background model for exposure
757  @param[in] sourceCat catalog of measured sources
758  @param[in] astromMatches list of source/refObj matches from the
759  astrometry solver
760  """
761  dataRef.put(sourceCat, "src")
762  if self.config.doWriteMatches and astromMatches is not None:
763  normalizedMatches = afwTable.packMatches(astromMatches)
764  normalizedMatches.table.setMetadata(matchMeta)
765  dataRef.put(normalizedMatches, "srcMatch")
766  if self.config.doWriteMatchesDenormalized:
767  denormMatches = denormalizeMatches(astromMatches, matchMeta)
768  dataRef.put(denormMatches, "srcMatchFull")
769  dataRef.put(exposure, "calexp")
770  dataRef.put(background, "calexpBackground")
771 
772  def getSchemaCatalogs(self):
773  """Return a dict of empty catalogs for each catalog dataset produced
774  by this task.
775  """
776  sourceCat = afwTable.SourceCatalog(self.schema)
777  sourceCat.getTable().setMetadata(self.algMetadata)
778  return {"src": sourceCat}
779 
780  def setMetadata(self, exposure, photoRes=None):
781  """!Set task and exposure metadata
782 
783  Logs a warning and continues if needed data is missing.
784 
785  @param[in,out] exposure exposure whose metadata is to be set
786  @param[in] photoRes results of running photoCal; if None then it was
787  not run
788  """
789  if photoRes is None:
790  return
791 
792  # convert zero-point to (mag/sec/adu) for task MAGZERO metadata
793  try:
794  exposureTime = exposure.getInfo().getVisitInfo().getExposureTime()
795  magZero = photoRes.zp - 2.5*math.log10(exposureTime)
796  self.metadata.set('MAGZERO', magZero)
797  except Exception:
798  self.log.warn("Could not set normalized MAGZERO in header: no "
799  "exposure time")
800 
801  try:
802  metadata = exposure.getMetadata()
803  metadata.set('MAGZERO_RMS', photoRes.sigma)
804  metadata.set('MAGZERO_NOBJ', photoRes.ngood)
805  metadata.set('COLORTERM1', 0.0)
806  metadata.set('COLORTERM2', 0.0)
807  metadata.set('COLORTERM3', 0.0)
808  except Exception as e:
809  self.log.warn("Could not set exposure metadata: %s" % (e,))
810 
811  def copyIcSourceFields(self, icSourceCat, sourceCat):
812  """!Match sources in icSourceCat and sourceCat and copy the specified fields
813 
814  @param[in] icSourceCat catalog from which to copy fields
815  @param[in,out] sourceCat catalog to which to copy fields
816 
817  The fields copied are those specified by `config.icSourceFieldsToCopy`
818  that actually exist in the schema. This was set up by the constructor
819  using self.schemaMapper.
820  """
821  if self.schemaMapper is None:
822  raise RuntimeError("To copy icSource fields you must specify "
823  "icSourceSchema nd icSourceKeys when "
824  "constructing this task")
825  if icSourceCat is None or sourceCat is None:
826  raise RuntimeError("icSourceCat and sourceCat must both be "
827  "specified")
828  if len(self.config.icSourceFieldsToCopy) == 0:
829  self.log.warn("copyIcSourceFields doing nothing because "
830  "icSourceFieldsToCopy is empty")
831  return
832 
833  mc = afwTable.MatchControl()
834  mc.findOnlyClosest = False # return all matched objects
835  matches = afwTable.matchXy(icSourceCat, sourceCat,
836  self.config.matchRadiusPix, mc)
837  if self.config.doDeblend:
838  deblendKey = sourceCat.schema["deblend_nChild"].asKey()
839  # if deblended, keep children
840  matches = [m for m in matches if m[1].get(deblendKey) == 0]
841 
842  # Because we had to allow multiple matches to handle parents, we now
843  # need to prune to the best matches
844  # closest matches as a dict of icSourceCat source ID:
845  # (icSourceCat source, sourceCat source, distance in pixels)
846  bestMatches = {}
847  for m0, m1, d in matches:
848  id0 = m0.getId()
849  match = bestMatches.get(id0)
850  if match is None or d <= match[2]:
851  bestMatches[id0] = (m0, m1, d)
852  matches = list(bestMatches.values())
853 
854  # Check that no sourceCat sources are listed twice (we already know
855  # that each match has a unique icSourceCat source ID, due to using
856  # that ID as the key in bestMatches)
857  numMatches = len(matches)
858  numUniqueSources = len(set(m[1].getId() for m in matches))
859  if numUniqueSources != numMatches:
860  self.log.warn("{} icSourceCat sources matched only {} sourceCat "
861  "sources".format(numMatches, numUniqueSources))
862 
863  self.log.info("Copying flags from icSourceCat to sourceCat for "
864  "%s sources" % (numMatches,))
865 
866  # For each match: set the calibSourceKey flag and copy the desired
867  # fields
868  for icSrc, src, d in matches:
869  src.setFlag(self.calibSourceKey, True)
870  # src.assign copies the footprint from icSrc, which we don't want
871  # (DM-407)
872  # so set icSrc's footprint to src's footprint before src.assign,
873  # then restore it
874  icSrcFootprint = icSrc.getFootprint()
875  try:
876  icSrc.setFootprint(src.getFootprint())
877  src.assign(icSrc, self.schemaMapper)
878  finally:
879  icSrc.setFootprint(icSrcFootprint)
def copyIcSourceFields(self, icSourceCat, sourceCat)
Match sources in icSourceCat and sourceCat and copy the specified fields.
Definition: calibrate.py:811
def __init__(self, butler=None, astromRefObjLoader=None, photoRefObjLoader=None, icSourceSchema=None, initInputs=None, kwargs)
Construct a CalibrateTask.
Definition: calibrate.py:364
def run(self, exposure, exposureIdInfo=None, background=None, icSourceCat=None)
Calibrate an exposure (science image or coadd)
Definition: calibrate.py:589
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:832
def writeOutputs(self, dataRef, exposure, background, sourceCat, astromMatches, matchMeta)
Definition: calibrate.py:750
bool any(CoordinateExpr< N > const &expr) noexcept
Return true if any elements are true.
def adaptArgsAndRun(self, inputData, inputDataIds, outputDataIds, butler)
Definition: calibrate.py:555
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:780
def displayAstrometry(refCat=None, sourceCat=None, distortedCentroidKey=None, bbox=None, exposure=None, matches=None, frame=1, title="", pause=True)
Definition: display.py:35
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:493
daf::base::PropertyList * list
Definition: fits.cc:833
Calibrate an exposure: measure sources and perform astrometric and photometric calibration.
Definition: calibrate.py:265