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)
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",
267 doc=
"Run subtask to apply aperture corrections"
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",
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.useScoreImageDetection
and not self.convolveTemplate:
402 "convolveTemplate=False and useScoreImageDetection=True "
403 "Pre-convolution and matching of the science image is not a supported operation.")
404 if self.doWriteSubtractedExp
and self.useScoreImageDetection:
406 "doWriteSubtractedExp=True and useScoreImageDetection=True "
407 "Regular difference image is not calculated. "
408 "AL subtraction calculates either the regular difference image or the score image.")
409 if self.doWriteScoreExp
and not self.useScoreImageDetection:
411 "doWriteScoreExp=True and useScoreImageDetection=False "
412 "Score image is not calculated. "
413 "AL subtraction calculates either the regular difference image or the score image.")
414 if self.doAddMetrics
and not self.doSubtract:
415 raise ValueError(
"Subtraction must be enabled for kernel metrics calculation.")
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)
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.")
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 self.checkTemplateIsSufficient(templateStruct.exposure)
548 outputs = self.run(exposure=inputs[
'exposure'],
549 templateExposure=templateStruct.exposure,
552 if outputs.diaSources
is None:
553 del outputs.diaSources
554 butlerQC.put(outputs, outputRefs)
557 def runDataRef(self, sensorRef, templateIdList=None):
558 """Subtract an image from a template coadd and measure the result.
560 Data I/O wrapper around `run` using the butler in Gen2.
564 sensorRef : `lsst.daf.persistence.ButlerDataRef`
565 Sensor-level butler data reference, used for the following data products:
572 - self.config.coaddName + "Coadd_skyMap"
573 - self.config.coaddName + "Coadd"
574 Input or output, depending on config:
575 - self.config.coaddName + "Diff_subtractedExp"
576 Output, depending on config:
577 - self.config.coaddName + "Diff_matchedExp"
578 - self.config.coaddName + "Diff_src"
582 results : `lsst.pipe.base.Struct`
583 Returns the Struct by `run`.
585 subtractedExposureName = self.config.coaddName +
"Diff_differenceExp"
586 subtractedExposure =
None
588 calexpBackgroundExposure =
None
589 self.log.
info(
"Processing %s", sensorRef.dataId)
594 idFactory = self.makeIdFactory(expId=int(sensorRef.get(
"ccdExposureId")),
595 expBits=sensorRef.get(
"ccdExposureId_bits"))
596 if self.config.doAddCalexpBackground:
597 calexpBackgroundExposure = sensorRef.get(
"calexpBackground")
600 exposure = sensorRef.get(
"calexp", immediate=
True)
603 template = self.getTemplate.runDataRef(exposure, sensorRef, templateIdList=templateIdList)
605 if sensorRef.datasetExists(
"src"):
606 self.log.
info(
"Source selection via src product")
608 selectSources = sensorRef.get(
"src")
610 if not self.config.doSubtract
and self.config.doDetection:
612 subtractedExposure = sensorRef.get(subtractedExposureName)
615 results = self.run(exposure=exposure,
616 selectSources=selectSources,
617 templateExposure=template.exposure,
618 templateSources=template.sources,
620 calexpBackgroundExposure=calexpBackgroundExposure,
621 subtractedExposure=subtractedExposure)
623 if self.config.doWriteSources
and results.diaSources
is not None:
624 sensorRef.put(results.diaSources, self.config.coaddName +
"Diff_diaSrc")
625 if self.config.doWriteWarpedExp:
626 sensorRef.put(results.warpedExposure, self.config.coaddName +
"Diff_warpedExp")
627 if self.config.doWriteMatchedExp:
628 sensorRef.put(results.matchedExposure, self.config.coaddName +
"Diff_matchedExp")
629 if self.config.doAddMetrics
and self.config.doSelectSources:
630 sensorRef.put(results.selectSources, self.config.coaddName +
"Diff_kernelSrc")
631 if self.config.doWriteSubtractedExp:
632 sensorRef.put(results.subtractedExposure, subtractedExposureName)
633 if self.config.doWriteScoreExp:
634 sensorRef.put(results.scoreExposure, self.config.coaddName +
"Diff_scoreExp")
638 def run(self, exposure=None, selectSources=None, templateExposure=None, templateSources=None,
639 idFactory=None, calexpBackgroundExposure=None, subtractedExposure=None):
640 """PSF matches, subtract two images and perform detection on the difference image.
644 exposure : `lsst.afw.image.ExposureF`, optional
645 The science exposure, the minuend in the image subtraction.
646 Can be None only if ``config.doSubtract==False``.
647 selectSources : `lsst.afw.table.SourceCatalog`, optional
648 Identified sources on the science exposure. This catalog is used to
649 select sources in order to perform the AL PSF matching on stamp images
650 around them. The selection steps depend on config options and whether
651 ``templateSources`` and ``matchingSources`` specified.
652 templateExposure : `lsst.afw.image.ExposureF`, optional
653 The template to be subtracted from ``exposure`` in the image subtraction.
654 ``templateExposure`` is modified in place if ``config.doScaleTemplateVariance==True``.
655 The template exposure should cover the same sky area as the science exposure.
656 It is either a stich of patches of a coadd skymap image or a calexp
657 of the same pointing as the science exposure. Can be None only
658 if ``config.doSubtract==False`` and ``subtractedExposure`` is not None.
659 templateSources : `lsst.afw.table.SourceCatalog`, optional
660 Identified sources on the template exposure.
661 idFactory : `lsst.afw.table.IdFactory`
662 Generator object to assign ids to detected sources in the difference image.
663 calexpBackgroundExposure : `lsst.afw.image.ExposureF`, optional
664 Background exposure to be added back to the science exposure
665 if ``config.doAddCalexpBackground==True``
666 subtractedExposure : `lsst.afw.image.ExposureF`, optional
667 If ``config.doSubtract==False`` and ``config.doDetection==True``,
668 performs the post subtraction source detection only on this exposure.
669 Otherwise should be None.
673 results : `lsst.pipe.base.Struct`
674 ``subtractedExposure`` : `lsst.afw.image.ExposureF`
676 ``scoreExposure`` : `lsst.afw.image.ExposureF` or `None`
677 The zogy score exposure, if calculated.
678 ``matchedExposure`` : `lsst.afw.image.ExposureF`
679 The matched PSF exposure.
680 ``subtractRes`` : `lsst.pipe.base.Struct`
681 The returned result structure of the ImagePsfMatchTask subtask.
682 ``diaSources`` : `lsst.afw.table.SourceCatalog`
683 The catalog of detected sources.
684 ``selectSources`` : `lsst.afw.table.SourceCatalog`
685 The input source catalog with optionally added Qa information.
689 The following major steps are included:
691 - warp template coadd to match WCS of image
692 - PSF match image to warped template
693 - subtract image from PSF-matched, warped template
697 For details about the image subtraction configuration modes
698 see `lsst.ip.diffim`.
701 controlSources =
None
702 subtractedExposure =
None
707 exposureOrig = exposure
709 if self.config.doAddCalexpBackground:
710 mi = exposure.getMaskedImage()
711 mi += calexpBackgroundExposure.getImage()
713 if not exposure.hasPsf():
714 raise pipeBase.TaskError(
"Exposure has no psf")
715 sciencePsf = exposure.getPsf()
717 if self.config.doSubtract:
718 if self.config.doScaleTemplateVariance:
719 self.log.
info(
"Rescaling template variance")
720 templateVarFactor = self.scaleVariance.
run(
721 templateExposure.getMaskedImage())
722 self.log.
info(
"Template variance scaling factor: %.2f", templateVarFactor)
723 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
724 self.metadata.add(
"psfMatchingAlgorithm", self.config.subtract.name)
726 if self.config.subtract.name ==
'zogy':
727 subtractRes = self.subtract.
run(exposure, templateExposure, doWarping=
True)
728 scoreExposure = subtractRes.scoreExp
729 subtractedExposure = subtractRes.diffExp
730 subtractRes.subtractedExposure = subtractedExposure
731 subtractRes.matchedExposure =
None
733 elif self.config.subtract.name ==
'al':
735 scienceSigmaOrig = sciencePsf.computeShape().getDeterminantRadius()
736 templateSigma = templateExposure.getPsf().computeShape().getDeterminantRadius()
744 if self.config.useScoreImageDetection:
745 self.log.
warn(
"AL likelihood image: pre-convolution of PSF is not implemented.")
748 srcMI = exposure.maskedImage
749 exposure = exposure.clone()
751 if self.config.useGaussianForPreConvolution:
753 "AL likelihood image: Using Gaussian (sigma={:.2f}) PSF estimation "
754 "for science image pre-convolution", scienceSigmaOrig)
756 kWidth, kHeight = sciencePsf.getLocalKernel().getDimensions()
761 "AL likelihood image: Using the science image PSF for pre-convolution.")
763 afwMath.convolve(exposure.maskedImage, srcMI, preConvPsf.getLocalKernel(), convControl)
764 scienceSigmaPost = scienceSigmaOrig*math.sqrt(2)
766 scienceSigmaPost = scienceSigmaOrig
771 if self.config.doSelectSources:
772 if selectSources
is None:
773 self.log.
warning(
"Src product does not exist; running detection, measurement,"
776 selectSources = self.subtract.getSelectSources(
778 sigma=scienceSigmaPost,
779 doSmooth=
not self.config.useScoreImageDetection,
783 if self.config.doAddMetrics:
787 referenceFwhmPix=scienceSigmaPost*FwhmPerSigma,
788 targetFwhmPix=templateSigma*FwhmPerSigma))
796 selectSources = kcQa.addToSchema(selectSources)
797 if self.config.kernelSourcesFromRef:
799 astromRet = self.astrometer.loadAndMatch(exposure=exposure, sourceCat=selectSources)
800 matches = astromRet.matches
801 elif templateSources:
804 mc.findOnlyClosest =
False
808 raise RuntimeError(
"doSelectSources=True and kernelSourcesFromRef=False,"
809 "but template sources not available. Cannot match science "
810 "sources with template sources. Run process* on data from "
811 "which templates are built.")
813 kernelSources = self.sourceSelector.
run(selectSources, exposure=exposure,
814 matches=matches).sourceCat
815 random.shuffle(kernelSources, random.random)
816 controlSources = kernelSources[::self.config.controlStepSize]
817 kernelSources = [k
for i, k
in enumerate(kernelSources)
818 if i % self.config.controlStepSize]
820 if self.config.doSelectDcrCatalog:
824 redSources = redSelector.selectStars(exposure, selectSources, matches=matches).starCat
825 controlSources.extend(redSources)
829 grMax=self.sourceSelector.config.grMin))
830 blueSources = blueSelector.selectStars(exposure, selectSources,
831 matches=matches).starCat
832 controlSources.extend(blueSources)
834 if self.config.doSelectVariableCatalog:
837 varSources = varSelector.selectStars(exposure, selectSources, matches=matches).starCat
838 controlSources.extend(varSources)
840 self.log.
info(
"Selected %d / %d sources for Psf matching (%d for control sample)",
841 len(kernelSources), len(selectSources), len(controlSources))
845 if self.config.doUseRegister:
846 self.log.
info(
"Registering images")
848 if templateSources
is None:
852 templateSigma = templateExposure.getPsf().computeShape().getDeterminantRadius()
853 templateSources = self.subtract.getSelectSources(
862 wcsResults = self.fitAstrometry(templateSources, templateExposure, selectSources)
863 warpedExp = self.register.
warpExposure(templateExposure, wcsResults.wcs,
864 exposure.getWcs(), exposure.getBBox())
865 templateExposure = warpedExp
870 if self.config.doDebugRegister:
872 srcToMatch = {x.second.getId(): x.first
for x
in matches}
874 refCoordKey = wcsResults.matches[0].first.getTable().getCoordKey()
875 inCentroidKey = wcsResults.matches[0].second.getTable().getCentroidSlot().getMeasKey()
876 sids = [m.first.getId()
for m
in wcsResults.matches]
877 positions = [m.first.get(refCoordKey)
for m
in wcsResults.matches]
878 residuals = [m.first.get(refCoordKey).getOffsetFrom(wcsResults.wcs.pixelToSky(
879 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
880 allresids = dict(zip(sids, zip(positions, residuals)))
882 cresiduals = [m.first.get(refCoordKey).getTangentPlaneOffset(
883 wcsResults.wcs.pixelToSky(
884 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
885 colors = numpy.array([-2.5*numpy.log10(srcToMatch[x].get(
"g"))
886 + 2.5*numpy.log10(srcToMatch[x].get(
"r"))
887 for x
in sids
if x
in srcToMatch.keys()])
888 dlong = numpy.array([r[0].asArcseconds()
for s, r
in zip(sids, cresiduals)
889 if s
in srcToMatch.keys()])
890 dlat = numpy.array([r[1].asArcseconds()
for s, r
in zip(sids, cresiduals)
891 if s
in srcToMatch.keys()])
892 idx1 = numpy.where(colors < self.sourceSelector.config.grMin)
893 idx2 = numpy.where((colors >= self.sourceSelector.config.grMin)
894 & (colors <= self.sourceSelector.config.grMax))
895 idx3 = numpy.where(colors > self.sourceSelector.config.grMax)
896 rms1Long = IqrToSigma*(
897 (numpy.percentile(dlong[idx1], 75) - numpy.percentile(dlong[idx1], 25)))
898 rms1Lat = IqrToSigma*(numpy.percentile(dlat[idx1], 75)
899 - numpy.percentile(dlat[idx1], 25))
900 rms2Long = IqrToSigma*(
901 (numpy.percentile(dlong[idx2], 75) - numpy.percentile(dlong[idx2], 25)))
902 rms2Lat = IqrToSigma*(numpy.percentile(dlat[idx2], 75)
903 - numpy.percentile(dlat[idx2], 25))
904 rms3Long = IqrToSigma*(
905 (numpy.percentile(dlong[idx3], 75) - numpy.percentile(dlong[idx3], 25)))
906 rms3Lat = IqrToSigma*(numpy.percentile(dlat[idx3], 75)
907 - numpy.percentile(dlat[idx3], 25))
908 self.log.
info(
"Blue star offsets'': %.3f %.3f, %.3f %.3f",
909 numpy.median(dlong[idx1]), rms1Long,
910 numpy.median(dlat[idx1]), rms1Lat)
911 self.log.
info(
"Green star offsets'': %.3f %.3f, %.3f %.3f",
912 numpy.median(dlong[idx2]), rms2Long,
913 numpy.median(dlat[idx2]), rms2Lat)
914 self.log.
info(
"Red star offsets'': %.3f %.3f, %.3f %.3f",
915 numpy.median(dlong[idx3]), rms3Long,
916 numpy.median(dlat[idx3]), rms3Lat)
918 self.metadata.add(
"RegisterBlueLongOffsetMedian", numpy.median(dlong[idx1]))
919 self.metadata.add(
"RegisterGreenLongOffsetMedian", numpy.median(dlong[idx2]))
920 self.metadata.add(
"RegisterRedLongOffsetMedian", numpy.median(dlong[idx3]))
921 self.metadata.add(
"RegisterBlueLongOffsetStd", rms1Long)
922 self.metadata.add(
"RegisterGreenLongOffsetStd", rms2Long)
923 self.metadata.add(
"RegisterRedLongOffsetStd", rms3Long)
925 self.metadata.add(
"RegisterBlueLatOffsetMedian", numpy.median(dlat[idx1]))
926 self.metadata.add(
"RegisterGreenLatOffsetMedian", numpy.median(dlat[idx2]))
927 self.metadata.add(
"RegisterRedLatOffsetMedian", numpy.median(dlat[idx3]))
928 self.metadata.add(
"RegisterBlueLatOffsetStd", rms1Lat)
929 self.metadata.add(
"RegisterGreenLatOffsetStd", rms2Lat)
930 self.metadata.add(
"RegisterRedLatOffsetStd", rms3Lat)
937 self.log.
info(
"Subtracting images")
938 subtractRes = self.subtract.subtractExposures(
939 templateExposure=templateExposure,
940 scienceExposure=exposure,
941 candidateList=kernelSources,
942 convolveTemplate=self.config.convolveTemplate,
943 doWarping=
not self.config.doUseRegister
945 if self.config.useScoreImageDetection:
946 scoreExposure = subtractRes.subtractedExposure
948 subtractedExposure = subtractRes.subtractedExposure
950 if self.config.doDetection:
951 self.log.
info(
"Computing diffim PSF")
954 if subtractedExposure
is not None and not subtractedExposure.hasPsf():
955 if self.config.convolveTemplate:
956 subtractedExposure.setPsf(exposure.getPsf())
958 subtractedExposure.setPsf(templateExposure.getPsf())
965 if self.config.doDecorrelation
and self.config.doSubtract:
967 if self.config.useGaussianForPreConvolution:
968 preConvKernel = preConvPsf.getLocalKernel()
969 if self.config.useScoreImageDetection:
970 scoreExposure = self.decorrelate.
run(exposureOrig, subtractRes.warpedExposure,
972 subtractRes.psfMatchingKernel,
973 spatiallyVarying=self.config.doSpatiallyVarying,
974 preConvKernel=preConvKernel,
975 templateMatched=
True,
976 preConvMode=
True).correctedExposure
979 subtractedExposure = self.decorrelate.
run(exposureOrig, subtractRes.warpedExposure,
981 subtractRes.psfMatchingKernel,
982 spatiallyVarying=self.config.doSpatiallyVarying,
984 templateMatched=self.config.convolveTemplate,
985 preConvMode=
False).correctedExposure
988 if self.config.doDetection:
989 self.log.
info(
"Running diaSource detection")
997 if self.config.useScoreImageDetection:
999 self.log.
info(
"Detection, diffim rescaling and measurements are "
1000 "on AL likelihood or Zogy score image.")
1001 detectionExposure = scoreExposure
1004 detectionExposure = subtractedExposure
1007 if self.config.doScaleDiffimVariance:
1008 self.log.
info(
"Rescaling diffim variance")
1009 diffimVarFactor = self.scaleVariance.
run(detectionExposure.getMaskedImage())
1010 self.log.
info(
"Diffim variance scaling factor: %.2f", diffimVarFactor)
1011 self.metadata.add(
"scaleDiffimVarianceFactor", diffimVarFactor)
1014 mask = detectionExposure.getMaskedImage().getMask()
1015 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
1017 table = afwTable.SourceTable.make(self.schema, idFactory)
1018 table.setMetadata(self.algMetadata)
1019 results = self.detection.
run(
1021 exposure=detectionExposure,
1022 doSmooth=
not self.config.useScoreImageDetection
1025 if self.config.doMerge:
1026 fpSet = results.fpSets.positive
1027 fpSet.merge(results.fpSets.negative, self.config.growFootprint,
1028 self.config.growFootprint,
False)
1030 fpSet.makeSources(diaSources)
1031 self.log.
info(
"Merging detections into %d sources", len(diaSources))
1033 diaSources = results.sources
1035 if self.config.doSkySources:
1036 skySourceFootprints = self.skySources.
run(
1037 mask=detectionExposure.mask,
1038 seed=detectionExposure.getInfo().getVisitInfo().
getExposureId())
1039 if skySourceFootprints:
1040 for foot
in skySourceFootprints:
1041 s = diaSources.addNew()
1042 s.setFootprint(foot)
1043 s.set(self.skySourceKey,
True)
1045 if self.config.doMeasurement:
1046 newDipoleFitting = self.config.doDipoleFitting
1047 self.log.
info(
"Running diaSource measurement: newDipoleFitting=%r", newDipoleFitting)
1048 if not newDipoleFitting:
1050 self.measurement.
run(diaSources, detectionExposure)
1053 if self.config.doSubtract
and 'matchedExposure' in subtractRes.getDict():
1054 self.measurement.
run(diaSources, detectionExposure, exposure,
1055 subtractRes.matchedExposure)
1057 self.measurement.
run(diaSources, detectionExposure, exposure)
1058 if self.config.doApCorr:
1059 self.applyApCorr.
run(
1061 apCorrMap=detectionExposure.getInfo().getApCorrMap()
1064 if self.config.doForcedMeasurement:
1067 forcedSources = self.forcedMeasurement.generateMeasCat(
1068 exposure, diaSources, detectionExposure.getWcs())
1069 self.forcedMeasurement.
run(forcedSources, exposure, diaSources, detectionExposure.getWcs())
1071 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
1072 "ip_diffim_forced_PsfFlux_instFlux",
True)
1073 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
1074 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
1075 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
1076 "ip_diffim_forced_PsfFlux_area",
True)
1077 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
1078 "ip_diffim_forced_PsfFlux_flag",
True)
1079 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
1080 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
1081 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
1082 "ip_diffim_forced_PsfFlux_flag_edge",
True)
1083 for diaSource, forcedSource
in zip(diaSources, forcedSources):
1084 diaSource.assign(forcedSource, mapper)
1087 if self.config.doMatchSources:
1088 if selectSources
is not None:
1090 matchRadAsec = self.config.diaSourceMatchRadius
1091 matchRadPixel = matchRadAsec/exposure.getWcs().getPixelScale().asArcseconds()
1094 srcMatchDict = dict([(srcMatch.second.getId(), srcMatch.first.getId())
for
1095 srcMatch
in srcMatches])
1096 self.log.
info(
"Matched %d / %d diaSources to sources",
1097 len(srcMatchDict), len(diaSources))
1099 self.log.
warning(
"Src product does not exist; cannot match with diaSources")
1104 refAstromConfig.matcher.maxMatchDistArcSec = matchRadAsec
1106 astromRet = refAstrometer.run(exposure=exposure, sourceCat=diaSources)
1107 refMatches = astromRet.matches
1108 if refMatches
is None:
1109 self.log.
warning(
"No diaSource matches with reference catalog")
1112 self.log.
info(
"Matched %d / %d diaSources to reference catalog",
1113 len(refMatches), len(diaSources))
1114 refMatchDict = dict([(refMatch.second.getId(), refMatch.first.getId())
for
1115 refMatch
in refMatches])
1118 for diaSource
in diaSources:
1119 sid = diaSource.getId()
1120 if sid
in srcMatchDict:
1121 diaSource.set(
"srcMatchId", srcMatchDict[sid])
1122 if sid
in refMatchDict:
1123 diaSource.set(
"refMatchId", refMatchDict[sid])
1125 if self.config.doAddMetrics
and self.config.doSelectSources:
1126 self.log.
info(
"Evaluating metrics and control sample")
1129 for cell
in subtractRes.kernelCellSet.getCellList():
1130 for cand
in cell.begin(
False):
1131 kernelCandList.append(cand)
1134 basisList = kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelList()
1135 nparam = len(kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelParameters())
1138 diffimTools.sourceTableToCandidateList(controlSources,
1139 subtractRes.warpedExposure, exposure,
1140 self.config.subtract.kernel.active,
1141 self.config.subtract.kernel.active.detectionConfig,
1142 self.log, doBuild=
True, basisList=basisList))
1144 KernelCandidateQa.apply(kernelCandList, subtractRes.psfMatchingKernel,
1145 subtractRes.backgroundModel, dof=nparam)
1146 KernelCandidateQa.apply(controlCandList, subtractRes.psfMatchingKernel,
1147 subtractRes.backgroundModel)
1149 if self.config.doDetection:
1150 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids, diaSources)
1152 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids)
1154 self.runDebug(exposure, subtractRes, selectSources, kernelSources, diaSources)
1155 return pipeBase.Struct(
1156 subtractedExposure=subtractedExposure,
1157 scoreExposure=scoreExposure,
1158 warpedExposure=subtractRes.warpedExposure,
1159 matchedExposure=subtractRes.matchedExposure,
1160 subtractRes=subtractRes,
1161 diaSources=diaSources,
1162 selectSources=selectSources
1165 def fitAstrometry(self, templateSources, templateExposure, selectSources):
1166 """Fit the relative astrometry between templateSources and selectSources
1171 Remove this method. It originally fit a new WCS to the template before calling register.run
1172 because our TAN-SIP fitter behaved badly for points far from CRPIX, but that's been fixed.
1173 It remains because a subtask overrides it.
1175 results = self.register.
run(templateSources, templateExposure.getWcs(),
1176 templateExposure.getBBox(), selectSources)
1179 def runDebug(self, exposure, subtractRes, selectSources, kernelSources, diaSources):
1180 """Make debug plots and displays.
1184 Test and update for current debug display and slot names
1194 disp = afwDisplay.getDisplay(frame=lsstDebug.frame)
1195 if not maskTransparency:
1196 maskTransparency = 0
1197 disp.setMaskTransparency(maskTransparency)
1199 if display
and showSubtracted:
1200 disp.mtv(subtractRes.subtractedExposure, title=
"Subtracted image")
1201 mi = subtractRes.subtractedExposure.getMaskedImage()
1202 x0, y0 = mi.getX0(), mi.getY0()
1203 with disp.Buffering():
1204 for s
in diaSources:
1205 x, y = s.getX() - x0, s.getY() - y0
1206 ctype =
"red" if s.get(
"flags_negative")
else "yellow"
1207 if (s.get(
"base_PixelFlags_flag_interpolatedCenter")
1208 or s.get(
"base_PixelFlags_flag_saturatedCenter")
1209 or s.get(
"base_PixelFlags_flag_crCenter")):
1211 elif (s.get(
"base_PixelFlags_flag_interpolated")
1212 or s.get(
"base_PixelFlags_flag_saturated")
1213 or s.get(
"base_PixelFlags_flag_cr")):
1217 disp.dot(ptype, x, y, size=4, ctype=ctype)
1218 lsstDebug.frame += 1
1220 if display
and showPixelResiduals
and selectSources:
1221 nonKernelSources = []
1222 for source
in selectSources:
1223 if source
not in kernelSources:
1224 nonKernelSources.append(source)
1226 diUtils.plotPixelResiduals(exposure,
1227 subtractRes.warpedExposure,
1228 subtractRes.subtractedExposure,
1229 subtractRes.kernelCellSet,
1230 subtractRes.psfMatchingKernel,
1231 subtractRes.backgroundModel,
1233 self.subtract.config.kernel.active.detectionConfig,
1235 diUtils.plotPixelResiduals(exposure,
1236 subtractRes.warpedExposure,
1237 subtractRes.subtractedExposure,
1238 subtractRes.kernelCellSet,
1239 subtractRes.psfMatchingKernel,
1240 subtractRes.backgroundModel,
1242 self.subtract.config.kernel.active.detectionConfig,
1244 if display
and showDiaSources:
1246 isFlagged = [flagChecker(x)
for x
in diaSources]
1247 isDipole = [x.get(
"ip_diffim_ClassificationDipole_value")
for x
in diaSources]
1248 diUtils.showDiaSources(diaSources, subtractRes.subtractedExposure, isFlagged, isDipole,
1249 frame=lsstDebug.frame)
1250 lsstDebug.frame += 1
1252 if display
and showDipoles:
1253 DipoleAnalysis().displayDipoles(subtractRes.subtractedExposure, diaSources,
1254 frame=lsstDebug.frame)
1255 lsstDebug.frame += 1
1257 def checkTemplateIsSufficient(self, templateExposure):
1258 """Raise NoWorkFound if template coverage < requiredTemplateFraction
1262 templateExposure : `lsst.afw.image.ExposureF`
1263 The template exposure to check
1268 Raised if fraction of good pixels, defined as not having NO_DATA
1269 set, is less then the configured requiredTemplateFraction
1273 pixNoData = numpy.count_nonzero(templateExposure.mask.array
1274 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
1275 pixGood = templateExposure.getBBox().getArea() - pixNoData
1276 self.log.
info(
"template has %d good pixels (%.1f%%)", pixGood,
1277 100*pixGood/templateExposure.getBBox().getArea())
1279 if pixGood/templateExposure.getBBox().getArea() < self.config.requiredTemplateFraction:
1280 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%) Not attempting subtraction. "
1281 "To force subtraction, set config requiredTemplateFraction=0." % (
1282 100*pixGood/templateExposure.getBBox().getArea(),
1283 100*self.config.requiredTemplateFraction))
1284 raise pipeBase.NoWorkFound(message)
1286 def _getConfigName(self):
1287 """Return the name of the config dataset
1289 return "%sDiff_config" % (self.config.coaddName,)
1291 def _getMetadataName(self):
1292 """Return the name of the metadata dataset
1294 return "%sDiff_metadata" % (self.config.coaddName,)
1297 """Return a dict of empty catalogs for each catalog dataset produced by this task."""
1298 return {self.config.coaddName +
"Diff_diaSrc": self.outputSchema}
1301 def _makeArgumentParser(cls):
1302 """Create an argument parser
1304 parser = pipeBase.ArgumentParser(name=cls._DefaultName)
1305 parser.add_id_argument(
"--id",
"calexp", help=
"data ID, e.g. --id visit=12345 ccd=1,2")
1306 parser.add_id_argument(
"--templateId",
"calexp", doMakeDataRefList=
True,
1307 help=
"Template data ID in case of calexp template,"
1308 " e.g. --templateId visit=6789")
1313 defaultTemplates={
"coaddName":
"goodSeeing"}
1315 inputTemplate = pipeBase.connectionTypes.Input(
1316 doc=(
"Warped template produced by GetMultiTractCoaddTemplate"),
1317 dimensions=(
"instrument",
"visit",
"detector"),
1318 storageClass=
"ExposureF",
1319 name=
"{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
1322 def __init__(self, *, config=None):
1323 super().__init__(config=config)
1326 if "coaddExposures" in self.inputs:
1327 self.inputs.remove(
"coaddExposures")
1328 if "dcrCoadds" in self.inputs:
1329 self.inputs.remove(
"dcrCoadds")
1333 pipelineConnections=ImageDifferenceFromTemplateConnections):
1338 ConfigClass = ImageDifferenceFromTemplateConfig
1339 _DefaultName =
"imageDifference"
1341 @lsst.utils.inheritDoc(pipeBase.PipelineTask)
1343 inputs = butlerQC.get(inputRefs)
1344 self.log.
info(
"Processing %s", butlerQC.quantum.dataId)
1345 self.checkTemplateIsSufficient(inputs[
'inputTemplate'])
1346 expId, expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
1348 idFactory = self.makeIdFactory(expId=expId, expBits=expBits)
1350 outputs = self.run(exposure=inputs[
'exposure'],
1351 templateExposure=inputs[
'inputTemplate'],
1352 idFactory=idFactory)
1355 if outputs.diaSources
is None:
1356 del outputs.diaSources
1357 butlerQC.put(outputs, outputRefs)
1361 winter2013WcsShift = pexConfig.Field(dtype=float, default=0.0,
1362 doc=
"Shift stars going into RegisterTask by this amount")
1363 winter2013WcsRms = pexConfig.Field(dtype=float, default=0.0,
1364 doc=
"Perturb stars going into RegisterTask by this amount")
1367 ImageDifferenceConfig.setDefaults(self)
1368 self.getTemplate.retarget(GetCalexpAsTemplateTask)
1372 """!Image difference Task used in the Winter 2013 data challege.
1373 Enables testing the effects of registration shifts and scatter.
1375 For use with winter 2013 simulated images:
1376 Use --templateId visit=88868666 for sparse data
1377 --templateId visit=22222200 for dense data (g)
1378 --templateId visit=11111100 for dense data (i)
1380 ConfigClass = Winter2013ImageDifferenceConfig
1381 _DefaultName =
"winter2013ImageDifference"
1384 ImageDifferenceTask.__init__(self, **kwargs)
1387 """Fit the relative astrometry between templateSources and selectSources"""
1388 if self.config.winter2013WcsShift > 0.0:
1390 self.config.winter2013WcsShift)
1391 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1392 for source
in templateSources:
1393 centroid = source.get(cKey)
1394 source.set(cKey, centroid + offset)
1395 elif self.config.winter2013WcsRms > 0.0:
1396 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1397 for source
in templateSources:
1398 offset =
geom.Extent2D(self.config.winter2013WcsRms*numpy.random.normal(),
1399 self.config.winter2013WcsRms*numpy.random.normal())
1400 centroid = source.get(cKey)
1401 source.set(cKey, centroid + offset)
1403 results = self.register.
run(templateSources, templateExposure.getWcs(),
1404 templateExposure.getBBox(), selectSources)
Parameters to control convolution.
Pass parameters to algorithms that match list of sources.
A mapping between the keys of two Schemas, used to copy data between them.
Class for storing ordered metadata with comments.
Represent a PSF as a circularly symmetrical Gaussian.
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)
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, ConvolutionControl const &convolutionControl=ConvolutionControl())
Convolve an Image or MaskedImage with a Kernel, setting pixels of an existing output image.
int warpExposure(DestExposureT &destExposure, SrcExposureT const &srcExposure, WarpingControl const &control, typename DestExposureT::MaskedImageT::SinglePixel padValue=lsst::afw::math::edgePixel< typename DestExposureT::MaskedImageT >(typename lsst::afw::image::detail::image_traits< typename DestExposureT::MaskedImageT >::image_category()))
Warp (remap) one exposure to another.
SourceMatchVector matchXy(SourceCatalog const &cat1, SourceCatalog const &cat2, double radius, MatchControl const &mc=MatchControl())
Compute all tuples (s1,s2,d) where s1 belings to cat1, s2 belongs to cat2 and d, the distance between...
std::vector< Match< typename Cat1::Record, typename Cat2::Record > > matchRaDec(Cat1 const &cat1, Cat2 const &cat2, lsst::geom::Angle radius, MatchControl const &mc=MatchControl())
Compute all tuples (s1,s2,d) where s1 belings to cat1, s2 belongs to cat2 and d, the distance between...
def run(self, coaddExposures, bbox, wcs)
def makeKernelBasisList(config, targetFwhmPix=None, referenceFwhmPix=None, basisDegGauss=None, basisSigmaGauss=None, metadata=None)
def infof(fmt, *args, **kwargs)
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations.
def getSchemaCatalogs(self)
def getExposureId(self, dataRef)