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