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