33import lsst.meas.extensions.trailedSources
39from lsst.meas.algorithms import SourceDetectionTask, SingleGaussianPsf, ObjectSizeStarSelectorTask
40from lsst.ip.diffim import (DipoleAnalysis, SourceFlagChecker, KernelCandidateF, makeKernelBasisList,
41 KernelCandidateQa, DiaCatalogSourceSelectorTask, DiaCatalogSourceSelectorConfig,
42 GetCoaddAsTemplateTask, GetCalexpAsTemplateTask, DipoleFitTask,
43 DecorrelateALKernelSpatialTask, subtractAlgorithmRegistry)
48from lsst.obs.base
import ExposureIdInfo
49from lsst.utils.timer
import timeMethod
51__all__ = [
"ImageDifferenceConfig",
"ImageDifferenceTask"]
52FwhmPerSigma = 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")
157class 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",
266 doApCorr = lsst.pex.config.Field(
269 doc=
"Run subtask to apply aperture corrections"
271 applyApCorr = lsst.pex.config.ConfigurableField(
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",
334 def setDefaults(self):
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 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.")
422class ImageDifferenceTaskRunner(pipeBase.ButlerInitializedTaskRunner):
425 def getTargetList(parsedCmd, **kwargs):
426 return pipeBase.TaskRunner.getTargetList(parsedCmd, templateIdList=parsedCmd.templateId.idList,
430class ImageDifferenceTask(pipeBase.CmdLineTask, pipeBase.PipelineTask):
431 """Subtract an image from a template and measure the result
433 ConfigClass = ImageDifferenceConfig
434 RunnerClass = ImageDifferenceTaskRunner
435 _DefaultName = "imageDifference"
437 def __init__(self, butler=None, **kwargs):
438 """!Construct an ImageDifference Task
440 @param[
in] butler Butler object to use
in constructing reference object loaders
442 super().__init__(**kwargs)
443 self.makeSubtask("getTemplate")
445 self.makeSubtask(
"subtract")
447 if self.config.subtract.name ==
'al' and self.config.doDecorrelation:
448 self.makeSubtask(
"decorrelate")
450 if self.config.doScaleTemplateVariance
or self.config.doScaleDiffimVariance:
451 self.makeSubtask(
"scaleVariance")
453 if self.config.doUseRegister:
454 self.makeSubtask(
"register")
455 self.schema = afwTable.SourceTable.makeMinimalSchema()
457 if self.config.doSelectSources:
458 self.makeSubtask(
"sourceSelector")
459 if self.config.kernelSourcesFromRef:
460 self.makeSubtask(
'refObjLoader', butler=butler)
461 self.makeSubtask(
"astrometer", refObjLoader=self.refObjLoader)
463 self.algMetadata = dafBase.PropertyList()
464 if self.config.doDetection:
465 self.makeSubtask(
"detection", schema=self.schema)
466 if self.config.doMeasurement:
467 self.makeSubtask(
"measurement", schema=self.schema,
468 algMetadata=self.algMetadata)
469 if self.config.doApCorr:
470 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
471 if self.config.doForcedMeasurement:
472 self.schema.addField(
473 "ip_diffim_forced_PsfFlux_instFlux",
"D",
474 "Forced PSF flux measured on the direct image.",
476 self.schema.addField(
477 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
478 "Forced PSF flux error measured on the direct image.",
480 self.schema.addField(
481 "ip_diffim_forced_PsfFlux_area",
"F",
482 "Forced PSF flux effective area of PSF.",
484 self.schema.addField(
485 "ip_diffim_forced_PsfFlux_flag",
"Flag",
486 "Forced PSF flux general failure flag.")
487 self.schema.addField(
488 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
489 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
490 self.schema.addField(
491 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
492 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
493 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
494 if self.config.doMatchSources:
495 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
496 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
497 if self.config.doSkySources:
498 self.makeSubtask(
"skySources")
499 self.skySourceKey = self.schema.addField(
"sky_source", type=
"Flag", doc=
"Sky objects.")
502 self.outputSchema = afwTable.SourceCatalog(self.schema)
503 self.outputSchema.getTable().setMetadata(self.algMetadata)
506 def makeIdFactory(expId, expBits):
507 """Create IdFactory instance for unique 64 bit diaSource id-s.
515 Number of used bits in ``expId``.
519 The diasource id-s consists of the ``expId`` stored fixed
in the highest value
520 ``expBits`` of the 64-bit integer plus (bitwise
or) a generated sequence number
in the
521 low value end of the integer.
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``.
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.
660 Identified sources on the template exposure.
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.
683 The catalog of detected sources.
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
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':
736 sciAvgPos = sciencePsf.getAveragePosition()
737 scienceSigmaOrig = sciencePsf.computeShape(sciAvgPos).getDeterminantRadius()
739 templatePsf = templateExposure.getPsf()
740 templateAvgPos = templatePsf.getAveragePosition()
741 templateSigma = templatePsf.computeShape(templateAvgPos).getDeterminantRadius()
749 if self.config.useScoreImageDetection:
750 self.log.warning(
"AL likelihood image: pre-convolution of PSF is not implemented.")
751 convControl = afwMath.ConvolutionControl()
753 srcMI = exposure.maskedImage
754 exposure = exposure.clone()
756 if self.config.useGaussianForPreConvolution:
758 "AL likelihood image: Using Gaussian (sigma=%.2f) PSF estimation "
759 "for science image pre-convolution", scienceSigmaOrig)
761 kWidth, kHeight = sciencePsf.getLocalKernel().getDimensions()
766 "AL likelihood image: Using the science image PSF for pre-convolution.")
768 afwMath.convolve(exposure.maskedImage, srcMI, preConvPsf.getLocalKernel(), convControl)
769 scienceSigmaPost = scienceSigmaOrig*math.sqrt(2)
771 scienceSigmaPost = scienceSigmaOrig
776 if self.config.doSelectSources:
777 if selectSources
is None:
778 self.log.warning(
"Src product does not exist; running detection, measurement,"
781 selectSources = self.subtract.getSelectSources(
783 sigma=scienceSigmaPost,
784 doSmooth=
not self.config.useScoreImageDetection,
788 if self.config.doAddMetrics:
791 nparam = len(makeKernelBasisList(self.subtract.config.kernel.active,
792 referenceFwhmPix=scienceSigmaPost*FwhmPerSigma,
793 targetFwhmPix=templateSigma*FwhmPerSigma))
800 kcQa = KernelCandidateQa(nparam)
801 selectSources = kcQa.addToSchema(selectSources)
802 if self.config.kernelSourcesFromRef:
804 astromRet = self.astrometer.loadAndMatch(exposure=exposure, sourceCat=selectSources)
805 matches = astromRet.matches
806 elif templateSources:
808 mc = afwTable.MatchControl()
809 mc.findOnlyClosest =
False
810 matches = afwTable.matchRaDec(templateSources, selectSources, 1.0*geom.arcseconds,
813 raise RuntimeError(
"doSelectSources=True and kernelSourcesFromRef=False,"
814 "but template sources not available. Cannot match science "
815 "sources with template sources. Run process* on data from "
816 "which templates are built.")
818 kernelSources = self.sourceSelector.run(selectSources, exposure=exposure,
819 matches=matches).sourceCat
820 random.shuffle(kernelSources, random.random)
821 controlSources = kernelSources[::self.config.controlStepSize]
822 kernelSources = [k
for i, k
in enumerate(kernelSources)
823 if i % self.config.controlStepSize]
825 if self.config.doSelectDcrCatalog:
826 redSelector = DiaCatalogSourceSelectorTask(
827 DiaCatalogSourceSelectorConfig(grMin=self.sourceSelector.config.grMax,
829 redSources = redSelector.selectStars(exposure, selectSources, matches=matches).starCat
830 controlSources.extend(redSources)
832 blueSelector = DiaCatalogSourceSelectorTask(
833 DiaCatalogSourceSelectorConfig(grMin=-99.999,
834 grMax=self.sourceSelector.config.grMin))
835 blueSources = blueSelector.selectStars(exposure, selectSources,
836 matches=matches).starCat
837 controlSources.extend(blueSources)
839 if self.config.doSelectVariableCatalog:
840 varSelector = DiaCatalogSourceSelectorTask(
841 DiaCatalogSourceSelectorConfig(includeVariable=
True))
842 varSources = varSelector.selectStars(exposure, selectSources, matches=matches).starCat
843 controlSources.extend(varSources)
845 self.log.info(
"Selected %d / %d sources for Psf matching (%d for control sample)",
846 len(kernelSources), len(selectSources), len(controlSources))
850 if self.config.doUseRegister:
851 self.log.info(
"Registering images")
853 if templateSources
is None:
857 templateSources = self.subtract.getSelectSources(
866 wcsResults = self.fitAstrometry(templateSources, templateExposure, selectSources)
867 warpedExp = self.register.warpExposure(templateExposure, wcsResults.wcs,
868 exposure.getWcs(), exposure.getBBox())
869 templateExposure = warpedExp
874 if self.config.doDebugRegister:
876 srcToMatch = {x.second.getId(): x.first
for x
in matches}
878 refCoordKey = wcsResults.matches[0].first.getTable().getCoordKey()
879 inCentroidKey = wcsResults.matches[0].second.getTable().getCentroidSlot().getMeasKey()
880 sids = [m.first.getId()
for m
in wcsResults.matches]
881 positions = [m.first.get(refCoordKey)
for m
in wcsResults.matches]
882 residuals = [m.first.get(refCoordKey).getOffsetFrom(wcsResults.wcs.pixelToSky(
883 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
884 allresids = dict(zip(sids, zip(positions, residuals)))
886 cresiduals = [m.first.get(refCoordKey).getTangentPlaneOffset(
887 wcsResults.wcs.pixelToSky(
888 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
889 colors = numpy.array([-2.5*numpy.log10(srcToMatch[x].get(
"g"))
890 + 2.5*numpy.log10(srcToMatch[x].get(
"r"))
891 for x
in sids
if x
in srcToMatch.keys()])
892 dlong = numpy.array([r[0].asArcseconds()
for s, r
in zip(sids, cresiduals)
893 if s
in srcToMatch.keys()])
894 dlat = numpy.array([r[1].asArcseconds()
for s, r
in zip(sids, cresiduals)
895 if s
in srcToMatch.keys()])
896 idx1 = numpy.where(colors < self.sourceSelector.config.grMin)
897 idx2 = numpy.where((colors >= self.sourceSelector.config.grMin)
898 & (colors <= self.sourceSelector.config.grMax))
899 idx3 = numpy.where(colors > self.sourceSelector.config.grMax)
900 rms1Long = IqrToSigma*(
901 (numpy.percentile(dlong[idx1], 75) - numpy.percentile(dlong[idx1], 25)))
902 rms1Lat = IqrToSigma*(numpy.percentile(dlat[idx1], 75)
903 - numpy.percentile(dlat[idx1], 25))
904 rms2Long = IqrToSigma*(
905 (numpy.percentile(dlong[idx2], 75) - numpy.percentile(dlong[idx2], 25)))
906 rms2Lat = IqrToSigma*(numpy.percentile(dlat[idx2], 75)
907 - numpy.percentile(dlat[idx2], 25))
908 rms3Long = IqrToSigma*(
909 (numpy.percentile(dlong[idx3], 75) - numpy.percentile(dlong[idx3], 25)))
910 rms3Lat = IqrToSigma*(numpy.percentile(dlat[idx3], 75)
911 - numpy.percentile(dlat[idx3], 25))
912 self.log.info(
"Blue star offsets'': %.3f %.3f, %.3f %.3f",
913 numpy.median(dlong[idx1]), rms1Long,
914 numpy.median(dlat[idx1]), rms1Lat)
915 self.log.info(
"Green star offsets'': %.3f %.3f, %.3f %.3f",
916 numpy.median(dlong[idx2]), rms2Long,
917 numpy.median(dlat[idx2]), rms2Lat)
918 self.log.info(
"Red star offsets'': %.3f %.3f, %.3f %.3f",
919 numpy.median(dlong[idx3]), rms3Long,
920 numpy.median(dlat[idx3]), rms3Lat)
922 self.metadata.add(
"RegisterBlueLongOffsetMedian", numpy.median(dlong[idx1]))
923 self.metadata.add(
"RegisterGreenLongOffsetMedian", numpy.median(dlong[idx2]))
924 self.metadata.add(
"RegisterRedLongOffsetMedian", numpy.median(dlong[idx3]))
925 self.metadata.add(
"RegisterBlueLongOffsetStd", rms1Long)
926 self.metadata.add(
"RegisterGreenLongOffsetStd", rms2Long)
927 self.metadata.add(
"RegisterRedLongOffsetStd", rms3Long)
929 self.metadata.add(
"RegisterBlueLatOffsetMedian", numpy.median(dlat[idx1]))
930 self.metadata.add(
"RegisterGreenLatOffsetMedian", numpy.median(dlat[idx2]))
931 self.metadata.add(
"RegisterRedLatOffsetMedian", numpy.median(dlat[idx3]))
932 self.metadata.add(
"RegisterBlueLatOffsetStd", rms1Lat)
933 self.metadata.add(
"RegisterGreenLatOffsetStd", rms2Lat)
934 self.metadata.add(
"RegisterRedLatOffsetStd", rms3Lat)
941 self.log.info(
"Subtracting images")
942 subtractRes = self.subtract.subtractExposures(
943 templateExposure=templateExposure,
944 scienceExposure=exposure,
945 candidateList=kernelSources,
946 convolveTemplate=self.config.convolveTemplate,
947 doWarping=
not self.config.doUseRegister
949 if self.config.useScoreImageDetection:
950 scoreExposure = subtractRes.subtractedExposure
952 subtractedExposure = subtractRes.subtractedExposure
954 if self.config.doDetection:
955 self.log.info(
"Computing diffim PSF")
958 if subtractedExposure
is not None and not subtractedExposure.hasPsf():
959 if self.config.convolveTemplate:
960 subtractedExposure.setPsf(exposure.getPsf())
962 subtractedExposure.setPsf(templateExposure.getPsf())
969 if self.config.doDecorrelation
and self.config.doSubtract:
971 if self.config.useGaussianForPreConvolution:
972 preConvKernel = preConvPsf.getLocalKernel()
973 if self.config.useScoreImageDetection:
974 scoreExposure = self.decorrelate.run(exposureOrig, subtractRes.warpedExposure,
976 subtractRes.psfMatchingKernel,
977 spatiallyVarying=self.config.doSpatiallyVarying,
978 preConvKernel=preConvKernel,
979 templateMatched=
True,
980 preConvMode=
True).correctedExposure
983 subtractedExposure = self.decorrelate.run(exposureOrig, subtractRes.warpedExposure,
985 subtractRes.psfMatchingKernel,
986 spatiallyVarying=self.config.doSpatiallyVarying,
988 templateMatched=self.config.convolveTemplate,
989 preConvMode=
False).correctedExposure
992 if self.config.doDetection:
993 self.log.info(
"Running diaSource detection")
1001 if self.config.useScoreImageDetection:
1003 self.log.info(
"Detection, diffim rescaling and measurements are "
1004 "on AL likelihood or Zogy score image.")
1005 detectionExposure = scoreExposure
1008 detectionExposure = subtractedExposure
1011 if self.config.doScaleDiffimVariance:
1012 self.log.info(
"Rescaling diffim variance")
1013 diffimVarFactor = self.scaleVariance.run(detectionExposure.getMaskedImage())
1014 self.log.info(
"Diffim variance scaling factor: %.2f", diffimVarFactor)
1015 self.metadata.add(
"scaleDiffimVarianceFactor", diffimVarFactor)
1018 mask = detectionExposure.getMaskedImage().getMask()
1019 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
1021 table = afwTable.SourceTable.make(self.schema, idFactory)
1022 table.setMetadata(self.algMetadata)
1023 results = self.detection.run(
1025 exposure=detectionExposure,
1026 doSmooth=
not self.config.useScoreImageDetection
1029 if self.config.doMerge:
1030 fpSet = results.fpSets.positive
1031 fpSet.merge(results.fpSets.negative, self.config.growFootprint,
1032 self.config.growFootprint,
False)
1033 diaSources = afwTable.SourceCatalog(table)
1034 fpSet.makeSources(diaSources)
1035 self.log.info(
"Merging detections into %d sources", len(diaSources))
1037 diaSources = results.sources
1039 if self.config.doSkySources:
1040 skySourceFootprints = self.skySources.run(
1041 mask=detectionExposure.mask,
1042 seed=detectionExposure.info.id)
1043 if skySourceFootprints:
1044 for foot
in skySourceFootprints:
1045 s = diaSources.addNew()
1046 s.setFootprint(foot)
1047 s.set(self.skySourceKey,
True)
1049 if self.config.doMeasurement:
1050 newDipoleFitting = self.config.doDipoleFitting
1051 self.log.info(
"Running diaSource measurement: newDipoleFitting=%r", newDipoleFitting)
1052 if not newDipoleFitting:
1054 self.measurement.run(diaSources, detectionExposure)
1057 if self.config.doSubtract
and 'matchedExposure' in subtractRes.getDict():
1058 self.measurement.run(diaSources, detectionExposure, exposure,
1059 subtractRes.matchedExposure)
1061 self.measurement.run(diaSources, detectionExposure, exposure)
1062 if self.config.doApCorr:
1063 self.applyApCorr.run(
1065 apCorrMap=detectionExposure.getInfo().getApCorrMap()
1068 if self.config.doForcedMeasurement:
1071 forcedSources = self.forcedMeasurement.generateMeasCat(
1072 exposure, diaSources, detectionExposure.getWcs())
1073 self.forcedMeasurement.run(forcedSources, exposure, diaSources, detectionExposure.getWcs())
1074 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
1075 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
1076 "ip_diffim_forced_PsfFlux_instFlux",
True)
1077 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
1078 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
1079 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
1080 "ip_diffim_forced_PsfFlux_area",
True)
1081 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
1082 "ip_diffim_forced_PsfFlux_flag",
True)
1083 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
1084 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
1085 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
1086 "ip_diffim_forced_PsfFlux_flag_edge",
True)
1087 for diaSource, forcedSource
in zip(diaSources, forcedSources):
1088 diaSource.assign(forcedSource, mapper)
1091 if self.config.doMatchSources:
1092 if selectSources
is not None:
1094 matchRadAsec = self.config.diaSourceMatchRadius
1095 matchRadPixel = matchRadAsec/exposure.getWcs().getPixelScale().asArcseconds()
1097 srcMatches = afwTable.matchXy(selectSources, diaSources, matchRadPixel)
1098 srcMatchDict = dict([(srcMatch.second.getId(), srcMatch.first.getId())
for
1099 srcMatch
in srcMatches])
1100 self.log.info(
"Matched %d / %d diaSources to sources",
1101 len(srcMatchDict), len(diaSources))
1103 self.log.warning(
"Src product does not exist; cannot match with diaSources")
1107 refAstromConfig = AstrometryConfig()
1108 refAstromConfig.matcher.maxMatchDistArcSec = matchRadAsec
1109 refAstrometer = AstrometryTask(self.refObjLoader, config=refAstromConfig)
1110 astromRet = refAstrometer.run(exposure=exposure, sourceCat=diaSources)
1111 refMatches = astromRet.matches
1112 if refMatches
is None:
1113 self.log.warning(
"No diaSource matches with reference catalog")
1116 self.log.info(
"Matched %d / %d diaSources to reference catalog",
1117 len(refMatches), len(diaSources))
1118 refMatchDict = dict([(refMatch.second.getId(), refMatch.first.getId())
for
1119 refMatch
in refMatches])
1122 for diaSource
in diaSources:
1123 sid = diaSource.getId()
1124 if sid
in srcMatchDict:
1125 diaSource.set(
"srcMatchId", srcMatchDict[sid])
1126 if sid
in refMatchDict:
1127 diaSource.set(
"refMatchId", refMatchDict[sid])
1129 if self.config.doAddMetrics
and self.config.doSelectSources:
1130 self.log.info(
"Evaluating metrics and control sample")
1133 for cell
in subtractRes.kernelCellSet.getCellList():
1134 for cand
in cell.begin(
False):
1135 kernelCandList.append(cand)
1138 basisList = kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelList()
1139 nparam = len(kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelParameters())
1142 diffimTools.sourceTableToCandidateList(controlSources,
1143 subtractRes.warpedExposure, exposure,
1144 self.config.subtract.kernel.active,
1145 self.config.subtract.kernel.active.detectionConfig,
1146 self.log, doBuild=
True, basisList=basisList))
1148 KernelCandidateQa.apply(kernelCandList, subtractRes.psfMatchingKernel,
1149 subtractRes.backgroundModel, dof=nparam)
1150 KernelCandidateQa.apply(controlCandList, subtractRes.psfMatchingKernel,
1151 subtractRes.backgroundModel)
1153 if self.config.doDetection:
1154 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids, diaSources)
1156 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids)
1158 self.runDebug(exposure, subtractRes, selectSources, kernelSources, diaSources)
1159 return pipeBase.Struct(
1160 subtractedExposure=subtractedExposure,
1161 scoreExposure=scoreExposure,
1162 warpedExposure=subtractRes.warpedExposure,
1163 matchedExposure=subtractRes.matchedExposure,
1164 subtractRes=subtractRes,
1165 diaSources=diaSources,
1166 selectSources=selectSources
1169 def fitAstrometry(self, templateSources, templateExposure, selectSources):
1170 """Fit the relative astrometry between templateSources and selectSources
1175 Remove this method. It originally fit a new WCS to the template before calling register.run
1176 because our TAN-SIP fitter behaved badly for points far
from CRPIX, but that
's been fixed.
1177 It remains because a subtask overrides it.
1179 results = self.register.run(templateSources, templateExposure.getWcs(),
1180 templateExposure.getBBox(), selectSources)
1183 def runDebug(self, exposure, subtractRes, selectSources, kernelSources, diaSources):
1184 """Make debug plots and displays.
1188 Test and update
for current debug display
and slot names
1198 disp = afwDisplay.getDisplay(frame=lsstDebug.frame)
1199 if not maskTransparency:
1200 maskTransparency = 0
1201 disp.setMaskTransparency(maskTransparency)
1203 if display
and showSubtracted:
1204 disp.mtv(subtractRes.subtractedExposure, title=
"Subtracted image")
1205 mi = subtractRes.subtractedExposure.getMaskedImage()
1206 x0, y0 = mi.getX0(), mi.getY0()
1207 with disp.Buffering():
1208 for s
in diaSources:
1209 x, y = s.getX() - x0, s.getY() - y0
1210 ctype =
"red" if s.get(
"flags_negative")
else "yellow"
1211 if (s.get(
"base_PixelFlags_flag_interpolatedCenter")
1212 or s.get(
"base_PixelFlags_flag_saturatedCenter")
1213 or s.get(
"base_PixelFlags_flag_crCenter")):
1215 elif (s.get(
"base_PixelFlags_flag_interpolated")
1216 or s.get(
"base_PixelFlags_flag_saturated")
1217 or s.get(
"base_PixelFlags_flag_cr")):
1221 disp.dot(ptype, x, y, size=4, ctype=ctype)
1222 lsstDebug.frame += 1
1224 if display
and showPixelResiduals
and selectSources:
1225 nonKernelSources = []
1226 for source
in selectSources:
1227 if source
not in kernelSources:
1228 nonKernelSources.append(source)
1230 diUtils.plotPixelResiduals(exposure,
1231 subtractRes.warpedExposure,
1232 subtractRes.subtractedExposure,
1233 subtractRes.kernelCellSet,
1234 subtractRes.psfMatchingKernel,
1235 subtractRes.backgroundModel,
1237 self.subtract.config.kernel.active.detectionConfig,
1239 diUtils.plotPixelResiduals(exposure,
1240 subtractRes.warpedExposure,
1241 subtractRes.subtractedExposure,
1242 subtractRes.kernelCellSet,
1243 subtractRes.psfMatchingKernel,
1244 subtractRes.backgroundModel,
1246 self.subtract.config.kernel.active.detectionConfig,
1248 if display
and showDiaSources:
1249 flagChecker = SourceFlagChecker(diaSources)
1250 isFlagged = [flagChecker(x)
for x
in diaSources]
1251 isDipole = [x.get(
"ip_diffim_ClassificationDipole_value")
for x
in diaSources]
1252 diUtils.showDiaSources(diaSources, subtractRes.subtractedExposure, isFlagged, isDipole,
1253 frame=lsstDebug.frame)
1254 lsstDebug.frame += 1
1256 if display
and showDipoles:
1257 DipoleAnalysis().displayDipoles(subtractRes.subtractedExposure, diaSources,
1258 frame=lsstDebug.frame)
1259 lsstDebug.frame += 1
1261 def checkTemplateIsSufficient(self, templateExposure):
1262 """Raise NoWorkFound if template coverage < requiredTemplateFraction
1266 templateExposure : `lsst.afw.image.ExposureF`
1267 The template exposure to check
1272 Raised if fraction of good pixels, defined
as not having NO_DATA
1273 set,
is less then the configured requiredTemplateFraction
1277 pixNoData = numpy.count_nonzero(templateExposure.mask.array
1278 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
1279 pixGood = templateExposure.getBBox().getArea() - pixNoData
1280 self.log.info(
"template has %d good pixels (%.1f%%)", pixGood,
1281 100*pixGood/templateExposure.getBBox().getArea())
1283 if pixGood/templateExposure.getBBox().getArea() < self.config.requiredTemplateFraction:
1284 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%) Not attempting subtraction. "
1285 "To force subtraction, set config requiredTemplateFraction=0." % (
1286 100*pixGood/templateExposure.getBBox().getArea(),
1287 100*self.config.requiredTemplateFraction))
1288 raise pipeBase.NoWorkFound(message)
1290 def _getConfigName(self):
1291 """Return the name of the config dataset
1293 return "%sDiff_config" % (self.config.coaddName,)
1295 def _getMetadataName(self):
1296 """Return the name of the metadata dataset
1298 return "%sDiff_metadata" % (self.config.coaddName,)
1300 def getSchemaCatalogs(self):
1301 """Return a dict of empty catalogs for each catalog dataset produced by this task."""
1302 return {self.config.coaddName +
"Diff_diaSrc": self.outputSchema}
1305 def _makeArgumentParser(cls):
1306 """Create an argument parser
1308 parser = pipeBase.ArgumentParser(name=cls._DefaultName)
1309 parser.add_id_argument("--id",
"calexp", help=
"data ID, e.g. --id visit=12345 ccd=1,2")
1310 parser.add_id_argument(
"--templateId",
"calexp", doMakeDataRefList=
True,
1311 help=
"Template data ID in case of calexp template,"
1312 " e.g. --templateId visit=6789")
1317 defaultTemplates={
"coaddName":
"goodSeeing"}
1319 inputTemplate = pipeBase.connectionTypes.Input(
1320 doc=(
"Warped template produced by GetMultiTractCoaddTemplate"),
1321 dimensions=(
"instrument",
"visit",
"detector"),
1322 storageClass=
"ExposureF",
1323 name=
"{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
1326 def __init__(self, *, config=None):
1327 super().__init__(config=config)
1330 if "coaddExposures" in self.inputs:
1331 self.inputs.remove(
"coaddExposures")
1332 if "dcrCoadds" in self.inputs:
1333 self.inputs.remove(
"dcrCoadds")
1337 pipelineConnections=ImageDifferenceFromTemplateConnections):
1342 ConfigClass = ImageDifferenceFromTemplateConfig
1343 _DefaultName =
"imageDifference"
1345 @lsst.utils.inheritDoc(pipeBase.PipelineTask)
1347 inputs = butlerQC.get(inputRefs)
1348 self.log.info(
"Processing %s", butlerQC.quantum.dataId)
1349 self.checkTemplateIsSufficient(inputs[
'inputTemplate'])
1350 expId, expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
1352 idFactory = self.makeIdFactory(expId=expId, expBits=expBits)
1354 outputs = self.run(exposure=inputs[
'exposure'],
1355 templateExposure=inputs[
'inputTemplate'],
1356 idFactory=idFactory)
1359 if outputs.diaSources
is None:
1360 del outputs.diaSources
1361 butlerQC.put(outputs, outputRefs)
1365 winter2013WcsShift = pexConfig.Field(dtype=float, default=0.0,
1366 doc=
"Shift stars going into RegisterTask by this amount")
1367 winter2013WcsRms = pexConfig.Field(dtype=float, default=0.0,
1368 doc=
"Perturb stars going into RegisterTask by this amount")
1371 ImageDifferenceConfig.setDefaults(self)
1372 self.getTemplate.retarget(GetCalexpAsTemplateTask)
1376 """!Image difference Task used in the Winter 2013 data challege.
1377 Enables testing the effects of registration shifts and scatter.
1379 For use
with winter 2013 simulated images:
1380 Use --templateId visit=88868666
for sparse data
1381 --templateId visit=22222200
for dense data (g)
1382 --templateId visit=11111100
for dense data (i)
1384 ConfigClass = Winter2013ImageDifferenceConfig
1385 _DefaultName = "winter2013ImageDifference"
1388 ImageDifferenceTask.__init__(self, **kwargs)
1391 """Fit the relative astrometry between templateSources and selectSources"""
1392 if self.config.winter2013WcsShift > 0.0:
1394 self.config.winter2013WcsShift)
1395 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1396 for source
in templateSources:
1397 centroid = source.get(cKey)
1398 source.set(cKey, centroid + offset)
1399 elif self.config.winter2013WcsRms > 0.0:
1400 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1401 for source
in templateSources:
1402 offset =
geom.Extent2D(self.config.winter2013WcsRms*numpy.random.normal(),
1403 self.config.winter2013WcsRms*numpy.random.normal())
1404 centroid = source.get(cKey)
1405 source.set(cKey, centroid + offset)
1407 results = self.register.run(templateSources, templateExposure.getWcs(),
1408 templateExposure.getBBox(), selectSources)
def runQuantum(self, butlerQC, inputRefs, outputRefs)
Image difference Task used in the Winter 2013 data challege.
def __init__(self, **kwargs)
def fitAstrometry(self, templateSources, templateExposure, selectSources)