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