33 import lsst.meas.extensions.trailedSources
39 from lsst.meas.algorithms import SourceDetectionTask, SingleGaussianPsf, ObjectSizeStarSelectorTask
40 from lsst.ip.diffim import (DipoleAnalysis, SourceFlagChecker, KernelCandidateF, makeKernelBasisList,
41 KernelCandidateQa, DiaCatalogSourceSelectorTask, DiaCatalogSourceSelectorConfig,
42 GetCoaddAsTemplateTask, GetCalexpAsTemplateTask, DipoleFitTask,
43 DecorrelateALKernelSpatialTask, subtractAlgorithmRegistry)
48 from lsst.obs.base
import ExposureIdInfo
50 __all__ = [
"ImageDifferenceConfig",
"ImageDifferenceTask"]
51 FwhmPerSigma = 2*math.sqrt(2*math.log(2))
56 dimensions=(
"instrument",
"visit",
"detector",
"skymap"),
57 defaultTemplates={
"coaddName":
"deep",
62 exposure = pipeBase.connectionTypes.Input(
63 doc=
"Input science exposure to subtract from.",
64 dimensions=(
"instrument",
"visit",
"detector"),
65 storageClass=
"ExposureF",
66 name=
"{fakesType}calexp"
77 skyMap = pipeBase.connectionTypes.Input(
78 doc=
"Input definition of geometry/bbox and projection/wcs for template exposures",
79 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
80 dimensions=(
"skymap", ),
81 storageClass=
"SkyMap",
83 coaddExposures = pipeBase.connectionTypes.Input(
84 doc=
"Input template to match and subtract from the exposure",
85 dimensions=(
"tract",
"patch",
"skymap",
"band"),
86 storageClass=
"ExposureF",
87 name=
"{fakesType}{coaddName}Coadd{warpTypeSuffix}",
91 dcrCoadds = pipeBase.connectionTypes.Input(
92 doc=
"Input DCR template to match and subtract from the exposure",
93 name=
"{fakesType}dcrCoadd{warpTypeSuffix}",
94 storageClass=
"ExposureF",
95 dimensions=(
"tract",
"patch",
"skymap",
"band",
"subfilter"),
99 outputSchema = pipeBase.connectionTypes.InitOutput(
100 doc=
"Schema (as an example catalog) for output DIASource catalog.",
101 storageClass=
"SourceCatalog",
102 name=
"{fakesType}{coaddName}Diff_diaSrc_schema",
104 subtractedExposure = pipeBase.connectionTypes.Output(
105 doc=
"Output AL difference or Zogy proper difference image",
106 dimensions=(
"instrument",
"visit",
"detector"),
107 storageClass=
"ExposureF",
108 name=
"{fakesType}{coaddName}Diff_differenceExp",
110 scoreExposure = pipeBase.connectionTypes.Output(
111 doc=
"Output AL likelihood or Zogy score image",
112 dimensions=(
"instrument",
"visit",
"detector"),
113 storageClass=
"ExposureF",
114 name=
"{fakesType}{coaddName}Diff_scoreExp",
116 warpedExposure = pipeBase.connectionTypes.Output(
117 doc=
"Warped template used to create `subtractedExposure`.",
118 dimensions=(
"instrument",
"visit",
"detector"),
119 storageClass=
"ExposureF",
120 name=
"{fakesType}{coaddName}Diff_warpedExp",
122 matchedExposure = pipeBase.connectionTypes.Output(
123 doc=
"Warped template used to create `subtractedExposure`.",
124 dimensions=(
"instrument",
"visit",
"detector"),
125 storageClass=
"ExposureF",
126 name=
"{fakesType}{coaddName}Diff_matchedExp",
128 diaSources = pipeBase.connectionTypes.Output(
129 doc=
"Output detected diaSources on the difference image",
130 dimensions=(
"instrument",
"visit",
"detector"),
131 storageClass=
"SourceCatalog",
132 name=
"{fakesType}{coaddName}Diff_diaSrc",
135 def __init__(self, *, config=None):
136 super().__init__(config=config)
137 if config.coaddName ==
'dcr':
138 self.inputs.remove(
"coaddExposures")
140 self.inputs.remove(
"dcrCoadds")
141 if not config.doWriteSubtractedExp:
142 self.outputs.remove(
"subtractedExposure")
143 if not config.doWriteScoreExp:
144 self.outputs.remove(
"scoreExposure")
145 if not config.doWriteWarpedExp:
146 self.outputs.remove(
"warpedExposure")
147 if not config.doWriteMatchedExp:
148 self.outputs.remove(
"matchedExposure")
149 if not config.doWriteSources:
150 self.outputs.remove(
"diaSources")
156 class ImageDifferenceConfig(pipeBase.PipelineTaskConfig,
157 pipelineConnections=ImageDifferenceTaskConnections):
158 """Config for ImageDifferenceTask
160 doAddCalexpBackground = pexConfig.Field(dtype=bool, default=
False,
161 doc=
"Add background to calexp before processing it. "
162 "Useful as ipDiffim does background matching.")
163 doUseRegister = pexConfig.Field(dtype=bool, default=
False,
164 doc=
"Re-compute astrometry on the template. "
165 "Use image-to-image registration to align template with "
166 "science image (AL only).")
167 doDebugRegister = pexConfig.Field(dtype=bool, default=
False,
168 doc=
"Writing debugging data for doUseRegister")
169 doSelectSources = pexConfig.Field(dtype=bool, default=
False,
170 doc=
"Select stars to use for kernel fitting (AL only)")
171 doSelectDcrCatalog = pexConfig.Field(dtype=bool, default=
False,
172 doc=
"Select stars of extreme color as part "
173 "of the control sample (AL only)")
174 doSelectVariableCatalog = pexConfig.Field(dtype=bool, default=
False,
175 doc=
"Select stars that are variable to be part "
176 "of the control sample (AL only)")
177 doSubtract = pexConfig.Field(dtype=bool, default=
True, doc=
"Compute subtracted exposure?")
178 doPreConvolve = pexConfig.Field(dtype=bool, default=
False,
179 doc=
"Not in use. Superseded by useScoreImageDetection.",
180 deprecated=
"This option superseded by useScoreImageDetection."
181 " Will be removed after v22.")
182 useScoreImageDetection = pexConfig.Field(
183 dtype=bool, default=
False, doc=
"Calculate the pre-convolved AL likelihood or "
184 "the Zogy score image. Use it for source detection (if doDetection=True).")
185 doWriteScoreExp = pexConfig.Field(
186 dtype=bool, default=
False, doc=
"Write AL likelihood or Zogy score exposure?")
187 doScaleTemplateVariance = pexConfig.Field(dtype=bool, default=
False,
188 doc=
"Scale variance of the template before PSF matching")
189 doScaleDiffimVariance = pexConfig.Field(dtype=bool, default=
True,
190 doc=
"Scale variance of the diffim before PSF matching. "
191 "You may do either this or template variance scaling, "
192 "or neither. (Doing both is a waste of CPU.)")
193 useGaussianForPreConvolution = pexConfig.Field(
194 dtype=bool, default=
False, doc=
"Use a simple gaussian PSF model for pre-convolution "
195 "(oherwise use exposure PSF)? (AL and if useScoreImageDetection=True only)")
196 doDetection = pexConfig.Field(dtype=bool, default=
True, doc=
"Detect sources?")
197 doDecorrelation = pexConfig.Field(dtype=bool, default=
True,
198 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L "
199 "kernel convolution (AL only)? If True, also update the diffim PSF.")
200 doMerge = pexConfig.Field(dtype=bool, default=
True,
201 doc=
"Merge positive and negative diaSources with grow radius "
202 "set by growFootprint")
203 doMatchSources = pexConfig.Field(dtype=bool, default=
False,
204 doc=
"Match diaSources with input calexp sources and ref catalog sources")
205 doMeasurement = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure diaSources?")
206 doDipoleFitting = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure dipoles using new algorithm?")
207 doForcedMeasurement = pexConfig.Field(
210 doc=
"Force photometer diaSource locations on PVI?")
211 doWriteSubtractedExp = pexConfig.Field(
212 dtype=bool, default=
True, doc=
"Write difference exposure (AL and Zogy) ?")
213 doWriteWarpedExp = pexConfig.Field(
214 dtype=bool, default=
False, doc=
"Write WCS, warped template coadd exposure?")
215 doWriteMatchedExp = pexConfig.Field(dtype=bool, default=
False,
216 doc=
"Write warped and PSF-matched template coadd exposure?")
217 doWriteSources = pexConfig.Field(dtype=bool, default=
True, doc=
"Write sources?")
218 doAddMetrics = pexConfig.Field(dtype=bool, default=
False,
219 doc=
"Add columns to the source table to hold analysis metrics?")
221 coaddName = pexConfig.Field(
222 doc=
"coadd name: typically one of deep, goodSeeing, or dcr",
226 convolveTemplate = pexConfig.Field(
227 doc=
"Which image gets convolved (default = template)",
231 refObjLoader = pexConfig.ConfigurableField(
232 target=LoadIndexedReferenceObjectsTask,
233 doc=
"reference object loader",
235 astrometer = pexConfig.ConfigurableField(
236 target=AstrometryTask,
237 doc=
"astrometry task; used to match sources to reference objects, but not to fit a WCS",
239 sourceSelector = pexConfig.ConfigurableField(
240 target=ObjectSizeStarSelectorTask,
241 doc=
"Source selection algorithm",
243 subtract = subtractAlgorithmRegistry.makeField(
"Subtraction Algorithm", default=
"al")
244 decorrelate = pexConfig.ConfigurableField(
245 target=DecorrelateALKernelSpatialTask,
246 doc=
"Decorrelate effects of A&L kernel convolution on image difference, only if doSubtract is True. "
247 "If this option is enabled, then detection.thresholdValue should be set to 5.0 (rather than the "
251 doSpatiallyVarying = pexConfig.Field(
254 doc=
"Perform A&L decorrelation on a grid across the "
255 "image in order to allow for spatial variations. Zogy does not use this option."
257 detection = pexConfig.ConfigurableField(
258 target=SourceDetectionTask,
259 doc=
"Low-threshold detection for final measurement",
261 measurement = pexConfig.ConfigurableField(
262 target=DipoleFitTask,
263 doc=
"Enable updated dipole fitting method",
265 doApCorr = lsst.pex.config.Field(
268 doc=
"Run subtask to apply aperture corrections"
270 applyApCorr = lsst.pex.config.ConfigurableField(
271 target=ApplyApCorrTask,
272 doc=
"Subtask to apply aperture corrections"
274 forcedMeasurement = pexConfig.ConfigurableField(
275 target=ForcedMeasurementTask,
276 doc=
"Subtask to force photometer PVI at diaSource location.",
278 getTemplate = pexConfig.ConfigurableField(
279 target=GetCoaddAsTemplateTask,
280 doc=
"Subtask to retrieve template exposure and sources",
282 scaleVariance = pexConfig.ConfigurableField(
283 target=ScaleVarianceTask,
284 doc=
"Subtask to rescale the variance of the template "
285 "to the statistically expected level"
287 controlStepSize = pexConfig.Field(
288 doc=
"What step size (every Nth one) to select a control sample from the kernelSources",
292 controlRandomSeed = pexConfig.Field(
293 doc=
"Random seed for shuffing the control sample",
297 register = pexConfig.ConfigurableField(
299 doc=
"Task to enable image-to-image image registration (warping)",
301 kernelSourcesFromRef = pexConfig.Field(
302 doc=
"Select sources to measure kernel from reference catalog if True, template if false",
306 templateSipOrder = pexConfig.Field(
307 dtype=int, default=2,
308 doc=
"Sip Order for fitting the Template Wcs (default is too high, overfitting)"
310 growFootprint = pexConfig.Field(
311 dtype=int, default=2,
312 doc=
"Grow positive and negative footprints by this amount before merging"
314 diaSourceMatchRadius = pexConfig.Field(
315 dtype=float, default=0.5,
316 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
318 requiredTemplateFraction = pexConfig.Field(
319 dtype=float, default=0.1,
320 doc=
"Do not attempt to run task if template covers less than this fraction of pixels."
321 "Setting to 0 will always attempt image subtraction"
323 doSkySources = pexConfig.Field(
326 doc=
"Generate sky sources?",
328 skySources = pexConfig.ConfigurableField(
329 target=SkyObjectsTask,
330 doc=
"Generate sky sources",
333 def setDefaults(self):
336 self.subtract[
'al'].kernel.name =
"AL"
337 self.subtract[
'al'].kernel.active.fitForBackground =
True
338 self.subtract[
'al'].kernel.active.spatialKernelOrder = 1
339 self.subtract[
'al'].kernel.active.spatialBgOrder = 2
342 self.detection.thresholdPolarity =
"both"
343 self.detection.thresholdValue = 5.0
344 self.detection.reEstimateBackground =
False
345 self.detection.thresholdType =
"pixel_stdev"
351 self.measurement.algorithms.names.add(
'base_PeakLikelihoodFlux')
352 self.measurement.plugins.names |= [
'ext_trailedSources_Naive',
353 'base_LocalPhotoCalib',
356 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
357 self.forcedMeasurement.copyColumns = {
358 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
359 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
360 self.forcedMeasurement.slots.shape =
None
363 random.seed(self.controlRandomSeed)
366 pexConfig.Config.validate(self)
367 if not self.doSubtract
and not self.doDetection:
368 raise ValueError(
"Either doSubtract or doDetection must be enabled.")
369 if self.doMeasurement
and not self.doDetection:
370 raise ValueError(
"Cannot run source measurement without source detection.")
371 if self.doMerge
and not self.doDetection:
372 raise ValueError(
"Cannot run source merging without source detection.")
373 if self.doSkySources
and not self.doDetection:
374 raise ValueError(
"Cannot run sky source creation without source detection.")
375 if self.doUseRegister
and not self.doSelectSources:
376 raise ValueError(
"doUseRegister=True and doSelectSources=False. "
377 "Cannot run RegisterTask without selecting sources.")
378 if hasattr(self.getTemplate,
"coaddName"):
379 if self.getTemplate.coaddName != self.coaddName:
380 raise ValueError(
"Mis-matched coaddName and getTemplate.coaddName in the config.")
381 if self.doScaleDiffimVariance
and self.doScaleTemplateVariance:
382 raise ValueError(
"Scaling the diffim variance and scaling the template variance "
383 "are both set. Please choose one or the other.")
385 if self.subtract.name ==
'zogy':
386 if self.doWriteMatchedExp:
387 raise ValueError(
"doWriteMatchedExp=True Matched exposure is not "
388 "calculated in zogy subtraction.")
389 if self.doAddMetrics:
390 raise ValueError(
"doAddMetrics=True Kernel metrics does not exist in zogy subtraction.")
391 if self.doDecorrelation:
393 "doDecorrelation=True The decorrelation afterburner does not exist in zogy subtraction.")
394 if self.doSelectSources:
396 "doSelectSources=True Selecting sources for PSF matching is not a zogy option.")
397 if self.useGaussianForPreConvolution:
399 "useGaussianForPreConvolution=True This is an AL subtraction only option.")
402 if self.useScoreImageDetection
and not self.convolveTemplate:
404 "convolveTemplate=False and useScoreImageDetection=True "
405 "Pre-convolution and matching of the science image is not a supported operation.")
406 if self.doWriteSubtractedExp
and self.useScoreImageDetection:
408 "doWriteSubtractedExp=True and useScoreImageDetection=True "
409 "Regular difference image is not calculated. "
410 "AL subtraction calculates either the regular difference image or the score image.")
411 if self.doWriteScoreExp
and not self.useScoreImageDetection:
413 "doWriteScoreExp=True and useScoreImageDetection=False "
414 "Score image is not calculated. "
415 "AL subtraction calculates either the regular difference image or the score image.")
416 if self.doAddMetrics
and not self.doSubtract:
417 raise ValueError(
"Subtraction must be enabled for kernel metrics calculation.")
418 if self.useGaussianForPreConvolution
and not self.useScoreImageDetection:
420 "useGaussianForPreConvolution=True and useScoreImageDetection=False "
421 "Gaussian PSF approximation exists only for AL subtraction w/ pre-convolution.")
424 class ImageDifferenceTaskRunner(pipeBase.ButlerInitializedTaskRunner):
427 def getTargetList(parsedCmd, **kwargs):
428 return pipeBase.TaskRunner.getTargetList(parsedCmd, templateIdList=parsedCmd.templateId.idList,
432 class ImageDifferenceTask(pipeBase.CmdLineTask, pipeBase.PipelineTask):
433 """Subtract an image from a template and measure the result
435 ConfigClass = ImageDifferenceConfig
436 RunnerClass = ImageDifferenceTaskRunner
437 _DefaultName =
"imageDifference"
439 def __init__(self, butler=None, **kwargs):
440 """!Construct an ImageDifference Task
442 @param[in] butler Butler object to use in constructing reference object loaders
444 super().__init__(**kwargs)
445 self.makeSubtask(
"getTemplate")
447 self.makeSubtask(
"subtract")
449 if self.config.subtract.name ==
'al' and self.config.doDecorrelation:
450 self.makeSubtask(
"decorrelate")
452 if self.config.doScaleTemplateVariance
or self.config.doScaleDiffimVariance:
453 self.makeSubtask(
"scaleVariance")
455 if self.config.doUseRegister:
456 self.makeSubtask(
"register")
457 self.schema = afwTable.SourceTable.makeMinimalSchema()
459 if self.config.doSelectSources:
460 self.makeSubtask(
"sourceSelector")
461 if self.config.kernelSourcesFromRef:
462 self.makeSubtask(
'refObjLoader', butler=butler)
463 self.makeSubtask(
"astrometer", refObjLoader=self.refObjLoader)
465 self.algMetadata = dafBase.PropertyList()
466 if self.config.doDetection:
467 self.makeSubtask(
"detection", schema=self.schema)
468 if self.config.doMeasurement:
469 self.makeSubtask(
"measurement", schema=self.schema,
470 algMetadata=self.algMetadata)
471 if self.config.doApCorr:
472 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
473 if self.config.doForcedMeasurement:
474 self.schema.addField(
475 "ip_diffim_forced_PsfFlux_instFlux",
"D",
476 "Forced PSF flux measured on the direct image.",
478 self.schema.addField(
479 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
480 "Forced PSF flux error measured on the direct image.",
482 self.schema.addField(
483 "ip_diffim_forced_PsfFlux_area",
"F",
484 "Forced PSF flux effective area of PSF.",
486 self.schema.addField(
487 "ip_diffim_forced_PsfFlux_flag",
"Flag",
488 "Forced PSF flux general failure flag.")
489 self.schema.addField(
490 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
491 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
492 self.schema.addField(
493 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
494 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
495 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
496 if self.config.doMatchSources:
497 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
498 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
499 if self.config.doSkySources:
500 self.makeSubtask(
"skySources")
501 self.skySourceKey = self.schema.addField(
"sky_source", type=
"Flag", doc=
"Sky objects.")
504 self.outputSchema = afwTable.SourceCatalog(self.schema)
505 self.outputSchema.getTable().setMetadata(self.algMetadata)
508 def makeIdFactory(expId, expBits):
509 """Create IdFactory instance for unique 64 bit diaSource id-s.
517 Number of used bits in ``expId``.
521 The diasource id-s consists of the ``expId`` stored fixed in the highest value
522 ``expBits`` of the 64-bit integer plus (bitwise or) a generated sequence number in the
523 low value end of the integer.
527 idFactory: `lsst.afw.table.IdFactory`
529 return ExposureIdInfo(expId, expBits).makeSourceIdFactory()
531 @lsst.utils.inheritDoc(pipeBase.PipelineTask)
532 def runQuantum(self, butlerQC: pipeBase.ButlerQuantumContext,
533 inputRefs: pipeBase.InputQuantizedConnection,
534 outputRefs: pipeBase.OutputQuantizedConnection):
535 inputs = butlerQC.get(inputRefs)
536 self.log.info(
"Processing %s", butlerQC.quantum.dataId)
537 expId, expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
539 idFactory = self.makeIdFactory(expId=expId, expBits=expBits)
540 if self.config.coaddName ==
'dcr':
541 templateExposures = inputRefs.dcrCoadds
543 templateExposures = inputRefs.coaddExposures
544 templateStruct = self.getTemplate.runQuantum(
545 inputs[
'exposure'], butlerQC, inputRefs.skyMap, templateExposures
548 self.checkTemplateIsSufficient(templateStruct.exposure)
550 outputs = self.run(exposure=inputs[
'exposure'],
551 templateExposure=templateStruct.exposure,
554 if outputs.diaSources
is None:
555 del outputs.diaSources
556 butlerQC.put(outputs, outputRefs)
559 def runDataRef(self, sensorRef, templateIdList=None):
560 """Subtract an image from a template coadd and measure the result.
562 Data I/O wrapper around `run` using the butler in Gen2.
566 sensorRef : `lsst.daf.persistence.ButlerDataRef`
567 Sensor-level butler data reference, used for the following data products:
574 - self.config.coaddName + "Coadd_skyMap"
575 - self.config.coaddName + "Coadd"
576 Input or output, depending on config:
577 - self.config.coaddName + "Diff_subtractedExp"
578 Output, depending on config:
579 - self.config.coaddName + "Diff_matchedExp"
580 - self.config.coaddName + "Diff_src"
584 results : `lsst.pipe.base.Struct`
585 Returns the Struct by `run`.
587 subtractedExposureName = self.config.coaddName +
"Diff_differenceExp"
588 subtractedExposure =
None
590 calexpBackgroundExposure =
None
591 self.log.info(
"Processing %s", sensorRef.dataId)
596 idFactory = self.makeIdFactory(expId=int(sensorRef.get(
"ccdExposureId")),
597 expBits=sensorRef.get(
"ccdExposureId_bits"))
598 if self.config.doAddCalexpBackground:
599 calexpBackgroundExposure = sensorRef.get(
"calexpBackground")
602 exposure = sensorRef.get(
"calexp", immediate=
True)
605 template = self.getTemplate.runDataRef(exposure, sensorRef, templateIdList=templateIdList)
607 if sensorRef.datasetExists(
"src"):
608 self.log.info(
"Source selection via src product")
610 selectSources = sensorRef.get(
"src")
612 if not self.config.doSubtract
and self.config.doDetection:
614 subtractedExposure = sensorRef.get(subtractedExposureName)
617 results = self.run(exposure=exposure,
618 selectSources=selectSources,
619 templateExposure=template.exposure,
620 templateSources=template.sources,
622 calexpBackgroundExposure=calexpBackgroundExposure,
623 subtractedExposure=subtractedExposure)
625 if self.config.doWriteSources
and results.diaSources
is not None:
626 sensorRef.put(results.diaSources, self.config.coaddName +
"Diff_diaSrc")
627 if self.config.doWriteWarpedExp:
628 sensorRef.put(results.warpedExposure, self.config.coaddName +
"Diff_warpedExp")
629 if self.config.doWriteMatchedExp:
630 sensorRef.put(results.matchedExposure, self.config.coaddName +
"Diff_matchedExp")
631 if self.config.doAddMetrics
and self.config.doSelectSources:
632 sensorRef.put(results.selectSources, self.config.coaddName +
"Diff_kernelSrc")
633 if self.config.doWriteSubtractedExp:
634 sensorRef.put(results.subtractedExposure, subtractedExposureName)
635 if self.config.doWriteScoreExp:
636 sensorRef.put(results.scoreExposure, self.config.coaddName +
"Diff_scoreExp")
640 def run(self, exposure=None, selectSources=None, templateExposure=None, templateSources=None,
641 idFactory=None, calexpBackgroundExposure=None, subtractedExposure=None):
642 """PSF matches, subtract two images and perform detection on the difference image.
646 exposure : `lsst.afw.image.ExposureF`, optional
647 The science exposure, the minuend in the image subtraction.
648 Can be None only if ``config.doSubtract==False``.
649 selectSources : `lsst.afw.table.SourceCatalog`, optional
650 Identified sources on the science exposure. This catalog is used to
651 select sources in order to perform the AL PSF matching on stamp images
652 around them. The selection steps depend on config options and whether
653 ``templateSources`` and ``matchingSources`` specified.
654 templateExposure : `lsst.afw.image.ExposureF`, optional
655 The template to be subtracted from ``exposure`` in the image subtraction.
656 ``templateExposure`` is modified in place if ``config.doScaleTemplateVariance==True``.
657 The template exposure should cover the same sky area as the science exposure.
658 It is either a stich of patches of a coadd skymap image or a calexp
659 of the same pointing as the science exposure. Can be None only
660 if ``config.doSubtract==False`` and ``subtractedExposure`` is not None.
661 templateSources : `lsst.afw.table.SourceCatalog`, optional
662 Identified sources on the template exposure.
663 idFactory : `lsst.afw.table.IdFactory`
664 Generator object to assign ids to detected sources in the difference image.
665 calexpBackgroundExposure : `lsst.afw.image.ExposureF`, optional
666 Background exposure to be added back to the science exposure
667 if ``config.doAddCalexpBackground==True``
668 subtractedExposure : `lsst.afw.image.ExposureF`, optional
669 If ``config.doSubtract==False`` and ``config.doDetection==True``,
670 performs the post subtraction source detection only on this exposure.
671 Otherwise should be None.
675 results : `lsst.pipe.base.Struct`
676 ``subtractedExposure`` : `lsst.afw.image.ExposureF`
678 ``scoreExposure`` : `lsst.afw.image.ExposureF` or `None`
679 The zogy score exposure, if calculated.
680 ``matchedExposure`` : `lsst.afw.image.ExposureF`
681 The matched PSF exposure.
682 ``subtractRes`` : `lsst.pipe.base.Struct`
683 The returned result structure of the ImagePsfMatchTask subtask.
684 ``diaSources`` : `lsst.afw.table.SourceCatalog`
685 The catalog of detected sources.
686 ``selectSources`` : `lsst.afw.table.SourceCatalog`
687 The input source catalog with optionally added Qa information.
691 The following major steps are included:
693 - warp template coadd to match WCS of image
694 - PSF match image to warped template
695 - subtract image from PSF-matched, warped template
699 For details about the image subtraction configuration modes
700 see `lsst.ip.diffim`.
703 controlSources =
None
704 subtractedExposure =
None
709 exposureOrig = exposure
711 if self.config.doAddCalexpBackground:
712 mi = exposure.getMaskedImage()
713 mi += calexpBackgroundExposure.getImage()
715 if not exposure.hasPsf():
716 raise pipeBase.TaskError(
"Exposure has no psf")
717 sciencePsf = exposure.getPsf()
719 if self.config.doSubtract:
720 if self.config.doScaleTemplateVariance:
721 self.log.info(
"Rescaling template variance")
722 templateVarFactor = self.scaleVariance.run(
723 templateExposure.getMaskedImage())
724 self.log.info(
"Template variance scaling factor: %.2f", templateVarFactor)
725 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
726 self.metadata.add(
"psfMatchingAlgorithm", self.config.subtract.name)
728 if self.config.subtract.name ==
'zogy':
729 subtractRes = self.subtract.run(exposure, templateExposure, doWarping=
True)
730 scoreExposure = subtractRes.scoreExp
731 subtractedExposure = subtractRes.diffExp
732 subtractRes.subtractedExposure = subtractedExposure
733 subtractRes.matchedExposure =
None
735 elif self.config.subtract.name ==
'al':
737 scienceSigmaOrig = sciencePsf.computeShape().getDeterminantRadius()
738 templateSigma = templateExposure.getPsf().computeShape().getDeterminantRadius()
746 if self.config.useScoreImageDetection:
747 self.log.warn(
"AL likelihood image: pre-convolution of PSF is not implemented.")
748 convControl = afwMath.ConvolutionControl()
750 srcMI = exposure.maskedImage
751 exposure = exposure.clone()
753 if self.config.useGaussianForPreConvolution:
755 "AL likelihood image: Using Gaussian (sigma={:.2f}) PSF estimation "
756 "for science image pre-convolution", scienceSigmaOrig)
758 kWidth, kHeight = sciencePsf.getLocalKernel().getDimensions()
763 "AL likelihood image: Using the science image PSF for pre-convolution.")
765 afwMath.convolve(exposure.maskedImage, srcMI, preConvPsf.getLocalKernel(), convControl)
766 scienceSigmaPost = scienceSigmaOrig*math.sqrt(2)
768 scienceSigmaPost = scienceSigmaOrig
773 if self.config.doSelectSources:
774 if selectSources
is None:
775 self.log.warning(
"Src product does not exist; running detection, measurement,"
778 selectSources = self.subtract.getSelectSources(
780 sigma=scienceSigmaPost,
781 doSmooth=
not self.config.useScoreImageDetection,
785 if self.config.doAddMetrics:
788 nparam = len(makeKernelBasisList(self.subtract.config.kernel.active,
789 referenceFwhmPix=scienceSigmaPost*FwhmPerSigma,
790 targetFwhmPix=templateSigma*FwhmPerSigma))
797 kcQa = KernelCandidateQa(nparam)
798 selectSources = kcQa.addToSchema(selectSources)
799 if self.config.kernelSourcesFromRef:
801 astromRet = self.astrometer.loadAndMatch(exposure=exposure, sourceCat=selectSources)
802 matches = astromRet.matches
803 elif templateSources:
805 mc = afwTable.MatchControl()
806 mc.findOnlyClosest =
False
807 matches = afwTable.matchRaDec(templateSources, selectSources, 1.0*geom.arcseconds,
810 raise RuntimeError(
"doSelectSources=True and kernelSourcesFromRef=False,"
811 "but template sources not available. Cannot match science "
812 "sources with template sources. Run process* on data from "
813 "which templates are built.")
815 kernelSources = self.sourceSelector.run(selectSources, exposure=exposure,
816 matches=matches).sourceCat
817 random.shuffle(kernelSources, random.random)
818 controlSources = kernelSources[::self.config.controlStepSize]
819 kernelSources = [k
for i, k
in enumerate(kernelSources)
820 if i % self.config.controlStepSize]
822 if self.config.doSelectDcrCatalog:
823 redSelector = DiaCatalogSourceSelectorTask(
824 DiaCatalogSourceSelectorConfig(grMin=self.sourceSelector.config.grMax,
826 redSources = redSelector.selectStars(exposure, selectSources, matches=matches).starCat
827 controlSources.extend(redSources)
829 blueSelector = DiaCatalogSourceSelectorTask(
830 DiaCatalogSourceSelectorConfig(grMin=-99.999,
831 grMax=self.sourceSelector.config.grMin))
832 blueSources = blueSelector.selectStars(exposure, selectSources,
833 matches=matches).starCat
834 controlSources.extend(blueSources)
836 if self.config.doSelectVariableCatalog:
837 varSelector = DiaCatalogSourceSelectorTask(
838 DiaCatalogSourceSelectorConfig(includeVariable=
True))
839 varSources = varSelector.selectStars(exposure, selectSources, matches=matches).starCat
840 controlSources.extend(varSources)
842 self.log.info(
"Selected %d / %d sources for Psf matching (%d for control sample)",
843 len(kernelSources), len(selectSources), len(controlSources))
847 if self.config.doUseRegister:
848 self.log.info(
"Registering images")
850 if templateSources
is None:
854 templateSigma = templateExposure.getPsf().computeShape().getDeterminantRadius()
855 templateSources = self.subtract.getSelectSources(
864 wcsResults = self.fitAstrometry(templateSources, templateExposure, selectSources)
865 warpedExp = self.register.warpExposure(templateExposure, wcsResults.wcs,
866 exposure.getWcs(), exposure.getBBox())
867 templateExposure = warpedExp
872 if self.config.doDebugRegister:
874 srcToMatch = {x.second.getId(): x.first
for x
in matches}
876 refCoordKey = wcsResults.matches[0].first.getTable().getCoordKey()
877 inCentroidKey = wcsResults.matches[0].second.getTable().getCentroidSlot().getMeasKey()
878 sids = [m.first.getId()
for m
in wcsResults.matches]
879 positions = [m.first.get(refCoordKey)
for m
in wcsResults.matches]
880 residuals = [m.first.get(refCoordKey).getOffsetFrom(wcsResults.wcs.pixelToSky(
881 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
882 allresids = dict(zip(sids, zip(positions, residuals)))
884 cresiduals = [m.first.get(refCoordKey).getTangentPlaneOffset(
885 wcsResults.wcs.pixelToSky(
886 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
887 colors = numpy.array([-2.5*numpy.log10(srcToMatch[x].get(
"g"))
888 + 2.5*numpy.log10(srcToMatch[x].get(
"r"))
889 for x
in sids
if x
in srcToMatch.keys()])
890 dlong = numpy.array([r[0].asArcseconds()
for s, r
in zip(sids, cresiduals)
891 if s
in srcToMatch.keys()])
892 dlat = numpy.array([r[1].asArcseconds()
for s, r
in zip(sids, cresiduals)
893 if s
in srcToMatch.keys()])
894 idx1 = numpy.where(colors < self.sourceSelector.config.grMin)
895 idx2 = numpy.where((colors >= self.sourceSelector.config.grMin)
896 & (colors <= self.sourceSelector.config.grMax))
897 idx3 = numpy.where(colors > self.sourceSelector.config.grMax)
898 rms1Long = IqrToSigma*(
899 (numpy.percentile(dlong[idx1], 75) - numpy.percentile(dlong[idx1], 25)))
900 rms1Lat = IqrToSigma*(numpy.percentile(dlat[idx1], 75)
901 - numpy.percentile(dlat[idx1], 25))
902 rms2Long = IqrToSigma*(
903 (numpy.percentile(dlong[idx2], 75) - numpy.percentile(dlong[idx2], 25)))
904 rms2Lat = IqrToSigma*(numpy.percentile(dlat[idx2], 75)
905 - numpy.percentile(dlat[idx2], 25))
906 rms3Long = IqrToSigma*(
907 (numpy.percentile(dlong[idx3], 75) - numpy.percentile(dlong[idx3], 25)))
908 rms3Lat = IqrToSigma*(numpy.percentile(dlat[idx3], 75)
909 - numpy.percentile(dlat[idx3], 25))
910 self.log.info(
"Blue star offsets'': %.3f %.3f, %.3f %.3f",
911 numpy.median(dlong[idx1]), rms1Long,
912 numpy.median(dlat[idx1]), rms1Lat)
913 self.log.info(
"Green star offsets'': %.3f %.3f, %.3f %.3f",
914 numpy.median(dlong[idx2]), rms2Long,
915 numpy.median(dlat[idx2]), rms2Lat)
916 self.log.info(
"Red star offsets'': %.3f %.3f, %.3f %.3f",
917 numpy.median(dlong[idx3]), rms3Long,
918 numpy.median(dlat[idx3]), rms3Lat)
920 self.metadata.add(
"RegisterBlueLongOffsetMedian", numpy.median(dlong[idx1]))
921 self.metadata.add(
"RegisterGreenLongOffsetMedian", numpy.median(dlong[idx2]))
922 self.metadata.add(
"RegisterRedLongOffsetMedian", numpy.median(dlong[idx3]))
923 self.metadata.add(
"RegisterBlueLongOffsetStd", rms1Long)
924 self.metadata.add(
"RegisterGreenLongOffsetStd", rms2Long)
925 self.metadata.add(
"RegisterRedLongOffsetStd", rms3Long)
927 self.metadata.add(
"RegisterBlueLatOffsetMedian", numpy.median(dlat[idx1]))
928 self.metadata.add(
"RegisterGreenLatOffsetMedian", numpy.median(dlat[idx2]))
929 self.metadata.add(
"RegisterRedLatOffsetMedian", numpy.median(dlat[idx3]))
930 self.metadata.add(
"RegisterBlueLatOffsetStd", rms1Lat)
931 self.metadata.add(
"RegisterGreenLatOffsetStd", rms2Lat)
932 self.metadata.add(
"RegisterRedLatOffsetStd", rms3Lat)
939 self.log.info(
"Subtracting images")
940 subtractRes = self.subtract.subtractExposures(
941 templateExposure=templateExposure,
942 scienceExposure=exposure,
943 candidateList=kernelSources,
944 convolveTemplate=self.config.convolveTemplate,
945 doWarping=
not self.config.doUseRegister
947 if self.config.useScoreImageDetection:
948 scoreExposure = subtractRes.subtractedExposure
950 subtractedExposure = subtractRes.subtractedExposure
952 if self.config.doDetection:
953 self.log.info(
"Computing diffim PSF")
956 if subtractedExposure
is not None and not subtractedExposure.hasPsf():
957 if self.config.convolveTemplate:
958 subtractedExposure.setPsf(exposure.getPsf())
960 subtractedExposure.setPsf(templateExposure.getPsf())
967 if self.config.doDecorrelation
and self.config.doSubtract:
969 if self.config.useGaussianForPreConvolution:
970 preConvKernel = preConvPsf.getLocalKernel()
971 if self.config.useScoreImageDetection:
972 scoreExposure = self.decorrelate.run(exposureOrig, subtractRes.warpedExposure,
974 subtractRes.psfMatchingKernel,
975 spatiallyVarying=self.config.doSpatiallyVarying,
976 preConvKernel=preConvKernel,
977 templateMatched=
True,
978 preConvMode=
True).correctedExposure
981 subtractedExposure = self.decorrelate.run(exposureOrig, subtractRes.warpedExposure,
983 subtractRes.psfMatchingKernel,
984 spatiallyVarying=self.config.doSpatiallyVarying,
986 templateMatched=self.config.convolveTemplate,
987 preConvMode=
False).correctedExposure
990 if self.config.doDetection:
991 self.log.info(
"Running diaSource detection")
999 if self.config.useScoreImageDetection:
1001 self.log.info(
"Detection, diffim rescaling and measurements are "
1002 "on AL likelihood or Zogy score image.")
1003 detectionExposure = scoreExposure
1006 detectionExposure = subtractedExposure
1009 if self.config.doScaleDiffimVariance:
1010 self.log.info(
"Rescaling diffim variance")
1011 diffimVarFactor = self.scaleVariance.run(detectionExposure.getMaskedImage())
1012 self.log.info(
"Diffim variance scaling factor: %.2f", diffimVarFactor)
1013 self.metadata.add(
"scaleDiffimVarianceFactor", diffimVarFactor)
1016 mask = detectionExposure.getMaskedImage().getMask()
1017 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
1019 table = afwTable.SourceTable.make(self.schema, idFactory)
1020 table.setMetadata(self.algMetadata)
1021 results = self.detection.run(
1023 exposure=detectionExposure,
1024 doSmooth=
not self.config.useScoreImageDetection
1027 if self.config.doMerge:
1028 fpSet = results.fpSets.positive
1029 fpSet.merge(results.fpSets.negative, self.config.growFootprint,
1030 self.config.growFootprint,
False)
1031 diaSources = afwTable.SourceCatalog(table)
1032 fpSet.makeSources(diaSources)
1033 self.log.info(
"Merging detections into %d sources", len(diaSources))
1035 diaSources = results.sources
1037 if self.config.doSkySources:
1038 skySourceFootprints = self.skySources.run(
1039 mask=detectionExposure.mask,
1040 seed=detectionExposure.getInfo().getVisitInfo().getExposureId())
1041 if skySourceFootprints:
1042 for foot
in skySourceFootprints:
1043 s = diaSources.addNew()
1044 s.setFootprint(foot)
1045 s.set(self.skySourceKey,
True)
1047 if self.config.doMeasurement:
1048 newDipoleFitting = self.config.doDipoleFitting
1049 self.log.info(
"Running diaSource measurement: newDipoleFitting=%r", newDipoleFitting)
1050 if not newDipoleFitting:
1052 self.measurement.run(diaSources, detectionExposure)
1055 if self.config.doSubtract
and 'matchedExposure' in subtractRes.getDict():
1056 self.measurement.run(diaSources, detectionExposure, exposure,
1057 subtractRes.matchedExposure)
1059 self.measurement.run(diaSources, detectionExposure, exposure)
1060 if self.config.doApCorr:
1061 self.applyApCorr.run(
1063 apCorrMap=detectionExposure.getInfo().getApCorrMap()
1066 if self.config.doForcedMeasurement:
1069 forcedSources = self.forcedMeasurement.generateMeasCat(
1070 exposure, diaSources, detectionExposure.getWcs())
1071 self.forcedMeasurement.run(forcedSources, exposure, diaSources, detectionExposure.getWcs())
1072 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
1073 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
1074 "ip_diffim_forced_PsfFlux_instFlux",
True)
1075 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
1076 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
1077 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
1078 "ip_diffim_forced_PsfFlux_area",
True)
1079 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
1080 "ip_diffim_forced_PsfFlux_flag",
True)
1081 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
1082 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
1083 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
1084 "ip_diffim_forced_PsfFlux_flag_edge",
True)
1085 for diaSource, forcedSource
in zip(diaSources, forcedSources):
1086 diaSource.assign(forcedSource, mapper)
1089 if self.config.doMatchSources:
1090 if selectSources
is not None:
1092 matchRadAsec = self.config.diaSourceMatchRadius
1093 matchRadPixel = matchRadAsec/exposure.getWcs().getPixelScale().asArcseconds()
1095 srcMatches = afwTable.matchXy(selectSources, diaSources, matchRadPixel)
1096 srcMatchDict = dict([(srcMatch.second.getId(), srcMatch.first.getId())
for
1097 srcMatch
in srcMatches])
1098 self.log.info(
"Matched %d / %d diaSources to sources",
1099 len(srcMatchDict), len(diaSources))
1101 self.log.warning(
"Src product does not exist; cannot match with diaSources")
1105 refAstromConfig = AstrometryConfig()
1106 refAstromConfig.matcher.maxMatchDistArcSec = matchRadAsec
1107 refAstrometer = AstrometryTask(refAstromConfig)
1108 astromRet = refAstrometer.run(exposure=exposure, sourceCat=diaSources)
1109 refMatches = astromRet.matches
1110 if refMatches
is None:
1111 self.log.warning(
"No diaSource matches with reference catalog")
1114 self.log.info(
"Matched %d / %d diaSources to reference catalog",
1115 len(refMatches), len(diaSources))
1116 refMatchDict = dict([(refMatch.second.getId(), refMatch.first.getId())
for
1117 refMatch
in refMatches])
1120 for diaSource
in diaSources:
1121 sid = diaSource.getId()
1122 if sid
in srcMatchDict:
1123 diaSource.set(
"srcMatchId", srcMatchDict[sid])
1124 if sid
in refMatchDict:
1125 diaSource.set(
"refMatchId", refMatchDict[sid])
1127 if self.config.doAddMetrics
and self.config.doSelectSources:
1128 self.log.info(
"Evaluating metrics and control sample")
1131 for cell
in subtractRes.kernelCellSet.getCellList():
1132 for cand
in cell.begin(
False):
1133 kernelCandList.append(cand)
1136 basisList = kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelList()
1137 nparam = len(kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelParameters())
1140 diffimTools.sourceTableToCandidateList(controlSources,
1141 subtractRes.warpedExposure, exposure,
1142 self.config.subtract.kernel.active,
1143 self.config.subtract.kernel.active.detectionConfig,
1144 self.log, doBuild=
True, basisList=basisList))
1146 KernelCandidateQa.apply(kernelCandList, subtractRes.psfMatchingKernel,
1147 subtractRes.backgroundModel, dof=nparam)
1148 KernelCandidateQa.apply(controlCandList, subtractRes.psfMatchingKernel,
1149 subtractRes.backgroundModel)
1151 if self.config.doDetection:
1152 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids, diaSources)
1154 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids)
1156 self.runDebug(exposure, subtractRes, selectSources, kernelSources, diaSources)
1157 return pipeBase.Struct(
1158 subtractedExposure=subtractedExposure,
1159 scoreExposure=scoreExposure,
1160 warpedExposure=subtractRes.warpedExposure,
1161 matchedExposure=subtractRes.matchedExposure,
1162 subtractRes=subtractRes,
1163 diaSources=diaSources,
1164 selectSources=selectSources
1167 def fitAstrometry(self, templateSources, templateExposure, selectSources):
1168 """Fit the relative astrometry between templateSources and selectSources
1173 Remove this method. It originally fit a new WCS to the template before calling register.run
1174 because our TAN-SIP fitter behaved badly for points far from CRPIX, but that's been fixed.
1175 It remains because a subtask overrides it.
1177 results = self.register.run(templateSources, templateExposure.getWcs(),
1178 templateExposure.getBBox(), selectSources)
1181 def runDebug(self, exposure, subtractRes, selectSources, kernelSources, diaSources):
1182 """Make debug plots and displays.
1186 Test and update for current debug display and slot names
1196 disp = afwDisplay.getDisplay(frame=lsstDebug.frame)
1197 if not maskTransparency:
1198 maskTransparency = 0
1199 disp.setMaskTransparency(maskTransparency)
1201 if display
and showSubtracted:
1202 disp.mtv(subtractRes.subtractedExposure, title=
"Subtracted image")
1203 mi = subtractRes.subtractedExposure.getMaskedImage()
1204 x0, y0 = mi.getX0(), mi.getY0()
1205 with disp.Buffering():
1206 for s
in diaSources:
1207 x, y = s.getX() - x0, s.getY() - y0
1208 ctype =
"red" if s.get(
"flags_negative")
else "yellow"
1209 if (s.get(
"base_PixelFlags_flag_interpolatedCenter")
1210 or s.get(
"base_PixelFlags_flag_saturatedCenter")
1211 or s.get(
"base_PixelFlags_flag_crCenter")):
1213 elif (s.get(
"base_PixelFlags_flag_interpolated")
1214 or s.get(
"base_PixelFlags_flag_saturated")
1215 or s.get(
"base_PixelFlags_flag_cr")):
1219 disp.dot(ptype, x, y, size=4, ctype=ctype)
1220 lsstDebug.frame += 1
1222 if display
and showPixelResiduals
and selectSources:
1223 nonKernelSources = []
1224 for source
in selectSources:
1225 if source
not in kernelSources:
1226 nonKernelSources.append(source)
1228 diUtils.plotPixelResiduals(exposure,
1229 subtractRes.warpedExposure,
1230 subtractRes.subtractedExposure,
1231 subtractRes.kernelCellSet,
1232 subtractRes.psfMatchingKernel,
1233 subtractRes.backgroundModel,
1235 self.subtract.config.kernel.active.detectionConfig,
1237 diUtils.plotPixelResiduals(exposure,
1238 subtractRes.warpedExposure,
1239 subtractRes.subtractedExposure,
1240 subtractRes.kernelCellSet,
1241 subtractRes.psfMatchingKernel,
1242 subtractRes.backgroundModel,
1244 self.subtract.config.kernel.active.detectionConfig,
1246 if display
and showDiaSources:
1247 flagChecker = SourceFlagChecker(diaSources)
1248 isFlagged = [flagChecker(x)
for x
in diaSources]
1249 isDipole = [x.get(
"ip_diffim_ClassificationDipole_value")
for x
in diaSources]
1250 diUtils.showDiaSources(diaSources, subtractRes.subtractedExposure, isFlagged, isDipole,
1251 frame=lsstDebug.frame)
1252 lsstDebug.frame += 1
1254 if display
and showDipoles:
1255 DipoleAnalysis().displayDipoles(subtractRes.subtractedExposure, diaSources,
1256 frame=lsstDebug.frame)
1257 lsstDebug.frame += 1
1259 def checkTemplateIsSufficient(self, templateExposure):
1260 """Raise NoWorkFound if template coverage < requiredTemplateFraction
1264 templateExposure : `lsst.afw.image.ExposureF`
1265 The template exposure to check
1270 Raised if fraction of good pixels, defined as not having NO_DATA
1271 set, is less then the configured requiredTemplateFraction
1275 pixNoData = numpy.count_nonzero(templateExposure.mask.array
1276 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
1277 pixGood = templateExposure.getBBox().getArea() - pixNoData
1278 self.log.info(
"template has %d good pixels (%.1f%%)", pixGood,
1279 100*pixGood/templateExposure.getBBox().getArea())
1281 if pixGood/templateExposure.getBBox().getArea() < self.config.requiredTemplateFraction:
1282 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%) Not attempting subtraction. "
1283 "To force subtraction, set config requiredTemplateFraction=0." % (
1284 100*pixGood/templateExposure.getBBox().getArea(),
1285 100*self.config.requiredTemplateFraction))
1286 raise pipeBase.NoWorkFound(message)
1288 def _getConfigName(self):
1289 """Return the name of the config dataset
1291 return "%sDiff_config" % (self.config.coaddName,)
1293 def _getMetadataName(self):
1294 """Return the name of the metadata dataset
1296 return "%sDiff_metadata" % (self.config.coaddName,)
1298 def getSchemaCatalogs(self):
1299 """Return a dict of empty catalogs for each catalog dataset produced by this task."""
1300 return {self.config.coaddName +
"Diff_diaSrc": self.outputSchema}
1303 def _makeArgumentParser(cls):
1304 """Create an argument parser
1306 parser = pipeBase.ArgumentParser(name=cls._DefaultName)
1307 parser.add_id_argument(
"--id",
"calexp", help=
"data ID, e.g. --id visit=12345 ccd=1,2")
1308 parser.add_id_argument(
"--templateId",
"calexp", doMakeDataRefList=
True,
1309 help=
"Template data ID in case of calexp template,"
1310 " e.g. --templateId visit=6789")
1315 defaultTemplates={
"coaddName":
"goodSeeing"}
1317 inputTemplate = pipeBase.connectionTypes.Input(
1318 doc=(
"Warped template produced by GetMultiTractCoaddTemplate"),
1319 dimensions=(
"instrument",
"visit",
"detector"),
1320 storageClass=
"ExposureF",
1321 name=
"{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
1324 def __init__(self, *, config=None):
1325 super().__init__(config=config)
1328 if "coaddExposures" in self.inputs:
1329 self.inputs.remove(
"coaddExposures")
1330 if "dcrCoadds" in self.inputs:
1331 self.inputs.remove(
"dcrCoadds")
1335 pipelineConnections=ImageDifferenceFromTemplateConnections):
1340 ConfigClass = ImageDifferenceFromTemplateConfig
1341 _DefaultName =
"imageDifference"
1343 @lsst.utils.inheritDoc(pipeBase.PipelineTask)
1345 inputs = butlerQC.get(inputRefs)
1346 self.log.info(
"Processing %s", butlerQC.quantum.dataId)
1347 self.checkTemplateIsSufficient(inputs[
'inputTemplate'])
1348 expId, expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
1350 idFactory = self.makeIdFactory(expId=expId, expBits=expBits)
1352 outputs = self.run(exposure=inputs[
'exposure'],
1353 templateExposure=inputs[
'inputTemplate'],
1354 idFactory=idFactory)
1357 if outputs.diaSources
is None:
1358 del outputs.diaSources
1359 butlerQC.put(outputs, outputRefs)
1363 winter2013WcsShift = pexConfig.Field(dtype=float, default=0.0,
1364 doc=
"Shift stars going into RegisterTask by this amount")
1365 winter2013WcsRms = pexConfig.Field(dtype=float, default=0.0,
1366 doc=
"Perturb stars going into RegisterTask by this amount")
1369 ImageDifferenceConfig.setDefaults(self)
1370 self.getTemplate.retarget(GetCalexpAsTemplateTask)
1374 """!Image difference Task used in the Winter 2013 data challege.
1375 Enables testing the effects of registration shifts and scatter.
1377 For use with winter 2013 simulated images:
1378 Use --templateId visit=88868666 for sparse data
1379 --templateId visit=22222200 for dense data (g)
1380 --templateId visit=11111100 for dense data (i)
1382 ConfigClass = Winter2013ImageDifferenceConfig
1383 _DefaultName =
"winter2013ImageDifference"
1386 ImageDifferenceTask.__init__(self, **kwargs)
1389 """Fit the relative astrometry between templateSources and selectSources"""
1390 if self.config.winter2013WcsShift > 0.0:
1392 self.config.winter2013WcsShift)
1393 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1394 for source
in templateSources:
1395 centroid = source.get(cKey)
1396 source.set(cKey, centroid + offset)
1397 elif self.config.winter2013WcsRms > 0.0:
1398 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1399 for source
in templateSources:
1400 offset =
geom.Extent2D(self.config.winter2013WcsRms*numpy.random.normal(),
1401 self.config.winter2013WcsRms*numpy.random.normal())
1402 centroid = source.get(cKey)
1403 source.set(cKey, centroid + offset)
1405 results = self.register.run(templateSources, templateExposure.getWcs(),
1406 templateExposure.getBBox(), selectSources)
def runQuantum(self, butlerQC, inputRefs, outputRefs)
Image difference Task used in the Winter 2013 data challege.
def __init__(self, **kwargs)
def fitAstrometry(self, templateSources, templateExposure, selectSources)