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