38 from lsst.meas.algorithms import SourceDetectionTask, SingleGaussianPsf, ObjectSizeStarSelectorTask
39 from lsst.ip.diffim import (DipoleAnalysis, SourceFlagChecker, KernelCandidateF, makeKernelBasisList,
40 KernelCandidateQa, DiaCatalogSourceSelectorTask, DiaCatalogSourceSelectorConfig,
41 GetCoaddAsTemplateTask, GetCalexpAsTemplateTask, DipoleFitTask,
42 DecorrelateALKernelSpatialTask, subtractAlgorithmRegistry)
47 from lsst.obs.base
import ExposureIdInfo
49 __all__ = [
"ImageDifferenceConfig",
"ImageDifferenceTask"]
50 FwhmPerSigma = 2*math.sqrt(2*math.log(2))
55 dimensions=(
"instrument",
"visit",
"detector",
"skymap"),
56 defaultTemplates={
"coaddName":
"deep",
61 exposure = pipeBase.connectionTypes.Input(
62 doc=
"Input science exposure to subtract from.",
63 dimensions=(
"instrument",
"visit",
"detector"),
64 storageClass=
"ExposureF",
65 name=
"{fakesType}calexp"
76 skyMap = pipeBase.connectionTypes.Input(
77 doc=
"Input definition of geometry/bbox and projection/wcs for template exposures",
78 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
79 dimensions=(
"skymap", ),
80 storageClass=
"SkyMap",
82 coaddExposures = pipeBase.connectionTypes.Input(
83 doc=
"Input template to match and subtract from the exposure",
84 dimensions=(
"tract",
"patch",
"skymap",
"band"),
85 storageClass=
"ExposureF",
86 name=
"{fakesType}{coaddName}Coadd{warpTypeSuffix}",
90 dcrCoadds = pipeBase.connectionTypes.Input(
91 doc=
"Input DCR template to match and subtract from the exposure",
92 name=
"{fakesType}dcrCoadd{warpTypeSuffix}",
93 storageClass=
"ExposureF",
94 dimensions=(
"tract",
"patch",
"skymap",
"band",
"subfilter"),
98 outputSchema = pipeBase.connectionTypes.InitOutput(
99 doc=
"Schema (as an example catalog) for output DIASource catalog.",
100 storageClass=
"SourceCatalog",
101 name=
"{fakesType}{coaddName}Diff_diaSrc_schema",
103 subtractedExposure = pipeBase.connectionTypes.Output(
104 doc=
"Output AL difference or Zogy proper difference image",
105 dimensions=(
"instrument",
"visit",
"detector"),
106 storageClass=
"ExposureF",
107 name=
"{fakesType}{coaddName}Diff_differenceExp",
109 scoreExposure = pipeBase.connectionTypes.Output(
110 doc=
"Output AL likelihood or Zogy score image",
111 dimensions=(
"instrument",
"visit",
"detector"),
112 storageClass=
"ExposureF",
113 name=
"{fakesType}{coaddName}Diff_scoreExp",
115 warpedExposure = pipeBase.connectionTypes.Output(
116 doc=
"Warped template used to create `subtractedExposure`.",
117 dimensions=(
"instrument",
"visit",
"detector"),
118 storageClass=
"ExposureF",
119 name=
"{fakesType}{coaddName}Diff_warpedExp",
121 matchedExposure = pipeBase.connectionTypes.Output(
122 doc=
"Warped template used to create `subtractedExposure`.",
123 dimensions=(
"instrument",
"visit",
"detector"),
124 storageClass=
"ExposureF",
125 name=
"{fakesType}{coaddName}Diff_matchedExp",
127 diaSources = pipeBase.connectionTypes.Output(
128 doc=
"Output detected diaSources on the difference image",
129 dimensions=(
"instrument",
"visit",
"detector"),
130 storageClass=
"SourceCatalog",
131 name=
"{fakesType}{coaddName}Diff_diaSrc",
134 def __init__(self, *, config=None):
135 super().__init__(config=config)
136 if config.coaddName ==
'dcr':
137 self.inputs.remove(
"coaddExposures")
139 self.inputs.remove(
"dcrCoadds")
140 if not config.doWriteSubtractedExp:
141 self.outputs.remove(
"subtractedExposure")
142 if not config.doWriteScoreExp:
143 self.outputs.remove(
"scoreExposure")
144 if not config.doWriteWarpedExp:
145 self.outputs.remove(
"warpedExposure")
146 if not config.doWriteMatchedExp:
147 self.outputs.remove(
"matchedExposure")
148 if not config.doWriteSources:
149 self.outputs.remove(
"diaSources")
155 class ImageDifferenceConfig(pipeBase.PipelineTaskConfig,
156 pipelineConnections=ImageDifferenceTaskConnections):
157 """Config for ImageDifferenceTask
159 doAddCalexpBackground = pexConfig.Field(dtype=bool, default=
False,
160 doc=
"Add background to calexp before processing it. "
161 "Useful as ipDiffim does background matching.")
162 doUseRegister = pexConfig.Field(dtype=bool, default=
False,
163 doc=
"Re-compute astrometry on the template. "
164 "Use image-to-image registration to align template with "
165 "science image (AL only).")
166 doDebugRegister = pexConfig.Field(dtype=bool, default=
False,
167 doc=
"Writing debugging data for doUseRegister")
168 doSelectSources = pexConfig.Field(dtype=bool, default=
False,
169 doc=
"Select stars to use for kernel fitting (AL only)")
170 doSelectDcrCatalog = pexConfig.Field(dtype=bool, default=
False,
171 doc=
"Select stars of extreme color as part "
172 "of the control sample (AL only)")
173 doSelectVariableCatalog = pexConfig.Field(dtype=bool, default=
False,
174 doc=
"Select stars that are variable to be part "
175 "of the control sample (AL only)")
176 doSubtract = pexConfig.Field(dtype=bool, default=
True, doc=
"Compute subtracted exposure?")
177 doPreConvolve = pexConfig.Field(dtype=bool, default=
False,
178 doc=
"Not in use. Superseded by useScoreImageDetection.",
179 deprecated=
"This option superseded by useScoreImageDetection."
180 " Will be removed after v22.")
181 useScoreImageDetection = pexConfig.Field(
182 dtype=bool, default=
False, doc=
"Calculate the pre-convolved AL likelihood or "
183 "the Zogy score image. Use it for source detection (if doDetection=True).")
184 doWriteScoreExp = pexConfig.Field(
185 dtype=bool, default=
False, doc=
"Write AL likelihood or Zogy score exposure?")
186 doScaleTemplateVariance = pexConfig.Field(dtype=bool, default=
False,
187 doc=
"Scale variance of the template before PSF matching")
188 doScaleDiffimVariance = pexConfig.Field(dtype=bool, default=
True,
189 doc=
"Scale variance of the diffim before PSF matching. "
190 "You may do either this or template variance scaling, "
191 "or neither. (Doing both is a waste of CPU.)")
192 useGaussianForPreConvolution = pexConfig.Field(
193 dtype=bool, default=
False, doc=
"Use a simple gaussian PSF model for pre-convolution "
194 "(oherwise use exposure PSF)? (AL and if useScoreImageDetection=True only)")
195 doDetection = pexConfig.Field(dtype=bool, default=
True, doc=
"Detect sources?")
196 doDecorrelation = pexConfig.Field(dtype=bool, default=
True,
197 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L "
198 "kernel convolution (AL only)? If True, also update the diffim PSF.")
199 doMerge = pexConfig.Field(dtype=bool, default=
True,
200 doc=
"Merge positive and negative diaSources with grow radius "
201 "set by growFootprint")
202 doMatchSources = pexConfig.Field(dtype=bool, default=
False,
203 doc=
"Match diaSources with input calexp sources and ref catalog sources")
204 doMeasurement = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure diaSources?")
205 doDipoleFitting = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure dipoles using new algorithm?")
206 doForcedMeasurement = pexConfig.Field(
209 doc=
"Force photometer diaSource locations on PVI?")
210 doWriteSubtractedExp = pexConfig.Field(
211 dtype=bool, default=
True, doc=
"Write difference exposure (AL and Zogy) ?")
212 doWriteWarpedExp = pexConfig.Field(
213 dtype=bool, default=
False, doc=
"Write WCS, warped template coadd exposure?")
214 doWriteMatchedExp = pexConfig.Field(dtype=bool, default=
False,
215 doc=
"Write warped and PSF-matched template coadd exposure?")
216 doWriteSources = pexConfig.Field(dtype=bool, default=
True, doc=
"Write sources?")
217 doAddMetrics = pexConfig.Field(dtype=bool, default=
False,
218 doc=
"Add columns to the source table to hold analysis metrics?")
220 coaddName = pexConfig.Field(
221 doc=
"coadd name: typically one of deep, goodSeeing, or dcr",
225 convolveTemplate = pexConfig.Field(
226 doc=
"Which image gets convolved (default = template)",
230 refObjLoader = pexConfig.ConfigurableField(
231 target=LoadIndexedReferenceObjectsTask,
232 doc=
"reference object loader",
234 astrometer = pexConfig.ConfigurableField(
235 target=AstrometryTask,
236 doc=
"astrometry task; used to match sources to reference objects, but not to fit a WCS",
238 sourceSelector = pexConfig.ConfigurableField(
239 target=ObjectSizeStarSelectorTask,
240 doc=
"Source selection algorithm",
242 subtract = subtractAlgorithmRegistry.makeField(
"Subtraction Algorithm", default=
"al")
243 decorrelate = pexConfig.ConfigurableField(
244 target=DecorrelateALKernelSpatialTask,
245 doc=
"Decorrelate effects of A&L kernel convolution on image difference, only if doSubtract is True. "
246 "If this option is enabled, then detection.thresholdValue should be set to 5.0 (rather than the "
250 doSpatiallyVarying = pexConfig.Field(
253 doc=
"Perform A&L decorrelation on a grid across the "
254 "image in order to allow for spatial variations. Zogy does not use this option."
256 detection = pexConfig.ConfigurableField(
257 target=SourceDetectionTask,
258 doc=
"Low-threshold detection for final measurement",
260 measurement = pexConfig.ConfigurableField(
261 target=DipoleFitTask,
262 doc=
"Enable updated dipole fitting method",
264 doApCorr = lsst.pex.config.Field(
267 doc=
"Run subtask to apply aperture corrections"
269 applyApCorr = lsst.pex.config.ConfigurableField(
270 target=ApplyApCorrTask,
271 doc=
"Subtask to apply aperture corrections"
273 forcedMeasurement = pexConfig.ConfigurableField(
274 target=ForcedMeasurementTask,
275 doc=
"Subtask to force photometer PVI at diaSource location.",
277 getTemplate = pexConfig.ConfigurableField(
278 target=GetCoaddAsTemplateTask,
279 doc=
"Subtask to retrieve template exposure and sources",
281 scaleVariance = pexConfig.ConfigurableField(
282 target=ScaleVarianceTask,
283 doc=
"Subtask to rescale the variance of the template "
284 "to the statistically expected level"
286 controlStepSize = pexConfig.Field(
287 doc=
"What step size (every Nth one) to select a control sample from the kernelSources",
291 controlRandomSeed = pexConfig.Field(
292 doc=
"Random seed for shuffing the control sample",
296 register = pexConfig.ConfigurableField(
298 doc=
"Task to enable image-to-image image registration (warping)",
300 kernelSourcesFromRef = pexConfig.Field(
301 doc=
"Select sources to measure kernel from reference catalog if True, template if false",
305 templateSipOrder = pexConfig.Field(
306 dtype=int, default=2,
307 doc=
"Sip Order for fitting the Template Wcs (default is too high, overfitting)"
309 growFootprint = pexConfig.Field(
310 dtype=int, default=2,
311 doc=
"Grow positive and negative footprints by this amount before merging"
313 diaSourceMatchRadius = pexConfig.Field(
314 dtype=float, default=0.5,
315 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
317 requiredTemplateFraction = pexConfig.Field(
318 dtype=float, default=0.1,
319 doc=
"Do not attempt to run task if template covers less than this fraction of pixels."
320 "Setting to 0 will always attempt image subtraction"
322 doSkySources = pexConfig.Field(
325 doc=
"Generate sky sources?",
327 skySources = pexConfig.ConfigurableField(
328 target=SkyObjectsTask,
329 doc=
"Generate sky sources",
332 def setDefaults(self):
335 self.subtract[
'al'].kernel.name =
"AL"
336 self.subtract[
'al'].kernel.active.fitForBackground =
True
337 self.subtract[
'al'].kernel.active.spatialKernelOrder = 1
338 self.subtract[
'al'].kernel.active.spatialBgOrder = 2
341 self.detection.thresholdPolarity =
"both"
342 self.detection.thresholdValue = 5.0
343 self.detection.reEstimateBackground =
False
344 self.detection.thresholdType =
"pixel_stdev"
350 self.measurement.algorithms.names.add(
'base_PeakLikelihoodFlux')
351 self.measurement.plugins.names |= [
'base_LocalPhotoCalib',
354 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
355 self.forcedMeasurement.copyColumns = {
356 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
357 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
358 self.forcedMeasurement.slots.shape =
None
361 random.seed(self.controlRandomSeed)
364 pexConfig.Config.validate(self)
365 if not self.doSubtract
and not self.doDetection:
366 raise ValueError(
"Either doSubtract or doDetection must be enabled.")
367 if self.doMeasurement
and not self.doDetection:
368 raise ValueError(
"Cannot run source measurement without source detection.")
369 if self.doMerge
and not self.doDetection:
370 raise ValueError(
"Cannot run source merging without source detection.")
371 if self.doSkySources
and not self.doDetection:
372 raise ValueError(
"Cannot run sky source creation without source detection.")
373 if self.doUseRegister
and not self.doSelectSources:
374 raise ValueError(
"doUseRegister=True and doSelectSources=False. "
375 "Cannot run RegisterTask without selecting sources.")
376 if hasattr(self.getTemplate,
"coaddName"):
377 if self.getTemplate.coaddName != self.coaddName:
378 raise ValueError(
"Mis-matched coaddName and getTemplate.coaddName in the config.")
379 if self.doScaleDiffimVariance
and self.doScaleTemplateVariance:
380 raise ValueError(
"Scaling the diffim variance and scaling the template variance "
381 "are both set. Please choose one or the other.")
383 if self.subtract.name ==
'zogy':
384 if self.doWriteMatchedExp:
385 raise ValueError(
"doWriteMatchedExp=True Matched exposure is not "
386 "calculated in zogy subtraction.")
387 if self.doAddMetrics:
388 raise ValueError(
"doAddMetrics=True Kernel metrics does not exist in zogy subtraction.")
389 if self.doDecorrelation:
391 "doDecorrelation=True The decorrelation afterburner does not exist in zogy subtraction.")
392 if self.doSelectSources:
394 "doSelectSources=True Selecting sources for PSF matching is not a zogy option.")
395 if self.useGaussianForPreConvolution:
397 "useGaussianForPreConvolution=True This is an AL subtraction only option.")
400 if self.doWriteSubtractedExp
and self.useScoreImageDetection:
402 "doWriteSubtractedExp=True and useScoreImageDetection=True "
403 "Regular difference image is not calculated. "
404 "AL subtraction calculates either the regular difference image or the score image.")
405 if self.doWriteScoreExp
and not self.useScoreImageDetection:
407 "doWriteScoreExp=True and useScoreImageDetection=False "
408 "Score image is not calculated. "
409 "AL subtraction calculates either the regular difference image or the score image.")
410 if self.doAddMetrics
and not self.doSubtract:
411 raise ValueError(
"Subtraction must be enabled for kernel metrics calculation.")
412 if self.useScoreImageDetection
and self.doDecorrelation:
413 raise NotImplementedError(
414 "doDecorrelation=True and useScoreImageDetection=True "
415 "The decorrelation afterburner for AL likelihood images is not implemented.")
416 if self.useGaussianForPreConvolution
and not self.useScoreImageDetection:
418 "useGaussianForPreConvolution=True and useScoreImageDetection=False "
419 "Gaussian PSF approximation exists only for AL subtraction w/ pre-convolution.")
422 class ImageDifferenceTaskRunner(pipeBase.ButlerInitializedTaskRunner):
425 def getTargetList(parsedCmd, **kwargs):
426 return pipeBase.TaskRunner.getTargetList(parsedCmd, templateIdList=parsedCmd.templateId.idList,
430 class ImageDifferenceTask(pipeBase.CmdLineTask, pipeBase.PipelineTask):
431 """Subtract an image from a template and measure the result
433 ConfigClass = ImageDifferenceConfig
434 RunnerClass = ImageDifferenceTaskRunner
435 _DefaultName =
"imageDifference"
437 def __init__(self, butler=None, **kwargs):
438 """!Construct an ImageDifference Task
440 @param[in] butler Butler object to use in constructing reference object loaders
442 super().__init__(**kwargs)
443 self.makeSubtask(
"getTemplate")
445 self.makeSubtask(
"subtract")
447 if self.config.subtract.name ==
'al' and self.config.doDecorrelation:
448 self.makeSubtask(
"decorrelate")
450 if self.config.doScaleTemplateVariance
or self.config.doScaleDiffimVariance:
451 self.makeSubtask(
"scaleVariance")
453 if self.config.doUseRegister:
454 self.makeSubtask(
"register")
455 self.schema = afwTable.SourceTable.makeMinimalSchema()
457 if self.config.doSelectSources:
458 self.makeSubtask(
"sourceSelector")
459 if self.config.kernelSourcesFromRef:
460 self.makeSubtask(
'refObjLoader', butler=butler)
461 self.makeSubtask(
"astrometer", refObjLoader=self.refObjLoader)
463 self.algMetadata = dafBase.PropertyList()
464 if self.config.doDetection:
465 self.makeSubtask(
"detection", schema=self.schema)
466 if self.config.doMeasurement:
467 self.makeSubtask(
"measurement", schema=self.schema,
468 algMetadata=self.algMetadata)
469 if self.config.doApCorr:
470 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
471 if self.config.doForcedMeasurement:
472 self.schema.addField(
473 "ip_diffim_forced_PsfFlux_instFlux",
"D",
474 "Forced PSF flux measured on the direct image.",
476 self.schema.addField(
477 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
478 "Forced PSF flux error measured on the direct image.",
480 self.schema.addField(
481 "ip_diffim_forced_PsfFlux_area",
"F",
482 "Forced PSF flux effective area of PSF.",
484 self.schema.addField(
485 "ip_diffim_forced_PsfFlux_flag",
"Flag",
486 "Forced PSF flux general failure flag.")
487 self.schema.addField(
488 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
489 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
490 self.schema.addField(
491 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
492 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
493 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
494 if self.config.doMatchSources:
495 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
496 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
497 if self.config.doSkySources:
498 self.makeSubtask(
"skySources")
499 self.skySourceKey = self.schema.addField(
"sky_source", type=
"Flag", doc=
"Sky objects.")
502 self.outputSchema = afwTable.SourceCatalog(self.schema)
503 self.outputSchema.getTable().setMetadata(self.algMetadata)
506 def makeIdFactory(expId, expBits):
507 """Create IdFactory instance for unique 64 bit diaSource id-s.
515 Number of used bits in ``expId``.
519 The diasource id-s consists of the ``expId`` stored fixed in the highest value
520 ``expBits`` of the 64-bit integer plus (bitwise or) a generated sequence number in the
521 low value end of the integer.
525 idFactory: `lsst.afw.table.IdFactory`
527 return ExposureIdInfo(expId, expBits).makeSourceIdFactory()
529 @lsst.utils.inheritDoc(pipeBase.PipelineTask)
530 def runQuantum(self, butlerQC: pipeBase.ButlerQuantumContext,
531 inputRefs: pipeBase.InputQuantizedConnection,
532 outputRefs: pipeBase.OutputQuantizedConnection):
533 inputs = butlerQC.get(inputRefs)
534 self.log.info(
"Processing %s", butlerQC.quantum.dataId)
535 expId, expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
537 idFactory = self.makeIdFactory(expId=expId, expBits=expBits)
538 if self.config.coaddName ==
'dcr':
539 templateExposures = inputRefs.dcrCoadds
541 templateExposures = inputRefs.coaddExposures
542 templateStruct = self.getTemplate.runQuantum(
543 inputs[
'exposure'], butlerQC, inputRefs.skyMap, templateExposures
546 if templateStruct.area/inputs[
'exposure'].getBBox().getArea() < self.config.requiredTemplateFraction:
547 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%) Not attempting subtraction. "
548 "To force subtraction, set config requiredTemplateFraction=0." % (
549 100*templateStruct.area/inputs[
'exposure'].getBBox().getArea(),
550 100*self.config.requiredTemplateFraction))
551 raise pipeBase.NoWorkFound(message)
553 outputs = self.run(exposure=inputs[
'exposure'],
554 templateExposure=templateStruct.exposure,
557 if outputs.diaSources
is None:
558 del outputs.diaSources
559 butlerQC.put(outputs, outputRefs)
562 def runDataRef(self, sensorRef, templateIdList=None):
563 """Subtract an image from a template coadd and measure the result.
565 Data I/O wrapper around `run` using the butler in Gen2.
569 sensorRef : `lsst.daf.persistence.ButlerDataRef`
570 Sensor-level butler data reference, used for the following data products:
577 - self.config.coaddName + "Coadd_skyMap"
578 - self.config.coaddName + "Coadd"
579 Input or output, depending on config:
580 - self.config.coaddName + "Diff_subtractedExp"
581 Output, depending on config:
582 - self.config.coaddName + "Diff_matchedExp"
583 - self.config.coaddName + "Diff_src"
587 results : `lsst.pipe.base.Struct`
588 Returns the Struct by `run`.
590 subtractedExposureName = self.config.coaddName +
"Diff_differenceExp"
591 subtractedExposure =
None
593 calexpBackgroundExposure =
None
594 self.log.info(
"Processing %s", sensorRef.dataId)
599 idFactory = self.makeIdFactory(expId=int(sensorRef.get(
"ccdExposureId")),
600 expBits=sensorRef.get(
"ccdExposureId_bits"))
601 if self.config.doAddCalexpBackground:
602 calexpBackgroundExposure = sensorRef.get(
"calexpBackground")
605 exposure = sensorRef.get(
"calexp", immediate=
True)
608 template = self.getTemplate.runDataRef(exposure, sensorRef, templateIdList=templateIdList)
610 if sensorRef.datasetExists(
"src"):
611 self.log.info(
"Source selection via src product")
613 selectSources = sensorRef.get(
"src")
615 if not self.config.doSubtract
and self.config.doDetection:
617 subtractedExposure = sensorRef.get(subtractedExposureName)
620 results = self.run(exposure=exposure,
621 selectSources=selectSources,
622 templateExposure=template.exposure,
623 templateSources=template.sources,
625 calexpBackgroundExposure=calexpBackgroundExposure,
626 subtractedExposure=subtractedExposure)
628 if self.config.doWriteSources
and results.diaSources
is not None:
629 sensorRef.put(results.diaSources, self.config.coaddName +
"Diff_diaSrc")
630 if self.config.doWriteWarpedExp:
631 sensorRef.put(results.warpedExposure, self.config.coaddName +
"Diff_warpedExp")
632 if self.config.doWriteMatchedExp:
633 sensorRef.put(results.matchedExposure, self.config.coaddName +
"Diff_matchedExp")
634 if self.config.doAddMetrics
and self.config.doSelectSources:
635 sensorRef.put(results.selectSources, self.config.coaddName +
"Diff_kernelSrc")
636 if self.config.doWriteSubtractedExp:
637 sensorRef.put(results.subtractedExposure, subtractedExposureName)
638 if self.config.doWriteScoreExp:
639 sensorRef.put(results.scoreExposure, self.config.coaddName +
"Diff_scoreExp")
643 def run(self, exposure=None, selectSources=None, templateExposure=None, templateSources=None,
644 idFactory=None, calexpBackgroundExposure=None, subtractedExposure=None):
645 """PSF matches, subtract two images and perform detection on the difference image.
649 exposure : `lsst.afw.image.ExposureF`, optional
650 The science exposure, the minuend in the image subtraction.
651 Can be None only if ``config.doSubtract==False``.
652 selectSources : `lsst.afw.table.SourceCatalog`, optional
653 Identified sources on the science exposure. This catalog is used to
654 select sources in order to perform the AL PSF matching on stamp images
655 around them. The selection steps depend on config options and whether
656 ``templateSources`` and ``matchingSources`` specified.
657 templateExposure : `lsst.afw.image.ExposureF`, optional
658 The template to be subtracted from ``exposure`` in the image subtraction.
659 ``templateExposure`` is modified in place if ``config.doScaleTemplateVariance==True``.
660 The template exposure should cover the same sky area as the science exposure.
661 It is either a stich of patches of a coadd skymap image or a calexp
662 of the same pointing as the science exposure. Can be None only
663 if ``config.doSubtract==False`` and ``subtractedExposure`` is not None.
664 templateSources : `lsst.afw.table.SourceCatalog`, optional
665 Identified sources on the template exposure.
666 idFactory : `lsst.afw.table.IdFactory`
667 Generator object to assign ids to detected sources in the difference image.
668 calexpBackgroundExposure : `lsst.afw.image.ExposureF`, optional
669 Background exposure to be added back to the science exposure
670 if ``config.doAddCalexpBackground==True``
671 subtractedExposure : `lsst.afw.image.ExposureF`, optional
672 If ``config.doSubtract==False`` and ``config.doDetection==True``,
673 performs the post subtraction source detection only on this exposure.
674 Otherwise should be None.
678 results : `lsst.pipe.base.Struct`
679 ``subtractedExposure`` : `lsst.afw.image.ExposureF`
681 ``scoreExposure`` : `lsst.afw.image.ExposureF` or `None`
682 The zogy score exposure, if calculated.
683 ``matchedExposure`` : `lsst.afw.image.ExposureF`
684 The matched PSF exposure.
685 ``subtractRes`` : `lsst.pipe.base.Struct`
686 The returned result structure of the ImagePsfMatchTask subtask.
687 ``diaSources`` : `lsst.afw.table.SourceCatalog`
688 The catalog of detected sources.
689 ``selectSources`` : `lsst.afw.table.SourceCatalog`
690 The input source catalog with optionally added Qa information.
694 The following major steps are included:
696 - warp template coadd to match WCS of image
697 - PSF match image to warped template
698 - subtract image from PSF-matched, warped template
702 For details about the image subtraction configuration modes
703 see `lsst.ip.diffim`.
706 controlSources =
None
707 subtractedExposure =
None
712 exposureOrig = exposure
714 if self.config.doAddCalexpBackground:
715 mi = exposure.getMaskedImage()
716 mi += calexpBackgroundExposure.getImage()
718 if not exposure.hasPsf():
719 raise pipeBase.TaskError(
"Exposure has no psf")
720 sciencePsf = exposure.getPsf()
722 if self.config.doSubtract:
723 if self.config.doScaleTemplateVariance:
724 self.log.info(
"Rescaling template variance")
725 templateVarFactor = self.scaleVariance.
run(
726 templateExposure.getMaskedImage())
727 self.log.info(
"Template variance scaling factor: %.2f", templateVarFactor)
728 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
729 self.metadata.add(
"psfMatchingAlgorithm", self.config.subtract.name)
731 if self.config.subtract.name ==
'zogy':
732 subtractRes = self.subtract.
run(exposure, templateExposure, doWarping=
True)
733 scoreExposure = subtractRes.scoreExp
734 subtractedExposure = subtractRes.diffExp
735 subtractRes.subtractedExposure = subtractedExposure
736 subtractRes.matchedExposure =
None
738 elif self.config.subtract.name ==
'al':
740 scienceSigmaOrig = sciencePsf.computeShape().getDeterminantRadius()
741 templateSigma = templateExposure.getPsf().computeShape().getDeterminantRadius()
749 if self.config.useScoreImageDetection:
750 self.log.warn(
"AL likelihood image: pre-convolution of PSF is not implemented.")
751 convControl = afwMath.ConvolutionControl()
753 srcMI = exposure.maskedImage
754 exposure = exposure.clone()
756 if self.config.useGaussianForPreConvolution:
758 "AL likelihood image: Using Gaussian (sigma={:.2f}) PSF estimation "
759 "for science image pre-convolution", scienceSigmaOrig)
761 kWidth, kHeight = sciencePsf.getLocalKernel().getDimensions()
766 "AL likelihood image: Using the science image PSF for pre-convolution.")
768 afwMath.convolve(exposure.maskedImage, srcMI, preConvPsf.getLocalKernel(), convControl)
769 scienceSigmaPost = scienceSigmaOrig*math.sqrt(2)
771 scienceSigmaPost = scienceSigmaOrig
776 if self.config.doSelectSources:
777 if selectSources
is None:
778 self.log.warning(
"Src product does not exist; running detection, measurement,"
781 selectSources = self.subtract.getSelectSources(
783 sigma=scienceSigmaPost,
784 doSmooth=
not self.config.useScoreImageDetection,
788 if self.config.doAddMetrics:
791 nparam = len(makeKernelBasisList(self.subtract.config.kernel.active,
792 referenceFwhmPix=scienceSigmaPost*FwhmPerSigma,
793 targetFwhmPix=templateSigma*FwhmPerSigma))
800 kcQa = KernelCandidateQa(nparam)
801 selectSources = kcQa.addToSchema(selectSources)
802 if self.config.kernelSourcesFromRef:
804 astromRet = self.astrometer.loadAndMatch(exposure=exposure, sourceCat=selectSources)
805 matches = astromRet.matches
806 elif templateSources:
808 mc = afwTable.MatchControl()
809 mc.findOnlyClosest =
False
810 matches = afwTable.matchRaDec(templateSources, selectSources, 1.0*geom.arcseconds,
813 raise RuntimeError(
"doSelectSources=True and kernelSourcesFromRef=False,"
814 "but template sources not available. Cannot match science "
815 "sources with template sources. Run process* on data from "
816 "which templates are built.")
818 kernelSources = self.sourceSelector.
run(selectSources, exposure=exposure,
819 matches=matches).sourceCat
820 random.shuffle(kernelSources, random.random)
821 controlSources = kernelSources[::self.config.controlStepSize]
822 kernelSources = [k
for i, k
in enumerate(kernelSources)
823 if i % self.config.controlStepSize]
825 if self.config.doSelectDcrCatalog:
826 redSelector = DiaCatalogSourceSelectorTask(
827 DiaCatalogSourceSelectorConfig(grMin=self.sourceSelector.config.grMax,
829 redSources = redSelector.selectStars(exposure, selectSources, matches=matches).starCat
830 controlSources.extend(redSources)
832 blueSelector = DiaCatalogSourceSelectorTask(
833 DiaCatalogSourceSelectorConfig(grMin=-99.999,
834 grMax=self.sourceSelector.config.grMin))
835 blueSources = blueSelector.selectStars(exposure, selectSources,
836 matches=matches).starCat
837 controlSources.extend(blueSources)
839 if self.config.doSelectVariableCatalog:
840 varSelector = DiaCatalogSourceSelectorTask(
841 DiaCatalogSourceSelectorConfig(includeVariable=
True))
842 varSources = varSelector.selectStars(exposure, selectSources, matches=matches).starCat
843 controlSources.extend(varSources)
845 self.log.info(
"Selected %d / %d sources for Psf matching (%d for control sample)",
846 len(kernelSources), len(selectSources), len(controlSources))
850 if self.config.doUseRegister:
851 self.log.info(
"Registering images")
853 if templateSources
is None:
857 templateSigma = templateExposure.getPsf().computeShape().getDeterminantRadius()
858 templateSources = self.subtract.getSelectSources(
867 wcsResults = self.fitAstrometry(templateSources, templateExposure, selectSources)
868 warpedExp = self.register.warpExposure(templateExposure, wcsResults.wcs,
869 exposure.getWcs(), exposure.getBBox())
870 templateExposure = warpedExp
875 if self.config.doDebugRegister:
877 srcToMatch = {x.second.getId(): x.first
for x
in matches}
879 refCoordKey = wcsResults.matches[0].first.getTable().getCoordKey()
880 inCentroidKey = wcsResults.matches[0].second.getTable().getCentroidSlot().getMeasKey()
881 sids = [m.first.getId()
for m
in wcsResults.matches]
882 positions = [m.first.get(refCoordKey)
for m
in wcsResults.matches]
883 residuals = [m.first.get(refCoordKey).getOffsetFrom(wcsResults.wcs.pixelToSky(
884 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
885 allresids = dict(zip(sids, zip(positions, residuals)))
887 cresiduals = [m.first.get(refCoordKey).getTangentPlaneOffset(
888 wcsResults.wcs.pixelToSky(
889 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
890 colors = numpy.array([-2.5*numpy.log10(srcToMatch[x].get(
"g"))
891 + 2.5*numpy.log10(srcToMatch[x].get(
"r"))
892 for x
in sids
if x
in srcToMatch.keys()])
893 dlong = numpy.array([r[0].asArcseconds()
for s, r
in zip(sids, cresiduals)
894 if s
in srcToMatch.keys()])
895 dlat = numpy.array([r[1].asArcseconds()
for s, r
in zip(sids, cresiduals)
896 if s
in srcToMatch.keys()])
897 idx1 = numpy.where(colors < self.sourceSelector.config.grMin)
898 idx2 = numpy.where((colors >= self.sourceSelector.config.grMin)
899 & (colors <= self.sourceSelector.config.grMax))
900 idx3 = numpy.where(colors > self.sourceSelector.config.grMax)
901 rms1Long = IqrToSigma*(
902 (numpy.percentile(dlong[idx1], 75) - numpy.percentile(dlong[idx1], 25)))
903 rms1Lat = IqrToSigma*(numpy.percentile(dlat[idx1], 75)
904 - numpy.percentile(dlat[idx1], 25))
905 rms2Long = IqrToSigma*(
906 (numpy.percentile(dlong[idx2], 75) - numpy.percentile(dlong[idx2], 25)))
907 rms2Lat = IqrToSigma*(numpy.percentile(dlat[idx2], 75)
908 - numpy.percentile(dlat[idx2], 25))
909 rms3Long = IqrToSigma*(
910 (numpy.percentile(dlong[idx3], 75) - numpy.percentile(dlong[idx3], 25)))
911 rms3Lat = IqrToSigma*(numpy.percentile(dlat[idx3], 75)
912 - numpy.percentile(dlat[idx3], 25))
913 self.log.info(
"Blue star offsets'': %.3f %.3f, %.3f %.3f",
914 numpy.median(dlong[idx1]), rms1Long,
915 numpy.median(dlat[idx1]), rms1Lat)
916 self.log.info(
"Green star offsets'': %.3f %.3f, %.3f %.3f",
917 numpy.median(dlong[idx2]), rms2Long,
918 numpy.median(dlat[idx2]), rms2Lat)
919 self.log.info(
"Red star offsets'': %.3f %.3f, %.3f %.3f",
920 numpy.median(dlong[idx3]), rms3Long,
921 numpy.median(dlat[idx3]), rms3Lat)
923 self.metadata.add(
"RegisterBlueLongOffsetMedian", numpy.median(dlong[idx1]))
924 self.metadata.add(
"RegisterGreenLongOffsetMedian", numpy.median(dlong[idx2]))
925 self.metadata.add(
"RegisterRedLongOffsetMedian", numpy.median(dlong[idx3]))
926 self.metadata.add(
"RegisterBlueLongOffsetStd", rms1Long)
927 self.metadata.add(
"RegisterGreenLongOffsetStd", rms2Long)
928 self.metadata.add(
"RegisterRedLongOffsetStd", rms3Long)
930 self.metadata.add(
"RegisterBlueLatOffsetMedian", numpy.median(dlat[idx1]))
931 self.metadata.add(
"RegisterGreenLatOffsetMedian", numpy.median(dlat[idx2]))
932 self.metadata.add(
"RegisterRedLatOffsetMedian", numpy.median(dlat[idx3]))
933 self.metadata.add(
"RegisterBlueLatOffsetStd", rms1Lat)
934 self.metadata.add(
"RegisterGreenLatOffsetStd", rms2Lat)
935 self.metadata.add(
"RegisterRedLatOffsetStd", rms3Lat)
942 self.log.info(
"Subtracting images")
943 subtractRes = self.subtract.subtractExposures(
944 templateExposure=templateExposure,
945 scienceExposure=exposure,
946 candidateList=kernelSources,
947 convolveTemplate=self.config.convolveTemplate,
948 doWarping=
not self.config.doUseRegister
950 if self.config.useScoreImageDetection:
951 scoreExposure = subtractRes.subtractedExposure
953 subtractedExposure = subtractRes.subtractedExposure
955 if self.config.doDetection:
956 self.log.info(
"Computing diffim PSF")
959 if subtractedExposure
is not None and not subtractedExposure.hasPsf():
960 if self.config.convolveTemplate:
961 subtractedExposure.setPsf(exposure.getPsf())
963 subtractedExposure.setPsf(templateExposure.getPsf())
971 if self.config.doDecorrelation
and self.config.doSubtract:
973 if preConvPsf
is not None:
974 preConvKernel = preConvPsf.getLocalKernel()
975 decorrResult = self.decorrelate.
run(exposureOrig, subtractRes.warpedExposure,
977 subtractRes.psfMatchingKernel,
978 spatiallyVarying=self.config.doSpatiallyVarying,
979 preConvKernel=preConvKernel,
980 templateMatched=self.config.convolveTemplate)
981 subtractedExposure = decorrResult.correctedExposure
985 if self.config.doDetection:
986 self.log.info(
"Running diaSource detection")
994 if self.config.useScoreImageDetection:
996 self.log.info(
"Detection, diffim rescaling and measurements are "
997 "on AL likelihood or Zogy score image.")
998 detectionExposure = scoreExposure
1001 detectionExposure = subtractedExposure
1004 if self.config.doScaleDiffimVariance:
1005 self.log.info(
"Rescaling diffim variance")
1006 diffimVarFactor = self.scaleVariance.
run(detectionExposure.getMaskedImage())
1007 self.log.info(
"Diffim variance scaling factor: %.2f", diffimVarFactor)
1008 self.metadata.add(
"scaleDiffimVarianceFactor", diffimVarFactor)
1011 mask = detectionExposure.getMaskedImage().getMask()
1012 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
1014 table = afwTable.SourceTable.make(self.schema, idFactory)
1015 table.setMetadata(self.algMetadata)
1016 results = self.detection.
run(
1018 exposure=detectionExposure,
1019 doSmooth=
not self.config.useScoreImageDetection
1022 if self.config.doMerge:
1023 fpSet = results.fpSets.positive
1024 fpSet.merge(results.fpSets.negative, self.config.growFootprint,
1025 self.config.growFootprint,
False)
1026 diaSources = afwTable.SourceCatalog(table)
1027 fpSet.makeSources(diaSources)
1028 self.log.info(
"Merging detections into %d sources", len(diaSources))
1030 diaSources = results.sources
1032 if self.config.doSkySources:
1033 skySourceFootprints = self.skySources.
run(
1034 mask=detectionExposure.mask,
1035 seed=detectionExposure.getInfo().getVisitInfo().getExposureId())
1036 if skySourceFootprints:
1037 for foot
in skySourceFootprints:
1038 s = diaSources.addNew()
1039 s.setFootprint(foot)
1040 s.set(self.skySourceKey,
True)
1042 if self.config.doMeasurement:
1043 newDipoleFitting = self.config.doDipoleFitting
1044 self.log.info(
"Running diaSource measurement: newDipoleFitting=%r", newDipoleFitting)
1045 if not newDipoleFitting:
1047 self.measurement.
run(diaSources, detectionExposure)
1050 if self.config.doSubtract
and 'matchedExposure' in subtractRes.getDict():
1051 self.measurement.
run(diaSources, detectionExposure, exposure,
1052 subtractRes.matchedExposure)
1054 self.measurement.
run(diaSources, detectionExposure, exposure)
1055 if self.config.doApCorr:
1056 self.applyApCorr.
run(
1058 apCorrMap=detectionExposure.getInfo().getApCorrMap()
1061 if self.config.doForcedMeasurement:
1064 forcedSources = self.forcedMeasurement.generateMeasCat(
1065 exposure, diaSources, detectionExposure.getWcs())
1066 self.forcedMeasurement.
run(forcedSources, exposure, diaSources, detectionExposure.getWcs())
1067 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
1068 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
1069 "ip_diffim_forced_PsfFlux_instFlux",
True)
1070 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
1071 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
1072 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
1073 "ip_diffim_forced_PsfFlux_area",
True)
1074 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
1075 "ip_diffim_forced_PsfFlux_flag",
True)
1076 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
1077 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
1078 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
1079 "ip_diffim_forced_PsfFlux_flag_edge",
True)
1080 for diaSource, forcedSource
in zip(diaSources, forcedSources):
1081 diaSource.assign(forcedSource, mapper)
1084 if self.config.doMatchSources:
1085 if selectSources
is not None:
1087 matchRadAsec = self.config.diaSourceMatchRadius
1088 matchRadPixel = matchRadAsec/exposure.getWcs().getPixelScale().asArcseconds()
1090 srcMatches = afwTable.matchXy(selectSources, diaSources, matchRadPixel)
1091 srcMatchDict = dict([(srcMatch.second.getId(), srcMatch.first.getId())
for
1092 srcMatch
in srcMatches])
1093 self.log.info(
"Matched %d / %d diaSources to sources",
1094 len(srcMatchDict), len(diaSources))
1096 self.log.warning(
"Src product does not exist; cannot match with diaSources")
1100 refAstromConfig = AstrometryConfig()
1101 refAstromConfig.matcher.maxMatchDistArcSec = matchRadAsec
1102 refAstrometer = AstrometryTask(refAstromConfig)
1103 astromRet = refAstrometer.run(exposure=exposure, sourceCat=diaSources)
1104 refMatches = astromRet.matches
1105 if refMatches
is None:
1106 self.log.warning(
"No diaSource matches with reference catalog")
1109 self.log.info(
"Matched %d / %d diaSources to reference catalog",
1110 len(refMatches), len(diaSources))
1111 refMatchDict = dict([(refMatch.second.getId(), refMatch.first.getId())
for
1112 refMatch
in refMatches])
1115 for diaSource
in diaSources:
1116 sid = diaSource.getId()
1117 if sid
in srcMatchDict:
1118 diaSource.set(
"srcMatchId", srcMatchDict[sid])
1119 if sid
in refMatchDict:
1120 diaSource.set(
"refMatchId", refMatchDict[sid])
1122 if self.config.doAddMetrics
and self.config.doSelectSources:
1123 self.log.info(
"Evaluating metrics and control sample")
1126 for cell
in subtractRes.kernelCellSet.getCellList():
1127 for cand
in cell.begin(
False):
1128 kernelCandList.append(cand)
1131 basisList = kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelList()
1132 nparam = len(kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelParameters())
1135 diffimTools.sourceTableToCandidateList(controlSources,
1136 subtractRes.warpedExposure, exposure,
1137 self.config.subtract.kernel.active,
1138 self.config.subtract.kernel.active.detectionConfig,
1139 self.log, doBuild=
True, basisList=basisList))
1141 KernelCandidateQa.apply(kernelCandList, subtractRes.psfMatchingKernel,
1142 subtractRes.backgroundModel, dof=nparam)
1143 KernelCandidateQa.apply(controlCandList, subtractRes.psfMatchingKernel,
1144 subtractRes.backgroundModel)
1146 if self.config.doDetection:
1147 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids, diaSources)
1149 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids)
1151 self.runDebug(exposure, subtractRes, selectSources, kernelSources, diaSources)
1152 return pipeBase.Struct(
1153 subtractedExposure=subtractedExposure,
1154 scoreExposure=scoreExposure,
1155 warpedExposure=subtractRes.warpedExposure,
1156 matchedExposure=subtractRes.matchedExposure,
1157 subtractRes=subtractRes,
1158 diaSources=diaSources,
1159 selectSources=selectSources
1162 def fitAstrometry(self, templateSources, templateExposure, selectSources):
1163 """Fit the relative astrometry between templateSources and selectSources
1168 Remove this method. It originally fit a new WCS to the template before calling register.run
1169 because our TAN-SIP fitter behaved badly for points far from CRPIX, but that's been fixed.
1170 It remains because a subtask overrides it.
1172 results = self.register.
run(templateSources, templateExposure.getWcs(),
1173 templateExposure.getBBox(), selectSources)
1176 def runDebug(self, exposure, subtractRes, selectSources, kernelSources, diaSources):
1177 """Make debug plots and displays.
1181 Test and update for current debug display and slot names
1191 disp = afwDisplay.getDisplay(frame=lsstDebug.frame)
1192 if not maskTransparency:
1193 maskTransparency = 0
1194 disp.setMaskTransparency(maskTransparency)
1196 if display
and showSubtracted:
1197 disp.mtv(subtractRes.subtractedExposure, title=
"Subtracted image")
1198 mi = subtractRes.subtractedExposure.getMaskedImage()
1199 x0, y0 = mi.getX0(), mi.getY0()
1200 with disp.Buffering():
1201 for s
in diaSources:
1202 x, y = s.getX() - x0, s.getY() - y0
1203 ctype =
"red" if s.get(
"flags_negative")
else "yellow"
1204 if (s.get(
"base_PixelFlags_flag_interpolatedCenter")
1205 or s.get(
"base_PixelFlags_flag_saturatedCenter")
1206 or s.get(
"base_PixelFlags_flag_crCenter")):
1208 elif (s.get(
"base_PixelFlags_flag_interpolated")
1209 or s.get(
"base_PixelFlags_flag_saturated")
1210 or s.get(
"base_PixelFlags_flag_cr")):
1214 disp.dot(ptype, x, y, size=4, ctype=ctype)
1215 lsstDebug.frame += 1
1217 if display
and showPixelResiduals
and selectSources:
1218 nonKernelSources = []
1219 for source
in selectSources:
1220 if source
not in kernelSources:
1221 nonKernelSources.append(source)
1223 diUtils.plotPixelResiduals(exposure,
1224 subtractRes.warpedExposure,
1225 subtractRes.subtractedExposure,
1226 subtractRes.kernelCellSet,
1227 subtractRes.psfMatchingKernel,
1228 subtractRes.backgroundModel,
1230 self.subtract.config.kernel.active.detectionConfig,
1232 diUtils.plotPixelResiduals(exposure,
1233 subtractRes.warpedExposure,
1234 subtractRes.subtractedExposure,
1235 subtractRes.kernelCellSet,
1236 subtractRes.psfMatchingKernel,
1237 subtractRes.backgroundModel,
1239 self.subtract.config.kernel.active.detectionConfig,
1241 if display
and showDiaSources:
1242 flagChecker = SourceFlagChecker(diaSources)
1243 isFlagged = [flagChecker(x)
for x
in diaSources]
1244 isDipole = [x.get(
"ip_diffim_ClassificationDipole_value")
for x
in diaSources]
1245 diUtils.showDiaSources(diaSources, subtractRes.subtractedExposure, isFlagged, isDipole,
1246 frame=lsstDebug.frame)
1247 lsstDebug.frame += 1
1249 if display
and showDipoles:
1250 DipoleAnalysis().displayDipoles(subtractRes.subtractedExposure, diaSources,
1251 frame=lsstDebug.frame)
1252 lsstDebug.frame += 1
1254 def _getConfigName(self):
1255 """Return the name of the config dataset
1257 return "%sDiff_config" % (self.config.coaddName,)
1259 def _getMetadataName(self):
1260 """Return the name of the metadata dataset
1262 return "%sDiff_metadata" % (self.config.coaddName,)
1264 def getSchemaCatalogs(self):
1265 """Return a dict of empty catalogs for each catalog dataset produced by this task."""
1266 return {self.config.coaddName +
"Diff_diaSrc": self.outputSchema}
1269 def _makeArgumentParser(cls):
1270 """Create an argument parser
1272 parser = pipeBase.ArgumentParser(name=cls._DefaultName)
1273 parser.add_id_argument(
"--id",
"calexp", help=
"data ID, e.g. --id visit=12345 ccd=1,2")
1274 parser.add_id_argument(
"--templateId",
"calexp", doMakeDataRefList=
True,
1275 help=
"Template data ID in case of calexp template,"
1276 " e.g. --templateId visit=6789")
1280 class Winter2013ImageDifferenceConfig(ImageDifferenceConfig):
1281 winter2013WcsShift = pexConfig.Field(dtype=float, default=0.0,
1282 doc=
"Shift stars going into RegisterTask by this amount")
1283 winter2013WcsRms = pexConfig.Field(dtype=float, default=0.0,
1284 doc=
"Perturb stars going into RegisterTask by this amount")
1286 def setDefaults(self):
1287 ImageDifferenceConfig.setDefaults(self)
1288 self.getTemplate.retarget(GetCalexpAsTemplateTask)
1291 class Winter2013ImageDifferenceTask(ImageDifferenceTask):
1292 """!Image difference Task used in the Winter 2013 data challege.
1293 Enables testing the effects of registration shifts and scatter.
1295 For use with winter 2013 simulated images:
1296 Use --templateId visit=88868666 for sparse data
1297 --templateId visit=22222200 for dense data (g)
1298 --templateId visit=11111100 for dense data (i)
1300 ConfigClass = Winter2013ImageDifferenceConfig
1301 _DefaultName =
"winter2013ImageDifference"
1303 def __init__(self, **kwargs):
1304 ImageDifferenceTask.__init__(self, **kwargs)
1306 def fitAstrometry(self, templateSources, templateExposure, selectSources):
1307 """Fit the relative astrometry between templateSources and selectSources"""
1308 if self.config.winter2013WcsShift > 0.0:
1310 self.config.winter2013WcsShift)
1311 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1312 for source
in templateSources:
1313 centroid = source.get(cKey)
1314 source.set(cKey, centroid + offset)
1315 elif self.config.winter2013WcsRms > 0.0:
1316 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1317 for source
in templateSources:
1318 offset =
geom.Extent2D(self.config.winter2013WcsRms*numpy.random.normal(),
1319 self.config.winter2013WcsRms*numpy.random.normal())
1320 centroid = source.get(cKey)
1321 source.set(cKey, centroid + offset)
1323 results = self.register.
run(templateSources, templateExposure.getWcs(),
1324 templateExposure.getBBox(), selectSources)
def run(self, skyInfo, tempExpRefList, imageScalerList, weightList, altMaskList=None, mask=None, supplementaryData=None)