39 from lsst.meas.algorithms import SourceDetectionTask, SingleGaussianPsf, ObjectSizeStarSelectorTask
40 from lsst.ip.diffim import (DipoleAnalysis, SourceFlagChecker, KernelCandidateF, makeKernelBasisList,
41 KernelCandidateQa, DiaCatalogSourceSelectorTask, DiaCatalogSourceSelectorConfig,
42 GetCoaddAsTemplateTask, GetCalexpAsTemplateTask, DipoleFitTask,
43 DecorrelateALKernelSpatialTask, subtractAlgorithmRegistry)
49 from lsst.utils.timer
import timeMethod
51 __all__ = [
"ImageDifferenceConfig",
"ImageDifferenceTask"]
52 FwhmPerSigma = 2*math.sqrt(2*math.log(2))
57 dimensions=(
"instrument",
"visit",
"detector",
"skymap"),
58 defaultTemplates={
"coaddName":
"deep",
63 exposure = pipeBase.connectionTypes.Input(
64 doc=
"Input science exposure to subtract from.",
65 dimensions=(
"instrument",
"visit",
"detector"),
66 storageClass=
"ExposureF",
67 name=
"{fakesType}calexp"
78 skyMap = pipeBase.connectionTypes.Input(
79 doc=
"Input definition of geometry/bbox and projection/wcs for template exposures",
80 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
81 dimensions=(
"skymap", ),
82 storageClass=
"SkyMap",
84 coaddExposures = pipeBase.connectionTypes.Input(
85 doc=
"Input template to match and subtract from the exposure",
86 dimensions=(
"tract",
"patch",
"skymap",
"band"),
87 storageClass=
"ExposureF",
88 name=
"{fakesType}{coaddName}Coadd{warpTypeSuffix}",
92 dcrCoadds = pipeBase.connectionTypes.Input(
93 doc=
"Input DCR template to match and subtract from the exposure",
94 name=
"{fakesType}dcrCoadd{warpTypeSuffix}",
95 storageClass=
"ExposureF",
96 dimensions=(
"tract",
"patch",
"skymap",
"band",
"subfilter"),
100 outputSchema = pipeBase.connectionTypes.InitOutput(
101 doc=
"Schema (as an example catalog) for output DIASource catalog.",
102 storageClass=
"SourceCatalog",
103 name=
"{fakesType}{coaddName}Diff_diaSrc_schema",
105 subtractedExposure = pipeBase.connectionTypes.Output(
106 doc=
"Output AL difference or Zogy proper difference image",
107 dimensions=(
"instrument",
"visit",
"detector"),
108 storageClass=
"ExposureF",
109 name=
"{fakesType}{coaddName}Diff_differenceExp",
111 scoreExposure = pipeBase.connectionTypes.Output(
112 doc=
"Output AL likelihood or Zogy score image",
113 dimensions=(
"instrument",
"visit",
"detector"),
114 storageClass=
"ExposureF",
115 name=
"{fakesType}{coaddName}Diff_scoreExp",
117 warpedExposure = pipeBase.connectionTypes.Output(
118 doc=
"Warped template used to create `subtractedExposure`.",
119 dimensions=(
"instrument",
"visit",
"detector"),
120 storageClass=
"ExposureF",
121 name=
"{fakesType}{coaddName}Diff_warpedExp",
123 matchedExposure = pipeBase.connectionTypes.Output(
124 doc=
"Warped template used to create `subtractedExposure`.",
125 dimensions=(
"instrument",
"visit",
"detector"),
126 storageClass=
"ExposureF",
127 name=
"{fakesType}{coaddName}Diff_matchedExp",
129 diaSources = pipeBase.connectionTypes.Output(
130 doc=
"Output detected diaSources on the difference image",
131 dimensions=(
"instrument",
"visit",
"detector"),
132 storageClass=
"SourceCatalog",
133 name=
"{fakesType}{coaddName}Diff_diaSrc",
136 def __init__(self, *, config=None):
137 super().__init__(config=config)
138 if config.coaddName ==
'dcr':
139 self.inputs.remove(
"coaddExposures")
141 self.inputs.remove(
"dcrCoadds")
142 if not config.doWriteSubtractedExp:
143 self.outputs.remove(
"subtractedExposure")
144 if not config.doWriteScoreExp:
145 self.outputs.remove(
"scoreExposure")
146 if not config.doWriteWarpedExp:
147 self.outputs.remove(
"warpedExposure")
148 if not config.doWriteMatchedExp:
149 self.outputs.remove(
"matchedExposure")
150 if not config.doWriteSources:
151 self.outputs.remove(
"diaSources")
157 class ImageDifferenceConfig(pipeBase.PipelineTaskConfig,
158 pipelineConnections=ImageDifferenceTaskConnections):
159 """Config for ImageDifferenceTask
161 doAddCalexpBackground = pexConfig.Field(dtype=bool, default=
False,
162 doc=
"Add background to calexp before processing it. "
163 "Useful as ipDiffim does background matching.")
164 doUseRegister = pexConfig.Field(dtype=bool, default=
False,
165 doc=
"Re-compute astrometry on the template. "
166 "Use image-to-image registration to align template with "
167 "science image (AL only).")
168 doDebugRegister = pexConfig.Field(dtype=bool, default=
False,
169 doc=
"Writing debugging data for doUseRegister")
170 doSelectSources = pexConfig.Field(dtype=bool, default=
False,
171 doc=
"Select stars to use for kernel fitting (AL only)")
172 doSelectDcrCatalog = pexConfig.Field(dtype=bool, default=
False,
173 doc=
"Select stars of extreme color as part "
174 "of the control sample (AL only)")
175 doSelectVariableCatalog = pexConfig.Field(dtype=bool, default=
False,
176 doc=
"Select stars that are variable to be part "
177 "of the control sample (AL only)")
178 doSubtract = pexConfig.Field(dtype=bool, default=
True, doc=
"Compute subtracted exposure?")
179 doPreConvolve = pexConfig.Field(dtype=bool, default=
False,
180 doc=
"Not in use. Superseded by useScoreImageDetection.",
181 deprecated=
"This option superseded by useScoreImageDetection."
182 " Will be removed after v22.")
183 useScoreImageDetection = pexConfig.Field(
184 dtype=bool, default=
False, doc=
"Calculate the pre-convolved AL likelihood or "
185 "the Zogy score image. Use it for source detection (if doDetection=True).")
186 doWriteScoreExp = pexConfig.Field(
187 dtype=bool, default=
False, doc=
"Write AL likelihood or Zogy score exposure?")
188 doScaleTemplateVariance = pexConfig.Field(dtype=bool, default=
False,
189 doc=
"Scale variance of the template before PSF matching")
190 doScaleDiffimVariance = pexConfig.Field(dtype=bool, default=
True,
191 doc=
"Scale variance of the diffim before PSF matching. "
192 "You may do either this or template variance scaling, "
193 "or neither. (Doing both is a waste of CPU.)")
194 useGaussianForPreConvolution = pexConfig.Field(
195 dtype=bool, default=
False, doc=
"Use a simple gaussian PSF model for pre-convolution "
196 "(oherwise use exposure PSF)? (AL and if useScoreImageDetection=True only)")
197 doDetection = pexConfig.Field(dtype=bool, default=
True, doc=
"Detect sources?")
198 doDecorrelation = pexConfig.Field(dtype=bool, default=
True,
199 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L "
200 "kernel convolution (AL only)? If True, also update the diffim PSF.")
201 doMerge = pexConfig.Field(dtype=bool, default=
True,
202 doc=
"Merge positive and negative diaSources with grow radius "
203 "set by growFootprint")
204 doMatchSources = pexConfig.Field(dtype=bool, default=
False,
205 doc=
"Match diaSources with input calexp sources and ref catalog sources")
206 doMeasurement = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure diaSources?")
207 doDipoleFitting = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure dipoles using new algorithm?")
208 doForcedMeasurement = pexConfig.Field(
211 doc=
"Force photometer diaSource locations on PVI?")
212 doWriteSubtractedExp = pexConfig.Field(
213 dtype=bool, default=
True, doc=
"Write difference exposure (AL and Zogy) ?")
214 doWriteWarpedExp = pexConfig.Field(
215 dtype=bool, default=
False, doc=
"Write WCS, warped template coadd exposure?")
216 doWriteMatchedExp = pexConfig.Field(dtype=bool, default=
False,
217 doc=
"Write warped and PSF-matched template coadd exposure?")
218 doWriteSources = pexConfig.Field(dtype=bool, default=
True, doc=
"Write sources?")
219 doAddMetrics = pexConfig.Field(dtype=bool, default=
False,
220 doc=
"Add columns to the source table to hold analysis metrics?")
222 coaddName = pexConfig.Field(
223 doc=
"coadd name: typically one of deep, goodSeeing, or dcr",
227 convolveTemplate = pexConfig.Field(
228 doc=
"Which image gets convolved (default = template)",
232 refObjLoader = pexConfig.ConfigurableField(
233 target=LoadIndexedReferenceObjectsTask,
234 doc=
"reference object loader",
236 astrometer = pexConfig.ConfigurableField(
237 target=AstrometryTask,
238 doc=
"astrometry task; used to match sources to reference objects, but not to fit a WCS",
240 sourceSelector = pexConfig.ConfigurableField(
241 target=ObjectSizeStarSelectorTask,
242 doc=
"Source selection algorithm",
244 subtract = subtractAlgorithmRegistry.makeField(
"Subtraction Algorithm", default=
"al")
245 decorrelate = pexConfig.ConfigurableField(
246 target=DecorrelateALKernelSpatialTask,
247 doc=
"Decorrelate effects of A&L kernel convolution on image difference, only if doSubtract is True. "
248 "If this option is enabled, then detection.thresholdValue should be set to 5.0 (rather than the "
252 doSpatiallyVarying = pexConfig.Field(
255 doc=
"Perform A&L decorrelation on a grid across the "
256 "image in order to allow for spatial variations. Zogy does not use this option."
258 detection = pexConfig.ConfigurableField(
259 target=SourceDetectionTask,
260 doc=
"Low-threshold detection for final measurement",
262 measurement = pexConfig.ConfigurableField(
263 target=DipoleFitTask,
264 doc=
"Enable updated dipole fitting method",
269 doc=
"Run subtask to apply aperture corrections"
272 target=ApplyApCorrTask,
273 doc=
"Subtask to apply aperture corrections"
275 forcedMeasurement = pexConfig.ConfigurableField(
276 target=ForcedMeasurementTask,
277 doc=
"Subtask to force photometer PVI at diaSource location.",
279 getTemplate = pexConfig.ConfigurableField(
280 target=GetCoaddAsTemplateTask,
281 doc=
"Subtask to retrieve template exposure and sources",
283 scaleVariance = pexConfig.ConfigurableField(
284 target=ScaleVarianceTask,
285 doc=
"Subtask to rescale the variance of the template "
286 "to the statistically expected level"
288 controlStepSize = pexConfig.Field(
289 doc=
"What step size (every Nth one) to select a control sample from the kernelSources",
293 controlRandomSeed = pexConfig.Field(
294 doc=
"Random seed for shuffing the control sample",
298 register = pexConfig.ConfigurableField(
300 doc=
"Task to enable image-to-image image registration (warping)",
302 kernelSourcesFromRef = pexConfig.Field(
303 doc=
"Select sources to measure kernel from reference catalog if True, template if false",
307 templateSipOrder = pexConfig.Field(
308 dtype=int, default=2,
309 doc=
"Sip Order for fitting the Template Wcs (default is too high, overfitting)"
311 growFootprint = pexConfig.Field(
312 dtype=int, default=2,
313 doc=
"Grow positive and negative footprints by this amount before merging"
315 diaSourceMatchRadius = pexConfig.Field(
316 dtype=float, default=0.5,
317 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
319 requiredTemplateFraction = pexConfig.Field(
320 dtype=float, default=0.1,
321 doc=
"Do not attempt to run task if template covers less than this fraction of pixels."
322 "Setting to 0 will always attempt image subtraction"
324 doSkySources = pexConfig.Field(
327 doc=
"Generate sky sources?",
329 skySources = pexConfig.ConfigurableField(
330 target=SkyObjectsTask,
331 doc=
"Generate sky sources",
337 self.subtract[
'al'].kernel.name =
"AL"
338 self.subtract[
'al'].kernel.active.fitForBackground =
True
339 self.subtract[
'al'].kernel.active.spatialKernelOrder = 1
340 self.subtract[
'al'].kernel.active.spatialBgOrder = 2
343 self.detection.thresholdPolarity =
"both"
344 self.detection.thresholdValue = 5.0
345 self.detection.reEstimateBackground =
False
346 self.detection.thresholdType =
"pixel_stdev"
352 self.measurement.algorithms.names.add(
'base_PeakLikelihoodFlux')
353 self.measurement.plugins.names |= [
'ext_trailedSources_Naive',
354 'base_LocalPhotoCalib',
357 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
358 self.forcedMeasurement.copyColumns = {
359 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
360 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
361 self.forcedMeasurement.slots.shape =
None
364 random.seed(self.controlRandomSeed)
367 pexConfig.Config.validate(self)
368 if not self.doSubtract
and not self.doDetection:
369 raise ValueError(
"Either doSubtract or doDetection must be enabled.")
370 if self.doMeasurement
and not self.doDetection:
371 raise ValueError(
"Cannot run source measurement without source detection.")
372 if self.doMerge
and not self.doDetection:
373 raise ValueError(
"Cannot run source merging without source detection.")
374 if self.doSkySources
and not self.doDetection:
375 raise ValueError(
"Cannot run sky source creation without source detection.")
376 if self.doUseRegister
and not self.doSelectSources:
377 raise ValueError(
"doUseRegister=True and doSelectSources=False. "
378 "Cannot run RegisterTask without selecting sources.")
379 if hasattr(self.getTemplate,
"coaddName"):
380 if self.getTemplate.coaddName != self.coaddName:
381 raise ValueError(
"Mis-matched coaddName and getTemplate.coaddName in the config.")
382 if self.doScaleDiffimVariance
and self.doScaleTemplateVariance:
383 raise ValueError(
"Scaling the diffim variance and scaling the template variance "
384 "are both set. Please choose one or the other.")
386 if self.subtract.name ==
'zogy':
387 if self.doWriteMatchedExp:
388 raise ValueError(
"doWriteMatchedExp=True Matched exposure is not "
389 "calculated in zogy subtraction.")
390 if self.doAddMetrics:
391 raise ValueError(
"doAddMetrics=True Kernel metrics does not exist in zogy subtraction.")
392 if self.doDecorrelation:
394 "doDecorrelation=True The decorrelation afterburner does not exist in zogy subtraction.")
395 if self.doSelectSources:
397 "doSelectSources=True Selecting sources for PSF matching is not a zogy option.")
398 if self.useGaussianForPreConvolution:
400 "useGaussianForPreConvolution=True This is an AL subtraction only option.")
403 if self.useScoreImageDetection
and not self.convolveTemplate:
405 "convolveTemplate=False and useScoreImageDetection=True "
406 "Pre-convolution and matching of the science image is not a supported operation.")
407 if self.doWriteSubtractedExp
and self.useScoreImageDetection:
409 "doWriteSubtractedExp=True and useScoreImageDetection=True "
410 "Regular difference image is not calculated. "
411 "AL subtraction calculates either the regular difference image or the score image.")
412 if self.doWriteScoreExp
and not self.useScoreImageDetection:
414 "doWriteScoreExp=True and useScoreImageDetection=False "
415 "Score image is not calculated. "
416 "AL subtraction calculates either the regular difference image or the score image.")
417 if self.doAddMetrics
and not self.doSubtract:
418 raise ValueError(
"Subtraction must be enabled for kernel metrics calculation.")
419 if self.useGaussianForPreConvolution
and not self.useScoreImageDetection:
421 "useGaussianForPreConvolution=True and useScoreImageDetection=False "
422 "Gaussian PSF approximation exists only for AL subtraction w/ pre-convolution.")
425 class ImageDifferenceTaskRunner(pipeBase.ButlerInitializedTaskRunner):
428 def getTargetList(parsedCmd, **kwargs):
429 return pipeBase.TaskRunner.getTargetList(parsedCmd, templateIdList=parsedCmd.templateId.idList,
433 class ImageDifferenceTask(pipeBase.CmdLineTask, pipeBase.PipelineTask):
434 """Subtract an image from a template and measure the result
436 ConfigClass = ImageDifferenceConfig
437 RunnerClass = ImageDifferenceTaskRunner
438 _DefaultName =
"imageDifference"
440 def __init__(self, butler=None, **kwargs):
441 """!Construct an ImageDifference Task
443 @param[in] butler Butler object to use in constructing reference object loaders
445 super().__init__(**kwargs)
446 self.makeSubtask(
"getTemplate")
448 self.makeSubtask(
"subtract")
450 if self.config.subtract.name ==
'al' and self.config.doDecorrelation:
451 self.makeSubtask(
"decorrelate")
453 if self.config.doScaleTemplateVariance
or self.config.doScaleDiffimVariance:
454 self.makeSubtask(
"scaleVariance")
456 if self.config.doUseRegister:
457 self.makeSubtask(
"register")
458 self.schema = afwTable.SourceTable.makeMinimalSchema()
460 if self.config.doSelectSources:
461 self.makeSubtask(
"sourceSelector")
462 if self.config.kernelSourcesFromRef:
463 self.makeSubtask(
'refObjLoader', butler=butler)
464 self.makeSubtask(
"astrometer", refObjLoader=self.refObjLoader)
467 if self.config.doDetection:
468 self.makeSubtask(
"detection", schema=self.schema)
469 if self.config.doMeasurement:
470 self.makeSubtask(
"measurement", schema=self.schema,
471 algMetadata=self.algMetadata)
472 if self.config.doApCorr:
473 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
474 if self.config.doForcedMeasurement:
475 self.schema.addField(
476 "ip_diffim_forced_PsfFlux_instFlux",
"D",
477 "Forced PSF flux measured on the direct image.",
479 self.schema.addField(
480 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
481 "Forced PSF flux error measured on the direct image.",
483 self.schema.addField(
484 "ip_diffim_forced_PsfFlux_area",
"F",
485 "Forced PSF flux effective area of PSF.",
487 self.schema.addField(
488 "ip_diffim_forced_PsfFlux_flag",
"Flag",
489 "Forced PSF flux general failure flag.")
490 self.schema.addField(
491 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
492 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
493 self.schema.addField(
494 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
495 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
496 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
497 if self.config.doMatchSources:
498 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
499 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
500 if self.config.doSkySources:
501 self.makeSubtask(
"skySources")
502 self.skySourceKey = self.schema.addField(
"sky_source", type=
"Flag", doc=
"Sky objects.")
506 self.outputSchema.getTable().setMetadata(self.algMetadata)
509 def makeIdFactory(expId, expBits):
510 """Create IdFactory instance for unique 64 bit diaSource id-s.
518 Number of used bits in ``expId``.
522 The diasource id-s consists of the ``expId`` stored fixed in the highest value
523 ``expBits`` of the 64-bit integer plus (bitwise or) a generated sequence number in the
524 low value end of the integer.
528 idFactory: `lsst.afw.table.IdFactory`
530 return ExposureIdInfo(expId, expBits).makeSourceIdFactory()
532 @lsst.utils.inheritDoc(pipeBase.PipelineTask)
533 def runQuantum(self, butlerQC: pipeBase.ButlerQuantumContext,
534 inputRefs: pipeBase.InputQuantizedConnection,
535 outputRefs: pipeBase.OutputQuantizedConnection):
536 inputs = butlerQC.get(inputRefs)
537 self.log.
info(
"Processing %s", butlerQC.quantum.dataId)
538 expId, expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
540 idFactory = self.makeIdFactory(expId=expId, expBits=expBits)
541 if self.config.coaddName ==
'dcr':
542 templateExposures = inputRefs.dcrCoadds
544 templateExposures = inputRefs.coaddExposures
545 templateStruct = self.getTemplate.runQuantum(
546 inputs[
'exposure'], butlerQC, inputRefs.skyMap, templateExposures
549 self.checkTemplateIsSufficient(templateStruct.exposure)
551 outputs = self.run(exposure=inputs[
'exposure'],
552 templateExposure=templateStruct.exposure,
555 if outputs.diaSources
is None:
556 del outputs.diaSources
557 butlerQC.put(outputs, outputRefs)
560 def runDataRef(self, sensorRef, templateIdList=None):
561 """Subtract an image from a template coadd and measure the result.
563 Data I/O wrapper around `run` using the butler in Gen2.
567 sensorRef : `lsst.daf.persistence.ButlerDataRef`
568 Sensor-level butler data reference, used for the following data products:
575 - self.config.coaddName + "Coadd_skyMap"
576 - self.config.coaddName + "Coadd"
577 Input or output, depending on config:
578 - self.config.coaddName + "Diff_subtractedExp"
579 Output, depending on config:
580 - self.config.coaddName + "Diff_matchedExp"
581 - self.config.coaddName + "Diff_src"
585 results : `lsst.pipe.base.Struct`
586 Returns the Struct by `run`.
588 subtractedExposureName = self.config.coaddName +
"Diff_differenceExp"
589 subtractedExposure =
None
591 calexpBackgroundExposure =
None
592 self.log.
info(
"Processing %s", sensorRef.dataId)
597 idFactory = self.makeIdFactory(expId=int(sensorRef.get(
"ccdExposureId")),
598 expBits=sensorRef.get(
"ccdExposureId_bits"))
599 if self.config.doAddCalexpBackground:
600 calexpBackgroundExposure = sensorRef.get(
"calexpBackground")
603 exposure = sensorRef.get(
"calexp", immediate=
True)
606 template = self.getTemplate.runDataRef(exposure, sensorRef, templateIdList=templateIdList)
608 if sensorRef.datasetExists(
"src"):
609 self.log.
info(
"Source selection via src product")
611 selectSources = sensorRef.get(
"src")
613 if not self.config.doSubtract
and self.config.doDetection:
615 subtractedExposure = sensorRef.get(subtractedExposureName)
618 results = self.run(exposure=exposure,
619 selectSources=selectSources,
620 templateExposure=template.exposure,
621 templateSources=template.sources,
623 calexpBackgroundExposure=calexpBackgroundExposure,
624 subtractedExposure=subtractedExposure)
626 if self.config.doWriteSources
and results.diaSources
is not None:
627 sensorRef.put(results.diaSources, self.config.coaddName +
"Diff_diaSrc")
628 if self.config.doWriteWarpedExp:
629 sensorRef.put(results.warpedExposure, self.config.coaddName +
"Diff_warpedExp")
630 if self.config.doWriteMatchedExp:
631 sensorRef.put(results.matchedExposure, self.config.coaddName +
"Diff_matchedExp")
632 if self.config.doAddMetrics
and self.config.doSelectSources:
633 sensorRef.put(results.selectSources, self.config.coaddName +
"Diff_kernelSrc")
634 if self.config.doWriteSubtractedExp:
635 sensorRef.put(results.subtractedExposure, subtractedExposureName)
636 if self.config.doWriteScoreExp:
637 sensorRef.put(results.scoreExposure, self.config.coaddName +
"Diff_scoreExp")
641 def run(self, exposure=None, selectSources=None, templateExposure=None, templateSources=None,
642 idFactory=None, calexpBackgroundExposure=None, subtractedExposure=None):
643 """PSF matches, subtract two images and perform detection on the difference image.
647 exposure : `lsst.afw.image.ExposureF`, optional
648 The science exposure, the minuend in the image subtraction.
649 Can be None only if ``config.doSubtract==False``.
650 selectSources : `lsst.afw.table.SourceCatalog`, optional
651 Identified sources on the science exposure. This catalog is used to
652 select sources in order to perform the AL PSF matching on stamp images
653 around them. The selection steps depend on config options and whether
654 ``templateSources`` and ``matchingSources`` specified.
655 templateExposure : `lsst.afw.image.ExposureF`, optional
656 The template to be subtracted from ``exposure`` in the image subtraction.
657 ``templateExposure`` is modified in place if ``config.doScaleTemplateVariance==True``.
658 The template exposure should cover the same sky area as the science exposure.
659 It is either a stich of patches of a coadd skymap image or a calexp
660 of the same pointing as the science exposure. Can be None only
661 if ``config.doSubtract==False`` and ``subtractedExposure`` is not None.
662 templateSources : `lsst.afw.table.SourceCatalog`, optional
663 Identified sources on the template exposure.
664 idFactory : `lsst.afw.table.IdFactory`
665 Generator object to assign ids to detected sources in the difference image.
666 calexpBackgroundExposure : `lsst.afw.image.ExposureF`, optional
667 Background exposure to be added back to the science exposure
668 if ``config.doAddCalexpBackground==True``
669 subtractedExposure : `lsst.afw.image.ExposureF`, optional
670 If ``config.doSubtract==False`` and ``config.doDetection==True``,
671 performs the post subtraction source detection only on this exposure.
672 Otherwise should be None.
676 results : `lsst.pipe.base.Struct`
677 ``subtractedExposure`` : `lsst.afw.image.ExposureF`
679 ``scoreExposure`` : `lsst.afw.image.ExposureF` or `None`
680 The zogy score exposure, if calculated.
681 ``matchedExposure`` : `lsst.afw.image.ExposureF`
682 The matched PSF exposure.
683 ``subtractRes`` : `lsst.pipe.base.Struct`
684 The returned result structure of the ImagePsfMatchTask subtask.
685 ``diaSources`` : `lsst.afw.table.SourceCatalog`
686 The catalog of detected sources.
687 ``selectSources`` : `lsst.afw.table.SourceCatalog`
688 The input source catalog with optionally added Qa information.
692 The following major steps are included:
694 - warp template coadd to match WCS of image
695 - PSF match image to warped template
696 - subtract image from PSF-matched, warped template
700 For details about the image subtraction configuration modes
701 see `lsst.ip.diffim`.
704 controlSources =
None
705 subtractedExposure =
None
710 exposureOrig = exposure
712 if self.config.doAddCalexpBackground:
713 mi = exposure.getMaskedImage()
714 mi += calexpBackgroundExposure.getImage()
716 if not exposure.hasPsf():
717 raise pipeBase.TaskError(
"Exposure has no psf")
718 sciencePsf = exposure.getPsf()
720 if self.config.doSubtract:
721 if self.config.doScaleTemplateVariance:
722 self.log.
info(
"Rescaling template variance")
723 templateVarFactor = self.scaleVariance.
run(
724 templateExposure.getMaskedImage())
725 self.log.
info(
"Template variance scaling factor: %.2f", templateVarFactor)
726 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
727 self.metadata.add(
"psfMatchingAlgorithm", self.config.subtract.name)
729 if self.config.subtract.name ==
'zogy':
730 subtractRes = self.subtract.
run(exposure, templateExposure, doWarping=
True)
731 scoreExposure = subtractRes.scoreExp
732 subtractedExposure = subtractRes.diffExp
733 subtractRes.subtractedExposure = subtractedExposure
734 subtractRes.matchedExposure =
None
736 elif self.config.subtract.name ==
'al':
739 sciAvgPos = sciencePsf.getAveragePosition()
740 scienceSigmaOrig = sciencePsf.computeShape(sciAvgPos).getDeterminantRadius()
742 templatePsf = templateExposure.getPsf()
743 templateAvgPos = templatePsf.getAveragePosition()
744 templateSigma = templatePsf.computeShape(templateAvgPos).getDeterminantRadius()
752 if self.config.useScoreImageDetection:
753 self.log.
warning(
"AL likelihood image: pre-convolution of PSF is not implemented.")
756 srcMI = exposure.maskedImage
757 exposure = exposure.clone()
759 if self.config.useGaussianForPreConvolution:
761 "AL likelihood image: Using Gaussian (sigma=%.2f) PSF estimation "
762 "for science image pre-convolution", scienceSigmaOrig)
764 kWidth, kHeight = sciencePsf.getLocalKernel().getDimensions()
769 "AL likelihood image: Using the science image PSF for pre-convolution.")
771 afwMath.convolve(exposure.maskedImage, srcMI, preConvPsf.getLocalKernel(), convControl)
772 scienceSigmaPost = scienceSigmaOrig*math.sqrt(2)
774 scienceSigmaPost = scienceSigmaOrig
779 if self.config.doSelectSources:
780 if selectSources
is None:
781 self.log.
warning(
"Src product does not exist; running detection, measurement,"
784 selectSources = self.subtract.getSelectSources(
786 sigma=scienceSigmaPost,
787 doSmooth=
not self.config.useScoreImageDetection,
791 if self.config.doAddMetrics:
795 referenceFwhmPix=scienceSigmaPost*FwhmPerSigma,
796 targetFwhmPix=templateSigma*FwhmPerSigma))
804 selectSources = kcQa.addToSchema(selectSources)
805 if self.config.kernelSourcesFromRef:
807 astromRet = self.astrometer.loadAndMatch(exposure=exposure, sourceCat=selectSources)
808 matches = astromRet.matches
809 elif templateSources:
812 mc.findOnlyClosest =
False
816 raise RuntimeError(
"doSelectSources=True and kernelSourcesFromRef=False,"
817 "but template sources not available. Cannot match science "
818 "sources with template sources. Run process* on data from "
819 "which templates are built.")
821 kernelSources = self.sourceSelector.
run(selectSources, exposure=exposure,
822 matches=matches).sourceCat
823 random.shuffle(kernelSources, random.random)
824 controlSources = kernelSources[::self.config.controlStepSize]
825 kernelSources = [k
for i, k
in enumerate(kernelSources)
826 if i % self.config.controlStepSize]
828 if self.config.doSelectDcrCatalog:
832 redSources = redSelector.selectStars(exposure, selectSources, matches=matches).starCat
833 controlSources.extend(redSources)
837 grMax=self.sourceSelector.config.grMin))
838 blueSources = blueSelector.selectStars(exposure, selectSources,
839 matches=matches).starCat
840 controlSources.extend(blueSources)
842 if self.config.doSelectVariableCatalog:
845 varSources = varSelector.selectStars(exposure, selectSources, matches=matches).starCat
846 controlSources.extend(varSources)
848 self.log.
info(
"Selected %d / %d sources for Psf matching (%d for control sample)",
849 len(kernelSources), len(selectSources), len(controlSources))
853 if self.config.doUseRegister:
854 self.log.
info(
"Registering images")
856 if templateSources
is None:
860 templateSources = self.subtract.getSelectSources(
869 wcsResults = self.fitAstrometry(templateSources, templateExposure, selectSources)
870 warpedExp = self.register.
warpExposure(templateExposure, wcsResults.wcs,
871 exposure.getWcs(), exposure.getBBox())
872 templateExposure = warpedExp
877 if self.config.doDebugRegister:
879 srcToMatch = {x.second.getId(): x.first
for x
in matches}
881 refCoordKey = wcsResults.matches[0].first.getTable().getCoordKey()
882 inCentroidKey = wcsResults.matches[0].second.getTable().getCentroidSlot().getMeasKey()
883 sids = [m.first.getId()
for m
in wcsResults.matches]
884 positions = [m.first.get(refCoordKey)
for m
in wcsResults.matches]
885 residuals = [m.first.get(refCoordKey).getOffsetFrom(wcsResults.wcs.pixelToSky(
886 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
887 allresids = dict(zip(sids, zip(positions, residuals)))
889 cresiduals = [m.first.get(refCoordKey).getTangentPlaneOffset(
890 wcsResults.wcs.pixelToSky(
891 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
892 colors = numpy.array([-2.5*numpy.log10(srcToMatch[x].get(
"g"))
893 + 2.5*numpy.log10(srcToMatch[x].get(
"r"))
894 for x
in sids
if x
in srcToMatch.keys()])
895 dlong = numpy.array([r[0].asArcseconds()
for s, r
in zip(sids, cresiduals)
896 if s
in srcToMatch.keys()])
897 dlat = numpy.array([r[1].asArcseconds()
for s, r
in zip(sids, cresiduals)
898 if s
in srcToMatch.keys()])
899 idx1 = numpy.where(colors < self.sourceSelector.config.grMin)
900 idx2 = numpy.where((colors >= self.sourceSelector.config.grMin)
901 & (colors <= self.sourceSelector.config.grMax))
902 idx3 = numpy.where(colors > self.sourceSelector.config.grMax)
903 rms1Long = IqrToSigma*(
904 (numpy.percentile(dlong[idx1], 75) - numpy.percentile(dlong[idx1], 25)))
905 rms1Lat = IqrToSigma*(numpy.percentile(dlat[idx1], 75)
906 - numpy.percentile(dlat[idx1], 25))
907 rms2Long = IqrToSigma*(
908 (numpy.percentile(dlong[idx2], 75) - numpy.percentile(dlong[idx2], 25)))
909 rms2Lat = IqrToSigma*(numpy.percentile(dlat[idx2], 75)
910 - numpy.percentile(dlat[idx2], 25))
911 rms3Long = IqrToSigma*(
912 (numpy.percentile(dlong[idx3], 75) - numpy.percentile(dlong[idx3], 25)))
913 rms3Lat = IqrToSigma*(numpy.percentile(dlat[idx3], 75)
914 - numpy.percentile(dlat[idx3], 25))
915 self.log.
info(
"Blue star offsets'': %.3f %.3f, %.3f %.3f",
916 numpy.median(dlong[idx1]), rms1Long,
917 numpy.median(dlat[idx1]), rms1Lat)
918 self.log.
info(
"Green star offsets'': %.3f %.3f, %.3f %.3f",
919 numpy.median(dlong[idx2]), rms2Long,
920 numpy.median(dlat[idx2]), rms2Lat)
921 self.log.
info(
"Red star offsets'': %.3f %.3f, %.3f %.3f",
922 numpy.median(dlong[idx3]), rms3Long,
923 numpy.median(dlat[idx3]), rms3Lat)
925 self.metadata.add(
"RegisterBlueLongOffsetMedian", numpy.median(dlong[idx1]))
926 self.metadata.add(
"RegisterGreenLongOffsetMedian", numpy.median(dlong[idx2]))
927 self.metadata.add(
"RegisterRedLongOffsetMedian", numpy.median(dlong[idx3]))
928 self.metadata.add(
"RegisterBlueLongOffsetStd", rms1Long)
929 self.metadata.add(
"RegisterGreenLongOffsetStd", rms2Long)
930 self.metadata.add(
"RegisterRedLongOffsetStd", rms3Long)
932 self.metadata.add(
"RegisterBlueLatOffsetMedian", numpy.median(dlat[idx1]))
933 self.metadata.add(
"RegisterGreenLatOffsetMedian", numpy.median(dlat[idx2]))
934 self.metadata.add(
"RegisterRedLatOffsetMedian", numpy.median(dlat[idx3]))
935 self.metadata.add(
"RegisterBlueLatOffsetStd", rms1Lat)
936 self.metadata.add(
"RegisterGreenLatOffsetStd", rms2Lat)
937 self.metadata.add(
"RegisterRedLatOffsetStd", rms3Lat)
944 self.log.
info(
"Subtracting images")
945 subtractRes = self.subtract.subtractExposures(
946 templateExposure=templateExposure,
947 scienceExposure=exposure,
948 candidateList=kernelSources,
949 convolveTemplate=self.config.convolveTemplate,
950 doWarping=
not self.config.doUseRegister
952 if self.config.useScoreImageDetection:
953 scoreExposure = subtractRes.subtractedExposure
955 subtractedExposure = subtractRes.subtractedExposure
957 if self.config.doDetection:
958 self.log.
info(
"Computing diffim PSF")
961 if subtractedExposure
is not None and not subtractedExposure.hasPsf():
962 if self.config.convolveTemplate:
963 subtractedExposure.setPsf(exposure.getPsf())
965 subtractedExposure.setPsf(templateExposure.getPsf())
972 if self.config.doDecorrelation
and self.config.doSubtract:
974 if self.config.useGaussianForPreConvolution:
975 preConvKernel = preConvPsf.getLocalKernel()
976 if self.config.useScoreImageDetection:
977 scoreExposure = self.decorrelate.
run(exposureOrig, subtractRes.warpedExposure,
979 subtractRes.psfMatchingKernel,
980 spatiallyVarying=self.config.doSpatiallyVarying,
981 preConvKernel=preConvKernel,
982 templateMatched=
True,
983 preConvMode=
True).correctedExposure
986 subtractedExposure = self.decorrelate.
run(exposureOrig, subtractRes.warpedExposure,
988 subtractRes.psfMatchingKernel,
989 spatiallyVarying=self.config.doSpatiallyVarying,
991 templateMatched=self.config.convolveTemplate,
992 preConvMode=
False).correctedExposure
995 if self.config.doDetection:
996 self.log.
info(
"Running diaSource detection")
1004 if self.config.useScoreImageDetection:
1006 self.log.
info(
"Detection, diffim rescaling and measurements are "
1007 "on AL likelihood or Zogy score image.")
1008 detectionExposure = scoreExposure
1011 detectionExposure = subtractedExposure
1014 if self.config.doScaleDiffimVariance:
1015 self.log.
info(
"Rescaling diffim variance")
1016 diffimVarFactor = self.scaleVariance.
run(detectionExposure.getMaskedImage())
1017 self.log.
info(
"Diffim variance scaling factor: %.2f", diffimVarFactor)
1018 self.metadata.add(
"scaleDiffimVarianceFactor", diffimVarFactor)
1021 mask = detectionExposure.getMaskedImage().getMask()
1022 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
1024 table = afwTable.SourceTable.make(self.schema, idFactory)
1025 table.setMetadata(self.algMetadata)
1026 results = self.detection.
run(
1028 exposure=detectionExposure,
1029 doSmooth=
not self.config.useScoreImageDetection
1032 if self.config.doMerge:
1033 fpSet = results.fpSets.positive
1034 fpSet.merge(results.fpSets.negative, self.config.growFootprint,
1035 self.config.growFootprint,
False)
1037 fpSet.makeSources(diaSources)
1038 self.log.
info(
"Merging detections into %d sources", len(diaSources))
1040 diaSources = results.sources
1042 if self.config.doSkySources:
1043 skySourceFootprints = self.skySources.
run(
1044 mask=detectionExposure.mask,
1045 seed=detectionExposure.info.id)
1046 if skySourceFootprints:
1047 for foot
in skySourceFootprints:
1048 s = diaSources.addNew()
1049 s.setFootprint(foot)
1050 s.set(self.skySourceKey,
True)
1052 if self.config.doMeasurement:
1053 newDipoleFitting = self.config.doDipoleFitting
1054 self.log.
info(
"Running diaSource measurement: newDipoleFitting=%r", newDipoleFitting)
1055 if not newDipoleFitting:
1057 self.measurement.
run(diaSources, detectionExposure)
1060 if self.config.doSubtract
and 'matchedExposure' in subtractRes.getDict():
1061 self.measurement.
run(diaSources, detectionExposure, exposure,
1062 subtractRes.matchedExposure)
1064 self.measurement.
run(diaSources, detectionExposure, exposure)
1065 if self.config.doApCorr:
1066 self.applyApCorr.
run(
1068 apCorrMap=detectionExposure.getInfo().getApCorrMap()
1071 if self.config.doForcedMeasurement:
1074 forcedSources = self.forcedMeasurement.generateMeasCat(
1075 exposure, diaSources, detectionExposure.getWcs())
1076 self.forcedMeasurement.
run(forcedSources, exposure, diaSources, detectionExposure.getWcs())
1078 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
1079 "ip_diffim_forced_PsfFlux_instFlux",
True)
1080 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
1081 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
1082 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
1083 "ip_diffim_forced_PsfFlux_area",
True)
1084 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
1085 "ip_diffim_forced_PsfFlux_flag",
True)
1086 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
1087 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
1088 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
1089 "ip_diffim_forced_PsfFlux_flag_edge",
True)
1090 for diaSource, forcedSource
in zip(diaSources, forcedSources):
1091 diaSource.assign(forcedSource, mapper)
1094 if self.config.doMatchSources:
1095 if selectSources
is not None:
1097 matchRadAsec = self.config.diaSourceMatchRadius
1098 matchRadPixel = matchRadAsec/exposure.getWcs().getPixelScale().asArcseconds()
1101 srcMatchDict = dict([(srcMatch.second.getId(), srcMatch.first.getId())
for
1102 srcMatch
in srcMatches])
1103 self.log.
info(
"Matched %d / %d diaSources to sources",
1104 len(srcMatchDict), len(diaSources))
1106 self.log.
warning(
"Src product does not exist; cannot match with diaSources")
1111 refAstromConfig.matcher.maxMatchDistArcSec = matchRadAsec
1113 astromRet = refAstrometer.run(exposure=exposure, sourceCat=diaSources)
1114 refMatches = astromRet.matches
1115 if refMatches
is None:
1116 self.log.
warning(
"No diaSource matches with reference catalog")
1119 self.log.
info(
"Matched %d / %d diaSources to reference catalog",
1120 len(refMatches), len(diaSources))
1121 refMatchDict = dict([(refMatch.second.getId(), refMatch.first.getId())
for
1122 refMatch
in refMatches])
1125 for diaSource
in diaSources:
1126 sid = diaSource.getId()
1127 if sid
in srcMatchDict:
1128 diaSource.set(
"srcMatchId", srcMatchDict[sid])
1129 if sid
in refMatchDict:
1130 diaSource.set(
"refMatchId", refMatchDict[sid])
1132 if self.config.doAddMetrics
and self.config.doSelectSources:
1133 self.log.
info(
"Evaluating metrics and control sample")
1136 for cell
in subtractRes.kernelCellSet.getCellList():
1137 for cand
in cell.begin(
False):
1138 kernelCandList.append(cand)
1141 basisList = kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelList()
1142 nparam = len(kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelParameters())
1145 diffimTools.sourceTableToCandidateList(controlSources,
1146 subtractRes.warpedExposure, exposure,
1147 self.config.subtract.kernel.active,
1148 self.config.subtract.kernel.active.detectionConfig,
1149 self.log, doBuild=
True, basisList=basisList))
1151 KernelCandidateQa.apply(kernelCandList, subtractRes.psfMatchingKernel,
1152 subtractRes.backgroundModel, dof=nparam)
1153 KernelCandidateQa.apply(controlCandList, subtractRes.psfMatchingKernel,
1154 subtractRes.backgroundModel)
1156 if self.config.doDetection:
1157 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids, diaSources)
1159 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids)
1161 self.runDebug(exposure, subtractRes, selectSources, kernelSources, diaSources)
1162 return pipeBase.Struct(
1163 subtractedExposure=subtractedExposure,
1164 scoreExposure=scoreExposure,
1165 warpedExposure=subtractRes.warpedExposure,
1166 matchedExposure=subtractRes.matchedExposure,
1167 subtractRes=subtractRes,
1168 diaSources=diaSources,
1169 selectSources=selectSources
1172 def fitAstrometry(self, templateSources, templateExposure, selectSources):
1173 """Fit the relative astrometry between templateSources and selectSources
1178 Remove this method. It originally fit a new WCS to the template before calling register.run
1179 because our TAN-SIP fitter behaved badly for points far from CRPIX, but that's been fixed.
1180 It remains because a subtask overrides it.
1182 results = self.register.
run(templateSources, templateExposure.getWcs(),
1183 templateExposure.getBBox(), selectSources)
1186 def runDebug(self, exposure, subtractRes, selectSources, kernelSources, diaSources):
1187 """Make debug plots and displays.
1191 Test and update for current debug display and slot names
1201 disp = afwDisplay.getDisplay(frame=lsstDebug.frame)
1202 if not maskTransparency:
1203 maskTransparency = 0
1204 disp.setMaskTransparency(maskTransparency)
1206 if display
and showSubtracted:
1207 disp.mtv(subtractRes.subtractedExposure, title=
"Subtracted image")
1208 mi = subtractRes.subtractedExposure.getMaskedImage()
1209 x0, y0 = mi.getX0(), mi.getY0()
1210 with disp.Buffering():
1211 for s
in diaSources:
1212 x, y = s.getX() - x0, s.getY() - y0
1213 ctype =
"red" if s.get(
"flags_negative")
else "yellow"
1214 if (s.get(
"base_PixelFlags_flag_interpolatedCenter")
1215 or s.get(
"base_PixelFlags_flag_saturatedCenter")
1216 or s.get(
"base_PixelFlags_flag_crCenter")):
1218 elif (s.get(
"base_PixelFlags_flag_interpolated")
1219 or s.get(
"base_PixelFlags_flag_saturated")
1220 or s.get(
"base_PixelFlags_flag_cr")):
1224 disp.dot(ptype, x, y, size=4, ctype=ctype)
1225 lsstDebug.frame += 1
1227 if display
and showPixelResiduals
and selectSources:
1228 nonKernelSources = []
1229 for source
in selectSources:
1230 if source
not in kernelSources:
1231 nonKernelSources.append(source)
1233 diUtils.plotPixelResiduals(exposure,
1234 subtractRes.warpedExposure,
1235 subtractRes.subtractedExposure,
1236 subtractRes.kernelCellSet,
1237 subtractRes.psfMatchingKernel,
1238 subtractRes.backgroundModel,
1240 self.subtract.config.kernel.active.detectionConfig,
1242 diUtils.plotPixelResiduals(exposure,
1243 subtractRes.warpedExposure,
1244 subtractRes.subtractedExposure,
1245 subtractRes.kernelCellSet,
1246 subtractRes.psfMatchingKernel,
1247 subtractRes.backgroundModel,
1249 self.subtract.config.kernel.active.detectionConfig,
1251 if display
and showDiaSources:
1253 isFlagged = [flagChecker(x)
for x
in diaSources]
1254 isDipole = [x.get(
"ip_diffim_ClassificationDipole_value")
for x
in diaSources]
1255 diUtils.showDiaSources(diaSources, subtractRes.subtractedExposure, isFlagged, isDipole,
1256 frame=lsstDebug.frame)
1257 lsstDebug.frame += 1
1259 if display
and showDipoles:
1260 DipoleAnalysis().displayDipoles(subtractRes.subtractedExposure, diaSources,
1261 frame=lsstDebug.frame)
1262 lsstDebug.frame += 1
1264 def checkTemplateIsSufficient(self, templateExposure):
1265 """Raise NoWorkFound if template coverage < requiredTemplateFraction
1269 templateExposure : `lsst.afw.image.ExposureF`
1270 The template exposure to check
1275 Raised if fraction of good pixels, defined as not having NO_DATA
1276 set, is less then the configured requiredTemplateFraction
1280 pixNoData = numpy.count_nonzero(templateExposure.mask.array
1281 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
1282 pixGood = templateExposure.getBBox().getArea() - pixNoData
1283 self.log.
info(
"template has %d good pixels (%.1f%%)", pixGood,
1284 100*pixGood/templateExposure.getBBox().getArea())
1286 if pixGood/templateExposure.getBBox().getArea() < self.config.requiredTemplateFraction:
1287 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%) Not attempting subtraction. "
1288 "To force subtraction, set config requiredTemplateFraction=0." % (
1289 100*pixGood/templateExposure.getBBox().getArea(),
1290 100*self.config.requiredTemplateFraction))
1291 raise pipeBase.NoWorkFound(message)
1293 def _getConfigName(self):
1294 """Return the name of the config dataset
1296 return "%sDiff_config" % (self.config.coaddName,)
1298 def _getMetadataName(self):
1299 """Return the name of the metadata dataset
1301 return "%sDiff_metadata" % (self.config.coaddName,)
1304 """Return a dict of empty catalogs for each catalog dataset produced by this task."""
1305 return {self.config.coaddName +
"Diff_diaSrc": self.outputSchema}
1308 def _makeArgumentParser(cls):
1309 """Create an argument parser
1311 parser = pipeBase.ArgumentParser(name=cls._DefaultName)
1312 parser.add_id_argument(
"--id",
"calexp", help=
"data ID, e.g. --id visit=12345 ccd=1,2")
1313 parser.add_id_argument(
"--templateId",
"calexp", doMakeDataRefList=
True,
1314 help=
"Template data ID in case of calexp template,"
1315 " e.g. --templateId visit=6789")
1320 defaultTemplates={
"coaddName":
"goodSeeing"}
1322 inputTemplate = pipeBase.connectionTypes.Input(
1323 doc=(
"Warped template produced by GetMultiTractCoaddTemplate"),
1324 dimensions=(
"instrument",
"visit",
"detector"),
1325 storageClass=
"ExposureF",
1326 name=
"{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
1329 def __init__(self, *, config=None):
1330 super().__init__(config=config)
1333 if "coaddExposures" in self.inputs:
1334 self.inputs.remove(
"coaddExposures")
1335 if "dcrCoadds" in self.inputs:
1336 self.inputs.remove(
"dcrCoadds")
1340 pipelineConnections=ImageDifferenceFromTemplateConnections):
1345 ConfigClass = ImageDifferenceFromTemplateConfig
1346 _DefaultName =
"imageDifference"
1348 @lsst.utils.inheritDoc(pipeBase.PipelineTask)
1350 inputs = butlerQC.get(inputRefs)
1351 self.log.
info(
"Processing %s", butlerQC.quantum.dataId)
1352 self.checkTemplateIsSufficient(inputs[
'inputTemplate'])
1353 expId, expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
1355 idFactory = self.makeIdFactory(expId=expId, expBits=expBits)
1357 outputs = self.run(exposure=inputs[
'exposure'],
1358 templateExposure=inputs[
'inputTemplate'],
1359 idFactory=idFactory)
1362 if outputs.diaSources
is None:
1363 del outputs.diaSources
1364 butlerQC.put(outputs, outputRefs)
1368 winter2013WcsShift = pexConfig.Field(dtype=float, default=0.0,
1369 doc=
"Shift stars going into RegisterTask by this amount")
1370 winter2013WcsRms = pexConfig.Field(dtype=float, default=0.0,
1371 doc=
"Perturb stars going into RegisterTask by this amount")
1374 ImageDifferenceConfig.setDefaults(self)
1375 self.getTemplate.retarget(GetCalexpAsTemplateTask)
1379 """!Image difference Task used in the Winter 2013 data challege.
1380 Enables testing the effects of registration shifts and scatter.
1382 For use with winter 2013 simulated images:
1383 Use --templateId visit=88868666 for sparse data
1384 --templateId visit=22222200 for dense data (g)
1385 --templateId visit=11111100 for dense data (i)
1387 ConfigClass = Winter2013ImageDifferenceConfig
1388 _DefaultName =
"winter2013ImageDifference"
1391 ImageDifferenceTask.__init__(self, **kwargs)
1394 """Fit the relative astrometry between templateSources and selectSources"""
1395 if self.config.winter2013WcsShift > 0.0:
1397 self.config.winter2013WcsShift)
1398 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1399 for source
in templateSources:
1400 centroid = source.get(cKey)
1401 source.set(cKey, centroid + offset)
1402 elif self.config.winter2013WcsRms > 0.0:
1403 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1404 for source
in templateSources:
1405 offset =
geom.Extent2D(self.config.winter2013WcsRms*numpy.random.normal(),
1406 self.config.winter2013WcsRms*numpy.random.normal())
1407 centroid = source.get(cKey)
1408 source.set(cKey, centroid + offset)
1410 results = self.register.
run(templateSources, templateExposure.getWcs(),
1411 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)
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations.
def getSchemaCatalogs(self)