LSSTApplications  11.0-13-gbb96280,12.1+18,12.1+7,12.1-1-g14f38d3+72,12.1-1-g16c0db7+5,12.1-1-g5961e7a+84,12.1-1-ge22e12b+23,12.1-11-g06625e2+4,12.1-11-g0d7f63b+4,12.1-19-gd507bfc,12.1-2-g7dda0ab+38,12.1-2-gc0bc6ab+81,12.1-21-g6ffe579+2,12.1-21-gbdb6c2a+4,12.1-24-g941c398+5,12.1-3-g57f6835+7,12.1-3-gf0736f3,12.1-37-g3ddd237,12.1-4-gf46015e+5,12.1-5-g06c326c+20,12.1-5-g648ee80+3,12.1-5-gc2189d7+4,12.1-6-ga608fc0+1,12.1-7-g3349e2a+5,12.1-7-gfd75620+9,12.1-9-g577b946+5,12.1-9-gc4df26a+10
LSSTDataManagementBasePackage
multiBand.py
Go to the documentation of this file.
1 from builtins import zip
2 from builtins import range
3 #!/usr/bin/env python
4 #
5 # LSST Data Management System
6 # Copyright 2008-2015 AURA/LSST.
7 #
8 # This product includes software developed by the
9 # LSST Project (http://www.lsst.org/).
10 #
11 # This program is free software: you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation, either version 3 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the LSST License Statement and
22 # the GNU General Public License along with this program. If not,
23 # see <https://www.lsstcorp.org/LegalNotices/>.
24 #
25 import numpy
26 
27 from lsst.coadd.utils.coaddDataIdContainer import ExistingCoaddDataIdContainer
28 from lsst.pipe.base import CmdLineTask, Struct, TaskRunner, ArgumentParser, ButlerInitializedTaskRunner
29 from lsst.pex.config import Config, Field, ListField, ConfigurableField, RangeField, ConfigField
30 from lsst.meas.algorithms import SourceDetectionTask
31 from lsst.meas.base import SingleFrameMeasurementTask, ApplyApCorrTask, CatalogCalculationTask
32 from lsst.meas.deblender import SourceDeblendTask
33 from lsst.pipe.tasks.coaddBase import getSkyInfo, scaleVariance
34 from lsst.meas.astrom import DirectMatchTask
35 from lsst.pipe.tasks.setPrimaryFlags import SetPrimaryFlagsTask
36 from lsst.pipe.tasks.propagateVisitFlags import PropagateVisitFlagsTask
37 import lsst.afw.image as afwImage
38 import lsst.afw.table as afwTable
39 import lsst.afw.math as afwMath
40 import lsst.afw.geom as afwGeom
41 import lsst.afw.detection as afwDetect
42 from lsst.daf.base import PropertyList
43 
44 """
45 New dataset types:
46 * deepCoadd_det: detections from what used to be processCoadd (tract, patch, filter)
47 * deepCoadd_mergeDet: merged detections (tract, patch)
48 * deepCoadd_meas: measurements of merged detections (tract, patch, filter)
49 * deepCoadd_ref: reference sources (tract, patch)
50 All of these have associated *_schema catalogs that require no data ID and hold no records.
51 
52 In addition, we have a schema-only dataset, which saves the schema for the PeakRecords in
53 the mergeDet, meas, and ref dataset Footprints:
54 * deepCoadd_peak_schema
55 """
56 
57 
58 def _makeGetSchemaCatalogs(datasetSuffix):
59  """Construct a getSchemaCatalogs instance method
60 
61  These are identical for most of the classes here, so we'll consolidate
62  the code.
63 
64  datasetSuffix: Suffix of dataset name, e.g., "src" for "deepCoadd_src"
65  """
66 
67  def getSchemaCatalogs(self):
68  """Return a dict of empty catalogs for each catalog dataset produced by this task."""
69  src = afwTable.SourceCatalog(self.schema)
70  if hasattr(self, "algMetadata"):
71  src.getTable().setMetadata(self.algMetadata)
72  return {self.config.coaddName + "Coadd_" + datasetSuffix: src}
73  return getSchemaCatalogs
74 
75 
76 def _makeMakeIdFactory(datasetName):
77  """Construct a makeIdFactory instance method
78 
79  These are identical for all the classes here, so this consolidates
80  the code.
81 
82  datasetName: Dataset name without the coadd name prefix, e.g., "CoaddId" for "deepCoaddId"
83  """
84 
85  def makeIdFactory(self, dataRef):
86  """Return an IdFactory for setting the detection identifiers
87 
88  The actual parameters used in the IdFactory are provided by
89  the butler (through the provided data reference.
90  """
91  expBits = dataRef.get(self.config.coaddName + datasetName + "_bits")
92  expId = int(dataRef.get(self.config.coaddName + datasetName))
93  return afwTable.IdFactory.makeSource(expId, 64 - expBits)
94  return makeIdFactory
95 
96 
98  """Given a longer, camera-specific filter name (e.g. "HSC-I") return its shorthand name ("i").
99  """
100  # I'm not sure if this is the way this is supposed to be implemented, but it seems to work,
101  # and its the only way I could get it to work.
102  return afwImage.Filter(name).getFilterProperty().getName()
103 
104 
105 ##############################################################################################################
106 
108  """!
109  \anchor DetectCoaddSourcesConfig_
110 
111  \brief Configuration parameters for the DetectCoaddSourcesTask
112  """
113  doScaleVariance = Field(dtype=bool, default=True, doc="Scale variance plane using empirical noise?")
114  detection = ConfigurableField(target=SourceDetectionTask, doc="Source detection")
115  coaddName = Field(dtype=str, default="deep", doc="Name of coadd")
116  mask = ListField(dtype=str, default=["DETECTED", "BAD", "SAT", "NO_DATA", "INTRP"],
117  doc="Mask planes for pixels to ignore when scaling variance")
118 
119  def setDefaults(self):
120  Config.setDefaults(self)
121  self.detection.thresholdType = "pixel_stdev"
122  self.detection.isotropicGrow = True
123  # Coadds are made from background-subtracted CCDs, so background subtraction should be very basic
124  self.detection.background.useApprox = False
125  self.detection.background.binSize = 4096
126  self.detection.background.undersampleStyle = 'REDUCE_INTERP_ORDER'
127 
128 ## \addtogroup LSST_task_documentation
129 ## \{
130 ## \page DetectCoaddSourcesTask
131 ## \ref DetectCoaddSourcesTask_ "DetectCoaddSourcesTask"
132 ## \copybrief DetectCoaddSourcesTask
133 ## \}
134 
135 
136 class DetectCoaddSourcesTask(CmdLineTask):
137  """!
138  \anchor DetectCoaddSourcesTask_
139 
140  \brief Detect sources on a coadd
141 
142  \section pipe_tasks_multiBand_Contents Contents
143 
144  - \ref pipe_tasks_multiBand_DetectCoaddSourcesTask_Purpose
145  - \ref pipe_tasks_multiBand_DetectCoaddSourcesTask_Initialize
146  - \ref pipe_tasks_multiBand_DetectCoaddSourcesTask_Run
147  - \ref pipe_tasks_multiBand_DetectCoaddSourcesTask_Config
148  - \ref pipe_tasks_multiBand_DetectCoaddSourcesTask_Debug
149  - \ref pipe_tasks_multiband_DetectCoaddSourcesTask_Example
150 
151  \section pipe_tasks_multiBand_DetectCoaddSourcesTask_Purpose Description
152 
153  Command-line task that detects sources on a coadd of exposures obtained with a single filter.
154 
155  Coadding individual visits requires each exposure to be warped. This introduces covariance in the noise
156  properties across pixels. Before detection, we correct the coadd variance by scaling the variance plane
157  in the coadd to match the observed variance. This is an approximate approach -- strictly, we should
158  propagate the full covariance matrix -- but it is simple and works well in practice.
159 
160  After scaling the variance plane, we detect sources and generate footprints by delegating to the \ref
161  SourceDetectionTask_ "detection" subtask.
162 
163  \par Inputs:
164  deepCoadd{tract,patch,filter}: ExposureF
165  \par Outputs:
166  deepCoadd_det{tract,patch,filter}: SourceCatalog (only parent Footprints)
167  \n deepCoadd_calexp{tract,patch,filter}: Variance scaled, background-subtracted input
168  exposure (ExposureF)
169  \n deepCoadd_calexp_background{tract,patch,filter}: BackgroundList
170  \par Data Unit:
171  tract, patch, filter
172 
173  DetectCoaddSourcesTask delegates most of its work to the \ref SourceDetectionTask_ "detection" subtask.
174  You can retarget this subtask if you wish.
175 
176  \section pipe_tasks_multiBand_DetectCoaddSourcesTask_Initialize Task initialization
177 
178  \copydoc \_\_init\_\_
179 
180  \section pipe_tasks_multiBand_DetectCoaddSourcesTask_Run Invoking the Task
181 
182  \copydoc run
183 
184  \section pipe_tasks_multiBand_DetectCoaddSourcesTask_Config Configuration parameters
185 
186  See \ref DetectCoaddSourcesConfig_ "DetectSourcesConfig"
187 
188  \section pipe_tasks_multiBand_DetectCoaddSourcesTask_Debug Debug variables
189 
190  The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
191  flag \c -d to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py
192  files.
193 
194  DetectCoaddSourcesTask has no debug variables of its own because it relegates all the work to
195  \ref SourceDetectionTask_ "SourceDetectionTask"; see the documetation for
196  \ref SourceDetectionTask_ "SourceDetectionTask" for further information.
197 
198  \section pipe_tasks_multiband_DetectCoaddSourcesTask_Example A complete example of using DetectCoaddSourcesTask
199 
200  DetectCoaddSourcesTask is meant to be run after assembling a coadded image in a given band. The purpose of
201  the task is to update the background, detect all sources in a single band and generate a set of parent
202  footprints. Subsequent tasks in the multi-band processing procedure will merge sources across bands and,
203  eventually, perform forced photometry. Command-line usage of DetectCoaddSourcesTask expects a data
204  reference to the coadd to be processed. A list of the available optional arguments can be obtained by
205  calling detectCoaddSources.py with the `--help` command line argument:
206  \code
207  detectCoaddSources.py --help
208  \endcode
209 
210  To demonstrate usage of the DetectCoaddSourcesTask in the larger context of multi-band processing, we
211  will process HSC data in the [ci_hsc](https://github.com/lsst/ci_hsc) package. Assuming one has followed
212  steps 1 - 4 at \ref pipeTasks_multiBand, one may detect all the sources in each coadd as follows:
213  \code
214  detectCoaddSources.py $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-I
215  \endcode
216  that will process the HSC-I band data. The results are written to
217  `$CI_HSC_DIR/DATA/deepCoadd-results/HSC-I`.
218 
219  It is also necessary to run:
220  \code
221  detectCoaddSources.py $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-R
222  \endcode
223  to generate the sources catalogs for the HSC-R band required by the next step in the multi-band
224  processing procedure: \ref MergeDetectionsTask_ "MergeDetectionsTask".
225  """
226  _DefaultName = "detectCoaddSources"
227  ConfigClass = DetectCoaddSourcesConfig
228  getSchemaCatalogs = _makeGetSchemaCatalogs("det")
229  makeIdFactory = _makeMakeIdFactory("CoaddId")
230 
231  @classmethod
233  parser = ArgumentParser(name=cls._DefaultName)
234  parser.add_id_argument("--id", "deepCoadd", help="data ID, e.g. --id tract=12345 patch=1,2 filter=r",
235  ContainerClass=ExistingCoaddDataIdContainer)
236  return parser
237 
238  def __init__(self, schema=None, **kwargs):
239  """!
240  \brief Initialize the task. Create the \ref SourceDetectionTask_ "detection" subtask.
241 
242  Keyword arguments (in addition to those forwarded to CmdLineTask.__init__):
243 
244  \param[in] schema: initial schema for the output catalog, modified-in place to include all
245  fields set by this task. If None, the source minimal schema will be used.
246  \param[in] **kwargs: keyword arguments to be passed to lsst.pipe.base.task.Task.__init__
247  """
248  CmdLineTask.__init__(self, **kwargs)
249  if schema is None:
250  schema = afwTable.SourceTable.makeMinimalSchema()
251  self.schema = schema
252  self.makeSubtask("detection", schema=self.schema)
253 
254  def run(self, patchRef):
255  """!
256  \brief Run detection on a coadd.
257 
258  Invokes \ref runDetection and then uses \ref write to output the
259  results.
260 
261  \param[in] patchRef: data reference for patch
262  """
263  exposure = patchRef.get(self.config.coaddName + "Coadd", immediate=True)
264  results = self.runDetection(exposure, self.makeIdFactory(patchRef))
265  self.write(exposure, results, patchRef)
266  return results
267 
268  def runDetection(self, exposure, idFactory):
269  """!
270  \brief Run detection on an exposure.
271 
272  First scale the variance plane to match the observed variance
273  using \ref scaleVariance. Then invoke the \ref SourceDetectionTask_ "detection" subtask to
274  detect sources.
275 
276  \param[in] exposure: Exposure on which to detect
277  \param[in] idFactory: IdFactory to set source identifiers
278 
279  \return a pipe.base.Struct with fields
280  - sources: catalog of detections
281  - backgrounds: list of backgrounds
282  """
283  if self.config.doScaleVariance:
284  scaleVariance(exposure.getMaskedImage(), self.config.mask, log=self.log)
285  backgrounds = afwMath.BackgroundList()
286  table = afwTable.SourceTable.make(self.schema, idFactory)
287  detections = self.detection.makeSourceCatalog(table, exposure)
288  sources = detections.sources
289  fpSets = detections.fpSets
290  if fpSets.background:
291  backgrounds.append(fpSets.background)
292  return Struct(sources=sources, backgrounds=backgrounds)
293 
294  def write(self, exposure, results, patchRef):
295  """!
296  \brief Write out results from runDetection.
297 
298  \param[in] exposure: Exposure to write out
299  \param[in] results: Struct returned from runDetection
300  \param[in] patchRef: data reference for patch
301  """
302  coaddName = self.config.coaddName + "Coadd"
303  patchRef.put(results.backgrounds, coaddName + "_calexp_background")
304  patchRef.put(results.sources, coaddName + "_det")
305  patchRef.put(exposure, coaddName + "_calexp")
306 
307 ##############################################################################################################
308 
309 
310 class MergeSourcesRunner(TaskRunner):
311  """!
312  \anchor MergeSourcesRunner_
313 
314  \brief Task runner for the \ref MergeSourcesTask_ "MergeSourcesTask". Required because the run method
315  requires a list of dataRefs rather than a single dataRef.
316  """
317 
318  def makeTask(self, parsedCmd=None, args=None):
319  """!
320  \brief Provide a butler to the Task constructor.
321 
322  \param[in] parsedCmd the parsed command
323  \param[in] args tuple of a list of data references and kwargs (un-used)
324  \throws RuntimeError if both parsedCmd & args are None
325  """
326  if parsedCmd is not None:
327  butler = parsedCmd.butler
328  elif args is not None:
329  dataRefList, kwargs = args
330  butler = dataRefList[0].getButler()
331  else:
332  raise RuntimeError("Neither parsedCmd or args specified")
333  return self.TaskClass(config=self.config, log=self.log, butler=butler)
334 
335  @staticmethod
336  def getTargetList(parsedCmd, **kwargs):
337  """!
338  \brief Provide a list of patch references for each patch.
339 
340  The patch references within the list will have different filters.
341 
342  \param[in] parsedCmd the parsed command
343  \param **kwargs key word arguments (unused)
344  \throws RuntimeError if multiple references are provided for the same combination of tract, patch and
345  filter
346  """
347  refList = {} # Will index this as refList[tract][patch][filter] = ref
348  for ref in parsedCmd.id.refList:
349  tract = ref.dataId["tract"]
350  patch = ref.dataId["patch"]
351  filter = ref.dataId["filter"]
352  if not tract in refList:
353  refList[tract] = {}
354  if not patch in refList[tract]:
355  refList[tract][patch] = {}
356  if filter in refList[tract][patch]:
357  raise RuntimeError("Multiple versions of %s" % (ref.dataId,))
358  refList[tract][patch][filter] = ref
359  return [(list(p.values()), kwargs) for t in refList.values() for p in t.values()]
360 
361 
362 class MergeSourcesConfig(Config):
363  """!
364  \anchor MergeSourcesConfig_
365 
366  \brief Configuration for merging sources.
367  """
368  priorityList = ListField(dtype=str, default=[],
369  doc="Priority-ordered list of bands for the merge.")
370  coaddName = Field(dtype=str, default="deep", doc="Name of coadd")
371 
372  def validate(self):
373  Config.validate(self)
374  if len(self.priorityList) == 0:
375  raise RuntimeError("No priority list provided")
376 
377 
378 class MergeSourcesTask(CmdLineTask):
379  """!
380  \anchor MergeSourcesTask_
381 
382  \brief A base class for merging source catalogs.
383 
384  Merging detections (MergeDetectionsTask) and merging measurements (MergeMeasurementsTask) are
385  so similar that it makes sense to re-use the code, in the form of this abstract base class.
386 
387  NB: Do not use this class directly. Instead use one of the child classes that inherit from
388  MergeSourcesTask such as \ref MergeDetectionsTask_ "MergeDetectionsTask" or \ref MergeMeasurementsTask_
389  "MergeMeasurementsTask"
390 
391  Sub-classes should set the following class variables:
392  * `_DefaultName`: name of Task
393  * `inputDataset`: name of dataset to read
394  * `outputDataset`: name of dataset to write
395  * `getSchemaCatalogs` to the result of `_makeGetSchemaCatalogs(outputDataset)`
396 
397  In addition, sub-classes must implement the mergeCatalogs method.
398  """
399  _DefaultName = None
400  ConfigClass = MergeSourcesConfig
401  RunnerClass = MergeSourcesRunner
402  inputDataset = None
403  outputDataset = None
404  getSchemaCatalogs = None
405 
406  @classmethod
408  """!
409  \brief Create a suitable ArgumentParser.
410 
411  We will use the ArgumentParser to get a provide a list of data
412  references for patches; the RunnerClass will sort them into lists
413  of data references for the same patch
414  """
415  parser = ArgumentParser(name=cls._DefaultName)
416  parser.add_id_argument("--id", "deepCoadd_" + cls.inputDataset,
417  ContainerClass=ExistingCoaddDataIdContainer,
418  help="data ID, e.g. --id tract=12345 patch=1,2 filter=g^r^i")
419  return parser
420 
421  def getInputSchema(self, butler=None, schema=None):
422  """!
423  \brief Obtain the input schema either directly or froma butler reference.
424 
425  \param[in] butler butler reference to obtain the input schema from
426  \param[in] schema the input schema
427  """
428  if schema is None:
429  assert butler is not None, "Neither butler nor schema specified"
430  schema = butler.get(self.config.coaddName + "Coadd_" + self.inputDataset + "_schema",
431  immediate=True).schema
432  return schema
433 
434  def __init__(self, butler=None, schema=None, **kwargs):
435  """!
436  \brief Initialize the task.
437 
438  Keyword arguments (in addition to those forwarded to CmdLineTask.__init__):
439  \param[in] schema the schema of the detection catalogs used as input to this one
440  \param[in] butler a butler used to read the input schema from disk, if schema is None
441 
442  Derived classes should use the getInputSchema() method to handle the additional
443  arguments and retreive the actual input schema.
444  """
445  CmdLineTask.__init__(self, **kwargs)
446 
447  def run(self, patchRefList):
448  """!
449  \brief Merge coadd sources from multiple bands. Calls \ref mergeCatalogs which must be defined in
450  subclasses that inherit from MergeSourcesTask.
451 
452  \param[in] patchRefList list of data references for each filter
453  """
454  catalogs = dict(self.readCatalog(patchRef) for patchRef in patchRefList)
455  mergedCatalog = self.mergeCatalogs(catalogs, patchRefList[0])
456  self.write(patchRefList[0], mergedCatalog)
457 
458  def readCatalog(self, patchRef):
459  """!
460  \brief Read input catalog.
461 
462  We read the input dataset provided by the 'inputDataset'
463  class variable.
464 
465  \param[in] patchRef data reference for patch
466  \return tuple consisting of the filter name and the catalog
467  """
468  filterName = patchRef.dataId["filter"]
469  catalog = patchRef.get(self.config.coaddName + "Coadd_" + self.inputDataset, immediate=True)
470  self.log.info("Read %d sources for filter %s: %s" % (len(catalog), filterName, patchRef.dataId))
471  return filterName, catalog
472 
473  def mergeCatalogs(self, catalogs, patchRef):
474  """!
475  \brief Merge multiple catalogs. This function must be defined in all subclasses that inherit from
476  MergeSourcesTask.
477 
478  \param[in] catalogs dict mapping filter name to source catalog
479 
480  \return merged catalog
481  """
482  raise NotImplementedError()
483 
484  def write(self, patchRef, catalog):
485  """!
486  \brief Write the output.
487 
488  \param[in] patchRef data reference for patch
489  \param[in] catalog catalog
490 
491  We write as the dataset provided by the 'outputDataset'
492  class variable.
493  """
494  patchRef.put(catalog, self.config.coaddName + "Coadd_" + self.outputDataset)
495  # since the filter isn't actually part of the data ID for the dataset we're saving,
496  # it's confusing to see it in the log message, even if the butler simply ignores it.
497  mergeDataId = patchRef.dataId.copy()
498  del mergeDataId["filter"]
499  self.log.info("Wrote merged catalog: %s" % (mergeDataId,))
500 
501  def writeMetadata(self, dataRefList):
502  """!
503  \brief No metadata to write, and not sure how to write it for a list of dataRefs.
504  """
505  pass
506 
507 
508 class CullPeaksConfig(Config):
509  """!
510  \anchor CullPeaksConfig_
511 
512  \brief Configuration for culling garbage peaks after merging footprints.
513 
514  Peaks may also be culled after detection or during deblending; this configuration object
515  only deals with culling after merging Footprints.
516 
517  These cuts are based on three quantities:
518  - nBands: the number of bands in which the peak was detected
519  - peakRank: the position of the peak within its family, sorted from brightest to faintest.
520  - peakRankNormalized: the peak rank divided by the total number of peaks in the family.
521 
522  The formula that identifie peaks to cull is:
523 
524  nBands < nBandsSufficient
525  AND (rank >= rankSufficient)
526  AND (rank >= rankConsider OR rank >= rankNormalizedConsider)
527 
528  To disable peak culling, simply set nBandsSafe=1.
529  """
530 
531  nBandsSufficient = RangeField(dtype=int, default=2, min=1,
532  doc="Always keep peaks detected in this many bands")
533  rankSufficient = RangeField(dtype=int, default=20, min=1,
534  doc="Always keep this many peaks in each family")
535  rankConsidered = RangeField(dtype=int, default=30, min=1,
536  doc=("Keep peaks with less than this rank that also match the "
537  "rankNormalizedConsidered condition."))
538  rankNormalizedConsidered = RangeField(dtype=float, default=0.7, min=0.0,
539  doc=("Keep peaks with less than this normalized rank that"
540  " also match the rankConsidered condition."))
541 
542 
544  """!
545  \anchor MergeDetectionsConfig_
546 
547  \brief Configuration parameters for the MergeDetectionsTask.
548  """
549  minNewPeak = Field(dtype=float, default=1,
550  doc="Minimum distance from closest peak to create a new one (in arcsec).")
551 
552  maxSamePeak = Field(dtype=float, default=0.3,
553  doc="When adding new catalogs to the merge, all peaks less than this distance "
554  " (in arcsec) to an existing peak will be flagged as detected in that catalog.")
555  cullPeaks = ConfigField(dtype=CullPeaksConfig, doc="Configuration for how to cull peaks.")
556 
557  skyFilterName = Field(dtype=str, default="sky",
558  doc="Name of `filter' used to label sky objects (e.g. flag merge_peak_sky is set)\n"
559  "(N.b. should be in MergeMeasurementsConfig.pseudoFilterList)")
560  skySourceRadius = Field(dtype=float, default=8,
561  doc="Radius, in pixels, of sky objects")
562  skyGrowDetectedFootprints = Field(dtype=int, default=0,
563  doc="Number of pixels to grow the detected footprint mask "
564  "when adding sky objects")
565  nSkySourcesPerPatch = Field(dtype=int, default=100,
566  doc="Try to add this many sky objects to the mergeDet list, which will\n"
567  "then be measured along with the detected objects in sourceMeasurementTask")
568  nTrialSkySourcesPerPatch = Field(dtype=int, default=None, optional=True,
569  doc="Maximum number of trial sky object positions\n"
570  "(default: nSkySourcesPerPatch*nTrialSkySourcesPerPatchMultiplier)")
571  nTrialSkySourcesPerPatchMultiplier = Field(dtype=int, default=5,
572  doc="Set nTrialSkySourcesPerPatch to\n"
573  " nSkySourcesPerPatch*nTrialSkySourcesPerPatchMultiplier\n"
574  "if nTrialSkySourcesPerPatch is None")
575 
576 ## \addtogroup LSST_task_documentation
577 ## \{
578 ## \page MergeDetectionsTask
579 ## \ref MergeDetectionsTask_ "MergeDetectionsTask"
580 ## \copybrief MergeDetectionsTask
581 ## \}
582 
583 
585  """!
586  \anchor MergeDetectionsTask_
587 
588  \brief Merge coadd detections from multiple bands.
589 
590  \section pipe_tasks_multiBand_Contents Contents
591 
592  - \ref pipe_tasks_multiBand_MergeDetectionsTask_Purpose
593  - \ref pipe_tasks_multiBand_MergeDetectionsTask_Init
594  - \ref pipe_tasks_multiBand_MergeDetectionsTask_Run
595  - \ref pipe_tasks_multiBand_MergeDetectionsTask_Config
596  - \ref pipe_tasks_multiBand_MergeDetectionsTask_Debug
597  - \ref pipe_tasks_multiband_MergeDetectionsTask_Example
598 
599  \section pipe_tasks_multiBand_MergeDetectionsTask_Purpose Description
600 
601  Command-line task that merges sources detected in coadds of exposures obtained with different filters.
602 
603  To perform photometry consistently across coadds in multiple filter bands, we create a master catalog of
604  sources from all bands by merging the sources (peaks & footprints) detected in each coadd, while keeping
605  track of which band each source originates in.
606 
607  The catalog merge is performed by \ref getMergedSourceCatalog. Spurious peaks detected around bright
608  objects are culled as described in \ref CullPeaksConfig_.
609 
610  \par Inputs:
611  deepCoadd_det{tract,patch,filter}: SourceCatalog (only parent Footprints)
612  \par Outputs:
613  deepCoadd_mergeDet{tract,patch}: SourceCatalog (only parent Footprints)
614  \par Data Unit:
615  tract, patch
616 
617  MergeDetectionsTask subclasses \ref MergeSourcesTask_ "MergeSourcesTask".
618 
619  \section pipe_tasks_multiBand_MergeDetectionsTask_Init Task initialisation
620 
621  \copydoc \_\_init\_\_
622 
623  \section pipe_tasks_multiBand_MergeDetectionsTask_Run Invoking the Task
624 
625  \copydoc run
626 
627  \section pipe_tasks_multiBand_MergeDetectionsTask_Config Configuration parameters
628 
629  See \ref MergeDetectionsConfig_
630 
631  \section pipe_tasks_multiBand_MergeDetectionsTask_Debug Debug variables
632 
633  The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a flag \c -d
634  to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py files.
635 
636  MergeDetectionsTask has no debug variables.
637 
638  \section pipe_tasks_multiband_MergeDetectionsTask_Example A complete example of using MergeDetectionsTask
639 
640  MergeDetectionsTask is meant to be run after detecting sources in coadds generated for the chosen subset
641  of the available bands.
642  The purpose of the task is to merge sources (peaks & footprints) detected in the coadds generated from the
643  chosen subset of filters.
644  Subsequent tasks in the multi-band processing procedure will deblend the generated master list of sources
645  and, eventually, perform forced photometry.
646  Command-line usage of MergeDetectionsTask expects data references for all the coadds to be processed.
647  A list of the available optional arguments can be obtained by calling mergeCoaddDetections.py with the
648  `--help` command line argument:
649  \code
650  mergeCoaddDetections.py --help
651  \endcode
652 
653  To demonstrate usage of the DetectCoaddSourcesTask in the larger context of multi-band processing, we
654  will process HSC data in the [ci_hsc](https://github.com/lsst/ci_hsc) package. Assuming one has finished
655  step 5 at \ref pipeTasks_multiBand, one may merge the catalogs of sources from each coadd as follows:
656  \code
657  mergeCoaddDetections.py $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-I^HSC-R
658  \endcode
659  This will merge the HSC-I & -R band parent source catalogs and write the results to
660  `$CI_HSC_DIR/DATA/deepCoadd-results/merged/0/5,4/mergeDet-0-5,4.fits`.
661 
662  The next step in the multi-band processing procedure is
663  \ref MeasureMergedCoaddSourcesTask_ "MeasureMergedCoaddSourcesTask"
664  """
665  ConfigClass = MergeDetectionsConfig
666  _DefaultName = "mergeCoaddDetections"
667  inputDataset = "det"
668  outputDataset = "mergeDet"
669  makeIdFactory = _makeMakeIdFactory("MergedCoaddId")
670 
671  def __init__(self, butler=None, schema=None, **kwargs):
672  """!
673  \brief Initialize the merge detections task.
674 
675  A \ref FootprintMergeList_ "FootprintMergeList" will be used to
676  merge the source catalogs.
677 
678  Additional keyword arguments (forwarded to MergeSourcesTask.__init__):
679  \param[in] schema the schema of the detection catalogs used as input to this one
680  \param[in] butler a butler used to read the input schema from disk, if schema is None
681  \param[in] **kwargs keyword arguments to be passed to MergeSourcesTask.__init__
682 
683  The task will set its own self.schema attribute to the schema of the output merged catalog.
684  """
685  MergeSourcesTask.__init__(self, butler=butler, schema=schema, **kwargs)
686  self.schema = self.getInputSchema(butler=butler, schema=schema)
687 
688  filterNames = [getShortFilterName(name) for name in self.config.priorityList]
689  if self.config.nSkySourcesPerPatch > 0:
690  filterNames += [self.config.skyFilterName]
691  self.merged = afwDetect.FootprintMergeList(self.schema, filterNames)
692 
693  def mergeCatalogs(self, catalogs, patchRef):
694  """!
695  \brief Merge multiple catalogs.
696 
697  After ordering the catalogs and filters in priority order,
698  \ref getMergedSourceCatalog of the \ref FootprintMergeList_ "FootprintMergeList" created by
699  \ref \_\_init\_\_ is used to perform the actual merging. Finally, \ref cullPeaks is used to remove
700  garbage peaks detected around bright objects.
701 
702  \param[in] catalogs
703  \param[in] patchRef
704  \param[out] mergedList
705  """
706 
707  # Convert distance to tract coordinate
708  skyInfo = getSkyInfo(coaddName=self.config.coaddName, patchRef=patchRef)
709  tractWcs = skyInfo.wcs
710  peakDistance = self.config.minNewPeak / tractWcs.pixelScale().asArcseconds()
711  samePeakDistance = self.config.maxSamePeak / tractWcs.pixelScale().asArcseconds()
712 
713  # Put catalogs, filters in priority order
714  orderedCatalogs = [catalogs[band] for band in self.config.priorityList if band in catalogs.keys()]
715  orderedBands = [getShortFilterName(band) for band in self.config.priorityList
716  if band in catalogs.keys()]
717 
718  mergedList = self.merged.getMergedSourceCatalog(orderedCatalogs, orderedBands, peakDistance,
719  self.schema, self.makeIdFactory(patchRef),
720  samePeakDistance)
721 
722  #
723  # Add extra sources that correspond to blank sky
724  #
725  skySourceFootprints = self.getSkySourceFootprints(
726  mergedList, skyInfo, self.config.skyGrowDetectedFootprints)
727  if skySourceFootprints:
728  key = mergedList.schema.find("merge_footprint_%s" % self.config.skyFilterName).key
729 
730  for foot in skySourceFootprints:
731  s = mergedList.addNew()
732  s.setFootprint(foot)
733  s.set(key, True)
734 
735  self.log.info("Added %d sky sources (%.0f%% of requested)",
736  len(skySourceFootprints),
737  100*len(skySourceFootprints)/float(self.config.nSkySourcesPerPatch))
738 
739  # Sort Peaks from brightest to faintest
740  for record in mergedList:
741  record.getFootprint().sortPeaks()
742  self.log.info("Merged to %d sources" % len(mergedList))
743  # Attempt to remove garbage peaks
744  self.cullPeaks(mergedList)
745  return mergedList
746 
747  def cullPeaks(self, catalog):
748  """!
749  \brief Attempt to remove garbage peaks (mostly on the outskirts of large blends).
750 
751  \param[in] catalog Source catalog
752  """
753  keys = [item.key for item in self.merged.getPeakSchema().extract("merge.peak.*").values()]
754  totalPeaks = 0
755  culledPeaks = 0
756  for parentSource in catalog:
757  # Make a list copy so we can clear the attached PeakCatalog and append the ones we're keeping
758  # to it (which is easier than deleting as we iterate).
759  keptPeaks = parentSource.getFootprint().getPeaks()
760  oldPeaks = list(keptPeaks)
761  keptPeaks.clear()
762  familySize = len(oldPeaks)
763  totalPeaks += familySize
764  for rank, peak in enumerate(oldPeaks):
765  if ((rank < self.config.cullPeaks.rankSufficient) or
766  (self.config.cullPeaks.nBandsSufficient > 1 and
767  sum([peak.get(k) for k in keys]) >= self.config.cullPeaks.nBandsSufficient) or
768  (rank < self.config.cullPeaks.rankConsidered and
769  rank < self.config.cullPeaks.rankNormalizedConsidered * familySize)):
770  keptPeaks.append(peak)
771  else:
772  culledPeaks += 1
773  self.log.info("Culled %d of %d peaks" % (culledPeaks, totalPeaks))
774 
775  def getSchemaCatalogs(self):
776  """!
777  Return a dict of empty catalogs for each catalog dataset produced by this task.
778 
779  \param[out] dictionary of empty catalogs
780  """
781  mergeDet = afwTable.SourceCatalog(self.schema)
782  peak = afwDetect.PeakCatalog(self.merged.getPeakSchema())
783  return {self.config.coaddName + "Coadd_mergeDet": mergeDet,
784  self.config.coaddName + "Coadd_peak": peak}
785 
786  def getSkySourceFootprints(self, mergedList, skyInfo, growDetectedFootprints=0):
787  """!
788  \brief Return a list of Footprints of sky objects which don't overlap with anything in mergedList
789 
790  \param mergedList The merged Footprints from all the input bands
791  \param skyInfo A description of the patch
792  \param growDetectedFootprints The number of pixels to grow the detected footprints
793  """
794 
795  if self.config.nSkySourcesPerPatch <= 0:
796  return []
797 
798  skySourceRadius = self.config.skySourceRadius
799  nSkySourcesPerPatch = self.config.nSkySourcesPerPatch
800  nTrialSkySourcesPerPatch = self.config.nTrialSkySourcesPerPatch
801  if nTrialSkySourcesPerPatch is None:
802  nTrialSkySourcesPerPatch = self.config.nTrialSkySourcesPerPatchMultiplier*nSkySourcesPerPatch
803  #
804  # We are going to find circular Footprints that don't intersect any pre-existing Footprints,
805  # and the easiest way to do this is to generate a Mask containing all the detected pixels (as
806  # merged by this task).
807  #
808  patchBBox = skyInfo.patchInfo.getOuterBBox()
809  mask = afwImage.MaskU(patchBBox)
810  detectedMask = mask.getPlaneBitMask("DETECTED")
811  for s in mergedList:
812  foot = s.getFootprint()
813  if growDetectedFootprints > 0:
814  foot = afwDetect.growFootprint(foot, growDetectedFootprints)
815  afwDetect.setMaskFromFootprint(mask, foot, detectedMask)
816 
817  xmin, ymin = patchBBox.getMin()
818  xmax, ymax = patchBBox.getMax()
819  # Avoid objects partially off the image
820  xmin += skySourceRadius + 1
821  ymin += skySourceRadius + 1
822  xmax -= skySourceRadius + 1
823  ymax -= skySourceRadius + 1
824 
825  skySourceFootprints = []
826  for i in range(nTrialSkySourcesPerPatch):
827  if len(skySourceFootprints) == nSkySourcesPerPatch:
828  break
829 
830  x = int(numpy.random.uniform(xmin, xmax))
831  y = int(numpy.random.uniform(ymin, ymax))
832  foot = afwDetect.Footprint(afwGeom.PointI(x, y), skySourceRadius, patchBBox)
833  foot.setPeakSchema(self.merged.getPeakSchema())
834 
835  if not foot.overlapsMask(mask):
836  foot.addPeak(x, y, 0)
837  foot.getPeaks()[0].set("merge_peak_%s" % self.config.skyFilterName, True)
838  skySourceFootprints.append(foot)
839 
840  return skySourceFootprints
841 
842 
844  """!
845  \anchor MeasureMergedCoaddSourcesConfig_
846 
847  \brief Configuration parameters for the MeasureMergedCoaddSourcesTask
848  """
849  doDeblend = Field(dtype=bool, default=True, doc="Deblend sources?")
850  deblend = ConfigurableField(target=SourceDeblendTask, doc="Deblend sources")
851  measurement = ConfigurableField(target=SingleFrameMeasurementTask, doc="Source measurement")
852  setPrimaryFlags = ConfigurableField(target=SetPrimaryFlagsTask, doc="Set flags for primary tract/patch")
853  doPropagateFlags = Field(
854  dtype=bool, default=True,
855  doc="Whether to match sources to CCD catalogs to propagate flags (to e.g. identify PSF stars)"
856  )
857  propagateFlags = ConfigurableField(target=PropagateVisitFlagsTask, doc="Propagate visit flags to coadd")
858  doMatchSources = Field(dtype=bool, default=True, doc="Match sources to reference catalog?")
859  match = ConfigurableField(target=DirectMatchTask, doc="Matching to reference catalog")
860  coaddName = Field(dtype=str, default="deep", doc="Name of coadd")
861  checkUnitsParseStrict = Field(
862  doc="Strictness of Astropy unit compatibility check, can be 'raise', 'warn' or 'silent'",
863  dtype=str,
864  default="raise",
865  )
866  doApCorr = Field(
867  dtype=bool,
868  default=True,
869  doc="Apply aperture corrections"
870  )
871  applyApCorr = ConfigurableField(
872  target=ApplyApCorrTask,
873  doc="Subtask to apply aperture corrections"
874  )
875  doRunCatalogCalculation = Field(
876  dtype=bool,
877  default=True,
878  doc='Run catalogCalculation task'
879  )
880  catalogCalculation = ConfigurableField(
881  target=CatalogCalculationTask,
882  doc="Subtask to run catalogCalculation plugins on catalog"
883  )
884 
885  def setDefaults(self):
886  Config.setDefaults(self)
887  self.deblend.propagateAllPeaks = True
888  self.measurement.plugins.names |= ['base_InputCount']
889  # The following line must be set if clipped pixel flags are to be added to the output table
890  # The clipped mask plane is added by running SafeClipAssembleCoaddTask
891  self.measurement.plugins['base_PixelFlags'].masksFpAnywhere = ['CLIPPED']
892 
893 ## \addtogroup LSST_task_documentation
894 ## \{
895 ## \page MeasureMergedCoaddSourcesTask
896 ## \ref MeasureMergedCoaddSourcesTask_ "MeasureMergedCoaddSourcesTask"
897 ## \copybrief MeasureMergedCoaddSourcesTask
898 ## \}
899 
900 
901 class MeasureMergedCoaddSourcesTask(CmdLineTask):
902  """!
903  \anchor MeasureMergedCoaddSourcesTask_
904 
905  \brief Deblend sources from master catalog in each coadd seperately and measure.
906 
907  \section pipe_tasks_multiBand_Contents Contents
908 
909  - \ref pipe_tasks_multiBand_MeasureMergedCoaddSourcesTask_Purpose
910  - \ref pipe_tasks_multiBand_MeasureMergedCoaddSourcesTask_Initialize
911  - \ref pipe_tasks_multiBand_MeasureMergedCoaddSourcesTask_Run
912  - \ref pipe_tasks_multiBand_MeasureMergedCoaddSourcesTask_Config
913  - \ref pipe_tasks_multiBand_MeasureMergedCoaddSourcesTask_Debug
914  - \ref pipe_tasks_multiband_MeasureMergedCoaddSourcesTask_Example
915 
916  \section pipe_tasks_multiBand_MeasureMergedCoaddSourcesTask_Purpose Description
917 
918  Command-line task that uses peaks and footprints from a master catalog to perform deblending and
919  measurement in each coadd.
920 
921  Given a master input catalog of sources (peaks and footprints), deblend and measure each source on the
922  coadd. Repeating this procedure with the same master catalog across multiple coadds will generate a
923  consistent set of child sources.
924 
925  The deblender retains all peaks and deblends any missing peaks (dropouts in that band) as PSFs. Source
926  properties are measured and the \c is-primary flag (indicating sources with no children) is set. Visit
927  flags are propagated to the coadd sources.
928 
929  Optionally, we can match the coadd sources to an external reference catalog.
930 
931  \par Inputs:
932  deepCoadd_mergeDet{tract,patch}: SourceCatalog
933  \n deepCoadd_calexp{tract,patch,filter}: ExposureF
934  \par Outputs:
935  deepCoadd_meas{tract,patch,filter}: SourceCatalog
936  \par Data Unit:
937  tract, patch, filter
938 
939  MeasureMergedCoaddSourcesTask delegates most of its work to a set of sub-tasks:
940 
941  <DL>
942  <DT> \ref SourceDeblendTask_ "deblend"
943  <DD> Deblend all the sources from the master catalog.</DD>
944  <DT> \ref SingleFrameMeasurementTask_ "measurement"
945  <DD> Measure source properties of deblended sources.</DD>
946  <DT> \ref SetPrimaryFlagsTask_ "setPrimaryFlags"
947  <DD> Set flag 'is-primary' as well as related flags on sources. 'is-primary' is set for sources that are
948  not at the edge of the field and that have either not been deblended or are the children of deblended
949  sources</DD>
950  <DT> \ref PropagateVisitFlagsTask_ "propagateFlags"
951  <DD> Propagate flags set in individual visits to the coadd.</DD>
952  <DT> \ref DirectMatchTask_ "match"
953  <DD> Match input sources to a reference catalog (optional).
954  </DD>
955  </DL>
956  These subtasks may be retargeted as required.
957 
958  \section pipe_tasks_multiBand_MeasureMergedCoaddSourcesTask_Initialize Task initialization
959 
960  \copydoc \_\_init\_\_
961 
962  \section pipe_tasks_multiBand_MeasureMergedCoaddSourcesTask_Run Invoking the Task
963 
964  \copydoc run
965 
966  \section pipe_tasks_multiBand_MeasureMergedCoaddSourcesTask_Config Configuration parameters
967 
968  See \ref MeasureMergedCoaddSourcesConfig_
969 
970  \section pipe_tasks_multiBand_MeasureMergedCoaddSourcesTask_Debug Debug variables
971 
972  The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
973  flag \c -d to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py
974  files.
975 
976  MeasureMergedCoaddSourcesTask has no debug variables of its own because it delegates all the work to
977  the various sub-tasks. See the documetation for individual sub-tasks for more information.
978 
979  \section pipe_tasks_multiband_MeasureMergedCoaddSourcesTask_Example A complete example of using MeasureMergedCoaddSourcesTask
980 
981  After MeasureMergedCoaddSourcesTask has been run on multiple coadds, we have a set of per-band catalogs.
982  The next stage in the multi-band processing procedure will merge these measurements into a suitable
983  catalog for driving forced photometry.
984 
985  Command-line usage of MeasureMergedCoaddSourcesTask expects a data reference to the coadds to be processed.
986  A list of the available optional arguments can be obtained by calling measureCoaddSources.py with the
987  `--help` command line argument:
988  \code
989  measureCoaddSources.py --help
990  \endcode
991 
992  To demonstrate usage of the DetectCoaddSourcesTask in the larger context of multi-band processing, we
993  will process HSC data in the [ci_hsc](https://github.com/lsst/ci_hsc) package. Assuming one has finished
994  step 6 at \ref pipeTasks_multiBand, one may perform deblending and measure sources in the HSC-I band
995  coadd as follows:
996  \code
997  measureCoaddSources.py $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-I
998  \endcode
999  This will process the HSC-I band data. The results are written in
1000  `$CI_HSC_DIR/DATA/deepCoadd-results/HSC-I/0/5,4/meas-HSC-I-0-5,4.fits
1001 
1002  It is also necessary to run
1003  \code
1004  measureCoaddSources.py $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-R
1005  \endcode
1006  to generate the sources catalogs for the HSC-R band required by the next step in the multi-band
1007  procedure: \ref MergeMeasurementsTask_ "MergeMeasurementsTask".
1008  """
1009  _DefaultName = "measureCoaddSources"
1010  ConfigClass = MeasureMergedCoaddSourcesConfig
1011  RunnerClass = ButlerInitializedTaskRunner
1012  getSchemaCatalogs = _makeGetSchemaCatalogs("meas")
1013  makeIdFactory = _makeMakeIdFactory("MergedCoaddId") # The IDs we already have are of this type
1014 
1015  @classmethod
1017  parser = ArgumentParser(name=cls._DefaultName)
1018  parser.add_id_argument("--id", "deepCoadd_calexp",
1019  help="data ID, e.g. --id tract=12345 patch=1,2 filter=r",
1020  ContainerClass=ExistingCoaddDataIdContainer)
1021  return parser
1022 
1023  def __init__(self, butler=None, schema=None, peakSchema=None, refObjLoader=None, **kwargs):
1024  """!
1025  \brief Initialize the task.
1026 
1027  Keyword arguments (in addition to those forwarded to CmdLineTask.__init__):
1028  \param[in] schema: the schema of the merged detection catalog used as input to this one
1029  \param[in] peakSchema: the schema of the PeakRecords in the Footprints in the merged detection catalog
1030  \param[in] refObjLoader: an instance of LoadReferenceObjectsTasks that supplies an external reference
1031  catalog. May be None if the loader can be constructed from the butler argument or all steps
1032  requiring a reference catalog are disabled.
1033  \param[in] butler: a butler used to read the input schemas from disk or construct the reference
1034  catalog loader, if schema or peakSchema or refObjLoader is None
1035 
1036  The task will set its own self.schema attribute to the schema of the output measurement catalog.
1037  This will include all fields from the input schema, as well as additional fields for all the
1038  measurements.
1039  """
1040  CmdLineTask.__init__(self, **kwargs)
1041  if schema is None:
1042  assert butler is not None, "Neither butler nor schema is defined"
1043  schema = butler.get(self.config.coaddName + "Coadd_mergeDet_schema", immediate=True).schema
1045  self.schemaMapper.addMinimalSchema(schema)
1046  self.schema = self.schemaMapper.getOutputSchema()
1048  if self.config.doDeblend:
1049  if peakSchema is None:
1050  assert butler is not None, "Neither butler nor peakSchema is defined"
1051  peakSchema = butler.get(self.config.coaddName + "Coadd_peak_schema", immediate=True).schema
1052  self.makeSubtask("deblend", schema=self.schema, peakSchema=peakSchema)
1053  self.makeSubtask("measurement", schema=self.schema, algMetadata=self.algMetadata)
1054  self.makeSubtask("setPrimaryFlags", schema=self.schema)
1055  if self.config.doMatchSources:
1056  if refObjLoader is None:
1057  assert butler is not None, "Neither butler nor refObjLoader is defined"
1058  self.makeSubtask("match", butler=butler, refObjLoader=refObjLoader)
1059  if self.config.doPropagateFlags:
1060  self.makeSubtask("propagateFlags", schema=self.schema)
1061  self.schema.checkUnits(parse_strict=self.config.checkUnitsParseStrict)
1062  if self.config.doApCorr:
1063  self.makeSubtask("applyApCorr", schema=self.schema)
1064  if self.config.doRunCatalogCalculation:
1065  self.makeSubtask("catalogCalculation", schema=self.schema)
1066 
1067  def run(self, patchRef):
1068  """!
1069  \brief Deblend and measure.
1070 
1071  \param[in] patchRef: Patch reference.
1072 
1073  Deblend each source in every coadd and measure. Set 'is-primary' and related flags. Propagate flags
1074  from individual visits. Optionally match the sources to a reference catalog and write the matches.
1075  Finally, write the deblended sources and measurements out.
1076  """
1077  exposure = patchRef.get(self.config.coaddName + "Coadd_calexp", immediate=True)
1078  sources = self.readSources(patchRef)
1079  if self.config.doDeblend:
1080  self.deblend.run(exposure, sources)
1081 
1082  bigKey = sources.schema["deblend_parentTooBig"].asKey()
1083  # catalog is non-contiguous so can't extract column
1084  numBig = sum((s.get(bigKey) for s in sources))
1085  if numBig > 0:
1086  self.log.warn("Patch %s contains %d large footprints that were not deblended" %
1087  (patchRef.dataId, numBig))
1088 
1089  table = sources.getTable()
1090  table.setMetadata(self.algMetadata) # Capture algorithm metadata to write out to the source catalog.
1091 
1092  self.measurement.run(sources, exposure, exposureId=self.getExposureId(patchRef))
1093 
1094  if self.config.doApCorr:
1095  self.applyApCorr.run(
1096  catalog=sources,
1097  apCorrMap=exposure.getInfo().getApCorrMap()
1098  )
1099 
1100  if self.config.doRunCatalogCalculation:
1101  self.catalogCalculation.run(sources)
1102 
1103  skyInfo = getSkyInfo(coaddName=self.config.coaddName, patchRef=patchRef)
1104  self.setPrimaryFlags.run(sources, skyInfo.skyMap, skyInfo.tractInfo, skyInfo.patchInfo,
1105  includeDeblend=self.config.doDeblend)
1106  if self.config.doPropagateFlags:
1107  self.propagateFlags.run(patchRef.getButler(), sources, self.propagateFlags.getCcdInputs(exposure),
1108  exposure.getWcs())
1109  if self.config.doMatchSources:
1110  self.writeMatches(patchRef, exposure, sources)
1111  self.write(patchRef, sources)
1112 
1113  def readSources(self, dataRef):
1114  """!
1115  \brief Read input sources.
1116 
1117  \param[in] dataRef: Data reference for catalog of merged detections
1118  \return List of sources in merged catalog
1119 
1120  We also need to add columns to hold the measurements we're about to make
1121  so we can measure in-place.
1122  """
1123  merged = dataRef.get(self.config.coaddName + "Coadd_mergeDet", immediate=True)
1124  self.log.info("Read %d detections: %s" % (len(merged), dataRef.dataId))
1125  idFactory = self.makeIdFactory(dataRef)
1126  for s in merged:
1127  idFactory.notify(s.getId())
1128  table = afwTable.SourceTable.make(self.schema, idFactory)
1129  sources = afwTable.SourceCatalog(table)
1130  sources.extend(merged, self.schemaMapper)
1131  return sources
1132 
1133  def writeMatches(self, dataRef, exposure, sources):
1134  """!
1135  \brief Write matches of the sources to the astrometric reference catalog.
1136 
1137  We use the Wcs in the exposure to match sources.
1138 
1139  \param[in] dataRef: data reference
1140  \param[in] exposure: exposure with Wcs
1141  \param[in] sources: source catalog
1142  """
1143  result = self.match.run(sources, exposure.getInfo().getFilter().getName())
1144  if result.matches:
1145  matches = afwTable.packMatches(result.matches)
1146  matches.table.setMetadata(result.matchMeta)
1147  dataRef.put(matches, self.config.coaddName + "Coadd_srcMatch")
1148 
1149  def write(self, dataRef, sources):
1150  """!
1151  \brief Write the source catalog.
1152 
1153  \param[in] dataRef: data reference
1154  \param[in] sources: source catalog
1155  """
1156  dataRef.put(sources, self.config.coaddName + "Coadd_meas")
1157  self.log.info("Wrote %d sources: %s" % (len(sources), dataRef.dataId))
1158 
1159  def getExposureId(self, dataRef):
1160  return int(dataRef.get(self.config.coaddName + "CoaddId"))
1161 
1162 ##############################################################################################################
1163 
1164 
1166  """!
1167  \anchor MergeMeasurementsConfig_
1168 
1169  \brief Configuration parameters for the MergeMeasurementsTask
1170  """
1171  pseudoFilterList = ListField(dtype=str, default=["sky"],
1172  doc="Names of filters which may have no associated detection\n"
1173  "(N.b. should include MergeDetectionsConfig.skyFilterName)")
1174  snName = Field(dtype=str, default="base_PsfFlux",
1175  doc="Name of flux measurement for calculating the S/N when choosing the reference band.")
1176  minSN = Field(dtype=float, default=10.,
1177  doc="If the S/N from the priority band is below this value (and the S/N "
1178  "is larger than minSNDiff compared to the priority band), use the band with "
1179  "the largest S/N as the reference band.")
1180  minSNDiff = Field(dtype=float, default=3.,
1181  doc="If the difference in S/N between another band and the priority band is larger "
1182  "than this value (and the S/N in the priority band is less than minSN) "
1183  "use the band with the largest S/N as the reference band")
1184  flags = ListField(dtype=str, doc="Require that these flags, if available, are not set",
1185  default=["base_PixelFlags_flag_interpolatedCenter", "base_PsfFlux_flag",
1186  "ext_photometryKron_KronFlux_flag", "modelfit_CModel_flag", ])
1187 
1188 ## \addtogroup LSST_task_documentation
1189 ## \{
1190 ## \page MergeMeasurementsTask
1191 ## \ref MergeMeasurementsTask_ "MergeMeasurementsTask"
1192 ## \copybrief MergeMeasurementsTask
1193 ## \}
1194 
1195 
1197  """!
1198  \anchor MergeMeasurementsTask_
1199 
1200  \brief Merge measurements from multiple bands
1201 
1202  \section pipe_tasks_multiBand_Contents Contents
1203 
1204  - \ref pipe_tasks_multiBand_MergeMeasurementsTask_Purpose
1205  - \ref pipe_tasks_multiBand_MergeMeasurementsTask_Initialize
1206  - \ref pipe_tasks_multiBand_MergeMeasurementsTask_Run
1207  - \ref pipe_tasks_multiBand_MergeMeasurementsTask_Config
1208  - \ref pipe_tasks_multiBand_MergeMeasurementsTask_Debug
1209  - \ref pipe_tasks_multiband_MergeMeasurementsTask_Example
1210 
1211  \section pipe_tasks_multiBand_MergeMeasurementsTask_Purpose Description
1212 
1213  Command-line task that merges measurements from multiple bands.
1214 
1215  Combines consistent (i.e. with the same peaks and footprints) catalogs of sources from multiple filter
1216  bands to construct a unified catalog that is suitable for driving forced photometry. Every source is
1217  required to have centroid, shape and flux measurements in each band.
1218 
1219  \par Inputs:
1220  deepCoadd_meas{tract,patch,filter}: SourceCatalog
1221  \par Outputs:
1222  deepCoadd_ref{tract,patch}: SourceCatalog
1223  \par Data Unit:
1224  tract, patch
1225 
1226  MergeMeasurementsTask subclasses \ref MergeSourcesTask_ "MergeSourcesTask".
1227 
1228  \section pipe_tasks_multiBand_MergeMeasurementsTask_Initialize Task initialization
1229 
1230  \copydoc \_\_init\_\_
1231 
1232  \section pipe_tasks_multiBand_MergeMeasurementsTask_Run Invoking the Task
1233 
1234  \copydoc run
1235 
1236  \section pipe_tasks_multiBand_MergeMeasurementsTask_Config Configuration parameters
1237 
1238  See \ref MergeMeasurementsConfig_
1239 
1240  \section pipe_tasks_multiBand_MergeMeasurementsTask_Debug Debug variables
1241 
1242  The \link lsst.pipe.base.cmdLineTask.CmdLineTask command line task\endlink interface supports a
1243  flag \c -d to import \b debug.py from your \c PYTHONPATH; see \ref baseDebug for more about \b debug.py
1244  files.
1245 
1246  MergeMeasurementsTask has no debug variables.
1247 
1248  \section pipe_tasks_multiband_MergeMeasurementsTask_Example A complete example of using MergeMeasurementsTask
1249 
1250  MergeMeasurementsTask is meant to be run after deblending & measuring sources in every band.
1251  The purpose of the task is to generate a catalog of sources suitable for driving forced photometry in
1252  coadds and individual exposures.
1253  Command-line usage of MergeMeasurementsTask expects a data reference to the coadds to be processed. A list
1254  of the available optional arguments can be obtained by calling mergeCoaddMeasurements.py with the `--help`
1255  command line argument:
1256  \code
1257  mergeCoaddMeasurements.py --help
1258  \endcode
1259 
1260  To demonstrate usage of the DetectCoaddSourcesTask in the larger context of multi-band processing, we
1261  will process HSC data in the [ci_hsc](https://github.com/lsst/ci_hsc) package. Assuming one has finished
1262  step 7 at \ref pipeTasks_multiBand, one may merge the catalogs generated after deblending and measuring
1263  as follows:
1264  \code
1265  mergeCoaddMeasurements.py $CI_HSC_DIR/DATA --id patch=5,4 tract=0 filter=HSC-I^HSC-R
1266  \endcode
1267  This will merge the HSC-I & HSC-R band catalogs. The results are written in
1268  `$CI_HSC_DIR/DATA/deepCoadd-results/`.
1269  """
1270  _DefaultName = "mergeCoaddMeasurements"
1271  ConfigClass = MergeMeasurementsConfig
1272  inputDataset = "meas"
1273  outputDataset = "ref"
1274  getSchemaCatalogs = _makeGetSchemaCatalogs("ref")
1275 
1276  def __init__(self, butler=None, schema=None, **kwargs):
1277  """!
1278  Initialize the task.
1279 
1280  Additional keyword arguments (forwarded to MergeSourcesTask.__init__):
1281  \param[in] schema: the schema of the detection catalogs used as input to this one
1282  \param[in] butler: a butler used to read the input schema from disk, if schema is None
1283 
1284  The task will set its own self.schema attribute to the schema of the output merged catalog.
1285  """
1286  MergeSourcesTask.__init__(self, butler=butler, schema=schema, **kwargs)
1287  inputSchema = self.getInputSchema(butler=butler, schema=schema)
1288  self.schemaMapper = afwTable.SchemaMapper(inputSchema, True)
1289  self.schemaMapper.addMinimalSchema(inputSchema, True)
1290  self.fluxKey = inputSchema.find(self.config.snName + "_flux").getKey()
1291  self.fluxErrKey = inputSchema.find(self.config.snName + "_fluxSigma").getKey()
1292  self.fluxFlagKey = inputSchema.find(self.config.snName + "_flag").getKey()
1293 
1294  self.flagKeys = {}
1295  for band in self.config.priorityList:
1296  short = getShortFilterName(band)
1297  outputKey = self.schemaMapper.editOutputSchema().addField(
1298  "merge_measurement_%s" % short,
1299  type="Flag",
1300  doc="Flag field set if the measurements here are from the %s filter" % band
1301  )
1302  peakKey = inputSchema.find("merge_peak_%s" % short).key
1303  footprintKey = inputSchema.find("merge_footprint_%s" % short).key
1304  self.flagKeys[band] = Struct(peak=peakKey, footprint=footprintKey, output=outputKey)
1305  self.schema = self.schemaMapper.getOutputSchema()
1306 
1308  for filt in self.config.pseudoFilterList:
1309  try:
1310  self.pseudoFilterKeys.append(self.schema.find("merge_peak_%s" % filt).getKey())
1311  except:
1312  self.log.warn("merge_peak is not set for pseudo-filter %s" % filt)
1313 
1314  self.badFlags = {}
1315  for flag in self.config.flags:
1316  try:
1317  self.badFlags[flag] = self.schema.find(flag).getKey()
1318  except KeyError as exc:
1319  self.log.warn("Can't find flag %s in schema: %s" % (flag, exc,))
1320 
1321  def mergeCatalogs(self, catalogs, patchRef):
1322  """!
1323  Merge measurement catalogs to create a single reference catalog for forced photometry
1324 
1325  \param[in] catalogs: the catalogs to be merged
1326  \param[in] patchRef: patch reference for data
1327 
1328  For parent sources, we choose the first band in config.priorityList for which the
1329  merge_footprint flag for that band is is True.
1330 
1331  For child sources, the logic is the same, except that we use the merge_peak flags.
1332  """
1333  # Put catalogs, filters in priority order
1334  orderedCatalogs = [catalogs[band] for band in self.config.priorityList if band in catalogs.keys()]
1335  orderedKeys = [self.flagKeys[band] for band in self.config.priorityList if band in catalogs.keys()]
1336 
1337  mergedCatalog = afwTable.SourceCatalog(self.schema)
1338  mergedCatalog.reserve(len(orderedCatalogs[0]))
1339 
1340  idKey = orderedCatalogs[0].table.getIdKey()
1341  for catalog in orderedCatalogs[1:]:
1342  if numpy.any(orderedCatalogs[0].get(idKey) != catalog.get(idKey)):
1343  raise ValueError("Error in inputs to MergeCoaddMeasurements: source IDs do not match")
1344 
1345  # This first zip iterates over all the catalogs simultaneously, yielding a sequence of one
1346  # record for each band, in priority order.
1347  for orderedRecords in zip(*orderedCatalogs):
1348 
1349  maxSNRecord = None
1350  maxSNFlagKeys = None
1351  maxSN = 0.
1352  priorityRecord = None
1353  priorityFlagKeys = None
1354  prioritySN = 0.
1355  hasPseudoFilter = False
1356 
1357  # Now we iterate over those record-band pairs, keeping track of the priority and the
1358  # largest S/N band.
1359  for inputRecord, flagKeys in zip(orderedRecords, orderedKeys):
1360  parent = (inputRecord.getParent() == 0 and inputRecord.get(flagKeys.footprint))
1361  child = (inputRecord.getParent() != 0 and inputRecord.get(flagKeys.peak))
1362 
1363  if not (parent or child):
1364  for pseudoFilterKey in self.pseudoFilterKeys:
1365  if inputRecord.get(pseudoFilterKey):
1366  hasPseudoFilter = True
1367  priorityRecord = inputRecord
1368  priorityFlagKeys = flagKeys
1369  break
1370  if hasPseudoFilter:
1371  break
1372 
1373  isBad = any(inputRecord.get(flag) for flag in self.badFlags)
1374  if isBad or inputRecord.get(self.fluxFlagKey) or inputRecord.get(self.fluxErrKey) == 0:
1375  sn = 0.
1376  else:
1377  sn = inputRecord.get(self.fluxKey)/inputRecord.get(self.fluxErrKey)
1378  if numpy.isnan(sn) or sn < 0.:
1379  sn = 0.
1380  if (parent or child) and priorityRecord is None:
1381  priorityRecord = inputRecord
1382  priorityFlagKeys = flagKeys
1383  prioritySN = sn
1384  if sn > maxSN:
1385  maxSNRecord = inputRecord
1386  maxSNFlagKeys = flagKeys
1387  maxSN = sn
1388 
1389  # If the priority band has a low S/N we would like to choose the band with the highest S/N as
1390  # the reference band instead. However, we only want to choose the highest S/N band if it is
1391  # significantly better than the priority band. Therefore, to choose a band other than the
1392  # priority, we require that the priority S/N is below the minimum threshold and that the
1393  # difference between the priority and highest S/N is larger than the difference threshold.
1394  #
1395  # For pseudo code objects we always choose the first band in the priority list.
1396  bestRecord = None
1397  bestFlagKeys = None
1398  if hasPseudoFilter:
1399  bestRecord = priorityRecord
1400  bestFlagKeys = priorityFlagKeys
1401  elif (prioritySN < self.config.minSN and (maxSN - prioritySN) > self.config.minSNDiff and
1402  maxSNRecord is not None):
1403  bestRecord = maxSNRecord
1404  bestFlagKeys = maxSNFlagKeys
1405  elif priorityRecord is not None:
1406  bestRecord = priorityRecord
1407  bestFlagKeys = priorityFlagKeys
1408 
1409  if bestRecord is not None and bestFlagKeys is not None:
1410  outputRecord = mergedCatalog.addNew()
1411  outputRecord.assign(bestRecord, self.schemaMapper)
1412  outputRecord.set(bestFlagKeys.output, True)
1413  else: # if we didn't find any records
1414  raise ValueError("Error in inputs to MergeCoaddMeasurements: no valid reference for %s" %
1415  inputRecord.getId())
1416 
1417  # more checking for sane inputs, since zip silently iterates over the smallest sequence
1418  for inputCatalog in orderedCatalogs:
1419  if len(mergedCatalog) != len(inputCatalog):
1420  raise ValueError("Mismatch between catalog sizes: %s != %s" %
1421  (len(mergedCatalog), len(orderedCatalogs)))
1422 
1423  return mergedCatalog
def writeMetadata
No metadata to write, and not sure how to write it for a list of dataRefs.
Definition: multiBand.py:501
def cullPeaks
Attempt to remove garbage peaks (mostly on the outskirts of large blends).
Definition: multiBand.py:747
Merge coadd detections from multiple bands.
Definition: multiBand.py:584
def mergeCatalogs
Merge measurement catalogs to create a single reference catalog for forced photometry.
Definition: multiBand.py:1321
boost::shared_ptr< Footprint > growFootprint(Footprint const &foot, int nGrow, bool left, bool right, bool up, bool down)
Grow a Footprint in at least one of the cardinal directions, returning a new Footprint.
def runDetection
Run detection on an exposure.
Definition: multiBand.py:268
Task runner for the MergeSourcesTask. Required because the run method requires a list of dataRefs rat...
Definition: multiBand.py:310
def run
Merge coadd sources from multiple bands.
Definition: multiBand.py:447
Class for storing ordered metadata with comments.
Definition: PropertyList.h:82
A mapping between the keys of two Schemas, used to copy data between them.
Definition: SchemaMapper.h:19
Configuration parameters for the DetectCoaddSourcesTask.
Definition: multiBand.py:107
Merge measurements from multiple bands.
Definition: multiBand.py:1196
Deblend sources from master catalog in each coadd seperately and measure.
Definition: multiBand.py:901
def getTargetList
Provide a list of patch references for each patch.
Definition: multiBand.py:336
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations...
bool any(CoordinateExpr< N > const &expr)
Return true if any elements are true.
def scaleVariance
Scale the variance in a maskedImage.
Definition: coaddBase.py:263
Custom catalog class for record/table subclasses that are guaranteed to have an ID, and should generally be sorted by that ID.
Definition: fwd.h:55
Configuration parameters for the MergeMeasurementsTask.
Definition: multiBand.py:1165
def mergeCatalogs
Merge multiple catalogs.
Definition: multiBand.py:693
Configuration parameters for the MergeDetectionsTask.
Definition: multiBand.py:543
Configuration for merging sources.
Definition: multiBand.py:362
def mergeCatalogs
Merge multiple catalogs.
Definition: multiBand.py:473
A set of pixels in an Image.
Definition: Footprint.h:62
Holds an integer identifier for an LSST filter.
Definition: Filter.h:108
BaseCatalog packMatches(std::vector< Match< Record1, Record2 > > const &matches)
Return a table representation of a MatchVector that can be used to persist it.
def makeTask
Provide a butler to the Task constructor.
Definition: multiBand.py:318
MaskT setMaskFromFootprint(lsst::afw::image::Mask< MaskT > *mask, Footprint const &footprint, MaskT const bitmask)
OR bitmask into all the Mask&#39;s pixels that are in the Footprint.
A base class for merging source catalogs.
Definition: multiBand.py:378
def getSkyInfo
Return the SkyMap, tract and patch information, wcs, and outer bbox of the patch to be coadded...
Definition: coaddBase.py:232
Configuration parameters for the MeasureMergedCoaddSourcesTask.
Definition: multiBand.py:843
def getSkySourceFootprints
Return a list of Footprints of sky objects which don&#39;t overlap with anything in mergedList.
Definition: multiBand.py:786
def __init__
Initialize the merge detections task.
Definition: multiBand.py:671
def getInputSchema
Obtain the input schema either directly or froma butler reference.
Definition: multiBand.py:421
def _makeArgumentParser
Create a suitable ArgumentParser.
Definition: multiBand.py:407
def getSchemaCatalogs
Return a dict of empty catalogs for each catalog dataset produced by this task.
Definition: multiBand.py:775
def write
Write out results from runDetection.
Definition: multiBand.py:294
def writeMatches
Write matches of the sources to the astrometric reference catalog.
Definition: multiBand.py:1133