34from lsst.meas.algorithms import (SourceDetectionTask, SingleGaussianPsf, ObjectSizeStarSelectorTask,
35 LoadIndexedReferenceObjectsTask, SkyObjectsTask,
40from lsst.ip.diffim import (DipoleAnalysis, SourceFlagChecker, KernelCandidateF, makeKernelBasisList,
41 KernelCandidateQa, DiaCatalogSourceSelectorTask, DiaCatalogSourceSelectorConfig,
42 GetCoaddAsTemplateTask, GetCalexpAsTemplateTask, DipoleFitTask,
43 DecorrelateALKernelSpatialTask, subtractAlgorithmRegistry)
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 finalizedPsfApCorrCatalog = pipeBase.connectionTypes.Input(
101 doc=(
"Per-visit finalized psf models and aperture correction maps. "
102 "These catalogs use the detector id for the catalog id, "
103 "sorted on id for fast lookup."),
104 name=
"finalized_psf_ap_corr_catalog",
105 storageClass=
"ExposureCatalog",
106 dimensions=(
"instrument",
"visit"),
108 outputSchema = pipeBase.connectionTypes.InitOutput(
109 doc=
"Schema (as an example catalog) for output DIASource catalog.",
110 storageClass=
"SourceCatalog",
111 name=
"{fakesType}{coaddName}Diff_diaSrc_schema",
113 subtractedExposure = pipeBase.connectionTypes.Output(
114 doc=
"Output AL difference or Zogy proper difference image",
115 dimensions=(
"instrument",
"visit",
"detector"),
116 storageClass=
"ExposureF",
117 name=
"{fakesType}{coaddName}Diff_differenceExp",
119 scoreExposure = pipeBase.connectionTypes.Output(
120 doc=
"Output AL likelihood or Zogy score image",
121 dimensions=(
"instrument",
"visit",
"detector"),
122 storageClass=
"ExposureF",
123 name=
"{fakesType}{coaddName}Diff_scoreExp",
125 warpedExposure = pipeBase.connectionTypes.Output(
126 doc=
"Warped template used to create `subtractedExposure`.",
127 dimensions=(
"instrument",
"visit",
"detector"),
128 storageClass=
"ExposureF",
129 name=
"{fakesType}{coaddName}Diff_warpedExp",
131 matchedExposure = pipeBase.connectionTypes.Output(
132 doc=
"Warped template used to create `subtractedExposure`.",
133 dimensions=(
"instrument",
"visit",
"detector"),
134 storageClass=
"ExposureF",
135 name=
"{fakesType}{coaddName}Diff_matchedExp",
137 diaSources = pipeBase.connectionTypes.Output(
138 doc=
"Output detected diaSources on the difference image",
139 dimensions=(
"instrument",
"visit",
"detector"),
140 storageClass=
"SourceCatalog",
141 name=
"{fakesType}{coaddName}Diff_diaSrc",
144 def __init__(self, *, config=None):
145 super().__init__(config=config)
146 if config.coaddName ==
'dcr':
147 self.inputs.remove(
"coaddExposures")
149 self.inputs.remove(
"dcrCoadds")
150 if not config.doWriteSubtractedExp:
151 self.outputs.remove(
"subtractedExposure")
152 if not config.doWriteScoreExp:
153 self.outputs.remove(
"scoreExposure")
154 if not config.doWriteWarpedExp:
155 self.outputs.remove(
"warpedExposure")
156 if not config.doWriteMatchedExp:
157 self.outputs.remove(
"matchedExposure")
158 if not config.doWriteSources:
159 self.outputs.remove(
"diaSources")
160 if not config.doApplyFinalizedPsf:
161 self.inputs.remove(
"finalizedPsfApCorrCatalog")
167class ImageDifferenceConfig(pipeBase.PipelineTaskConfig,
168 pipelineConnections=ImageDifferenceTaskConnections):
169 """Config for ImageDifferenceTask
171 doAddCalexpBackground = pexConfig.Field(dtype=bool, default=False,
172 doc=
"Add background to calexp before processing it. "
173 "Useful as ipDiffim does background matching.")
174 doUseRegister = pexConfig.Field(dtype=bool, default=
False,
175 doc=
"Re-compute astrometry on the template. "
176 "Use image-to-image registration to align template with "
177 "science image (AL only).")
178 doDebugRegister = pexConfig.Field(dtype=bool, default=
False,
179 doc=
"Writing debugging data for doUseRegister")
180 doSelectSources = pexConfig.Field(dtype=bool, default=
False,
181 doc=
"Select stars to use for kernel fitting (AL only)")
182 doSelectDcrCatalog = pexConfig.Field(dtype=bool, default=
False,
183 doc=
"Select stars of extreme color as part "
184 "of the control sample (AL only)")
185 doSelectVariableCatalog = pexConfig.Field(dtype=bool, default=
False,
186 doc=
"Select stars that are variable to be part "
187 "of the control sample (AL only)")
188 doSubtract = pexConfig.Field(dtype=bool, default=
True, doc=
"Compute subtracted exposure?")
189 doPreConvolve = pexConfig.Field(dtype=bool, default=
False,
190 doc=
"Not in use. Superseded by useScoreImageDetection.",
191 deprecated=
"This option superseded by useScoreImageDetection."
192 " Will be removed after v22.")
193 useScoreImageDetection = pexConfig.Field(
194 dtype=bool, default=
False, doc=
"Calculate the pre-convolved AL likelihood or "
195 "the Zogy score image. Use it for source detection (if doDetection=True).")
196 doWriteScoreExp = pexConfig.Field(
197 dtype=bool, default=
False, doc=
"Write AL likelihood or Zogy score exposure?")
198 doScaleTemplateVariance = pexConfig.Field(dtype=bool, default=
False,
199 doc=
"Scale variance of the template before PSF matching")
200 doScaleDiffimVariance = pexConfig.Field(dtype=bool, default=
True,
201 doc=
"Scale variance of the diffim before PSF matching. "
202 "You may do either this or template variance scaling, "
203 "or neither. (Doing both is a waste of CPU.)")
204 useGaussianForPreConvolution = pexConfig.Field(
205 dtype=bool, default=
False, doc=
"Use a simple gaussian PSF model for pre-convolution "
206 "(oherwise use exposure PSF)? (AL and if useScoreImageDetection=True only)")
207 doDetection = pexConfig.Field(dtype=bool, default=
True, doc=
"Detect sources?")
208 doDecorrelation = pexConfig.Field(dtype=bool, default=
True,
209 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L "
210 "kernel convolution (AL only)? If True, also update the diffim PSF.")
211 doMerge = pexConfig.Field(dtype=bool, default=
True,
212 doc=
"Merge positive and negative diaSources with grow radius "
213 "set by growFootprint")
214 doMatchSources = pexConfig.Field(dtype=bool, default=
False,
215 doc=
"Match diaSources with input calexp sources and ref catalog sources")
216 doMeasurement = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure diaSources?")
217 doDipoleFitting = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure dipoles using new algorithm?")
218 doForcedMeasurement = pexConfig.Field(
221 doc=
"Force photometer diaSource locations on PVI?")
222 doWriteSubtractedExp = pexConfig.Field(
223 dtype=bool, default=
True, doc=
"Write difference exposure (AL and Zogy) ?")
224 doWriteWarpedExp = pexConfig.Field(
225 dtype=bool, default=
False, doc=
"Write WCS, warped template coadd exposure?")
226 doWriteMatchedExp = pexConfig.Field(dtype=bool, default=
False,
227 doc=
"Write warped and PSF-matched template coadd exposure?")
228 doWriteSources = pexConfig.Field(dtype=bool, default=
True, doc=
"Write sources?")
229 doAddMetrics = pexConfig.Field(dtype=bool, default=
False,
230 doc=
"Add columns to the source table to hold analysis metrics?")
231 doApplyFinalizedPsf = pexConfig.Field(
232 doc=
"Whether to apply finalized psf models and aperture correction map.",
237 coaddName = pexConfig.Field(
238 doc=
"coadd name: typically one of deep, goodSeeing, or dcr",
242 convolveTemplate = pexConfig.Field(
243 doc=
"Which image gets convolved (default = template)",
247 refObjLoader = pexConfig.ConfigurableField(
248 target=LoadIndexedReferenceObjectsTask,
249 doc=
"reference object loader",
251 astrometer = pexConfig.ConfigurableField(
252 target=AstrometryTask,
253 doc=
"astrometry task; used to match sources to reference objects, but not to fit a WCS",
255 sourceSelector = pexConfig.ConfigurableField(
256 target=ObjectSizeStarSelectorTask,
257 doc=
"Source selection algorithm",
259 subtract = subtractAlgorithmRegistry.makeField(
"Subtraction Algorithm", default=
"al")
260 decorrelate = pexConfig.ConfigurableField(
261 target=DecorrelateALKernelSpatialTask,
262 doc=
"Decorrelate effects of A&L kernel convolution on image difference, only if doSubtract is True. "
263 "If this option is enabled, then detection.thresholdValue should be set to 5.0 (rather than the "
267 doSpatiallyVarying = pexConfig.Field(
270 doc=
"Perform A&L decorrelation on a grid across the "
271 "image in order to allow for spatial variations. Zogy does not use this option."
273 detection = pexConfig.ConfigurableField(
274 target=SourceDetectionTask,
275 doc=
"Low-threshold detection for final measurement",
277 measurement = pexConfig.ConfigurableField(
278 target=DipoleFitTask,
279 doc=
"Enable updated dipole fitting method",
284 doc=
"Run subtask to apply aperture corrections"
287 target=ApplyApCorrTask,
288 doc=
"Subtask to apply aperture corrections"
290 forcedMeasurement = pexConfig.ConfigurableField(
291 target=ForcedMeasurementTask,
292 doc=
"Subtask to force photometer PVI at diaSource location.",
294 getTemplate = pexConfig.ConfigurableField(
295 target=GetCoaddAsTemplateTask,
296 doc=
"Subtask to retrieve template exposure and sources",
298 scaleVariance = pexConfig.ConfigurableField(
299 target=ScaleVarianceTask,
300 doc=
"Subtask to rescale the variance of the template "
301 "to the statistically expected level"
303 controlStepSize = pexConfig.Field(
304 doc=
"What step size (every Nth one) to select a control sample from the kernelSources",
308 controlRandomSeed = pexConfig.Field(
309 doc=
"Random seed for shuffing the control sample",
313 register = pexConfig.ConfigurableField(
315 doc=
"Task to enable image-to-image image registration (warping)",
317 kernelSourcesFromRef = pexConfig.Field(
318 doc=
"Select sources to measure kernel from reference catalog if True, template if false",
322 templateSipOrder = pexConfig.Field(
323 dtype=int, default=2,
324 doc=
"Sip Order for fitting the Template Wcs (default is too high, overfitting)"
326 growFootprint = pexConfig.Field(
327 dtype=int, default=2,
328 doc=
"Grow positive and negative footprints by this amount before merging"
330 diaSourceMatchRadius = pexConfig.Field(
331 dtype=float, default=0.5,
332 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
334 requiredTemplateFraction = pexConfig.Field(
335 dtype=float, default=0.1,
336 doc=
"Do not attempt to run task if template covers less than this fraction of pixels."
337 "Setting to 0 will always attempt image subtraction"
339 doSkySources = pexConfig.Field(
342 doc=
"Generate sky sources?",
344 skySources = pexConfig.ConfigurableField(
345 target=SkyObjectsTask,
346 doc=
"Generate sky sources",
352 self.subtract[
'al'].kernel.name =
"AL"
353 self.subtract[
'al'].kernel.active.fitForBackground =
True
354 self.subtract[
'al'].kernel.active.spatialKernelOrder = 1
355 self.subtract[
'al'].kernel.active.spatialBgOrder = 2
358 self.detection.thresholdPolarity =
"both"
359 self.detection.thresholdValue = 5.0
360 self.detection.reEstimateBackground =
False
361 self.detection.thresholdType =
"pixel_stdev"
367 self.measurement.algorithms.names.add(
'base_PeakLikelihoodFlux')
368 self.measurement.plugins.names |= [
'ext_trailedSources_Naive',
369 'base_LocalPhotoCalib',
372 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
373 self.forcedMeasurement.copyColumns = {
374 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
375 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
376 self.forcedMeasurement.slots.shape =
None
379 random.seed(self.controlRandomSeed)
382 pexConfig.Config.validate(self)
383 if not self.doSubtract
and not self.doDetection:
384 raise ValueError(
"Either doSubtract or doDetection must be enabled.")
385 if self.doMeasurement
and not self.doDetection:
386 raise ValueError(
"Cannot run source measurement without source detection.")
387 if self.doMerge
and not self.doDetection:
388 raise ValueError(
"Cannot run source merging without source detection.")
389 if self.doSkySources
and not self.doDetection:
390 raise ValueError(
"Cannot run sky source creation without source detection.")
391 if self.doUseRegister
and not self.doSelectSources:
392 raise ValueError(
"doUseRegister=True and doSelectSources=False. "
393 "Cannot run RegisterTask without selecting sources.")
394 if self.doScaleDiffimVariance
and self.doScaleTemplateVariance:
395 raise ValueError(
"Scaling the diffim variance and scaling the template variance "
396 "are both set. Please choose one or the other.")
398 if self.subtract.name ==
'zogy':
399 if self.doWriteMatchedExp:
400 raise ValueError(
"doWriteMatchedExp=True Matched exposure is not "
401 "calculated in zogy subtraction.")
402 if self.doAddMetrics:
403 raise ValueError(
"doAddMetrics=True Kernel metrics does not exist in zogy subtraction.")
404 if self.doDecorrelation:
406 "doDecorrelation=True The decorrelation afterburner does not exist in zogy subtraction.")
407 if self.doSelectSources:
409 "doSelectSources=True Selecting sources for PSF matching is not a zogy option.")
410 if self.useGaussianForPreConvolution:
412 "useGaussianForPreConvolution=True This is an AL subtraction only option.")
415 if self.useScoreImageDetection
and not self.convolveTemplate:
417 "convolveTemplate=False and useScoreImageDetection=True "
418 "Pre-convolution and matching of the science image is not a supported operation.")
419 if self.doWriteSubtractedExp
and self.useScoreImageDetection:
421 "doWriteSubtractedExp=True and useScoreImageDetection=True "
422 "Regular difference image is not calculated. "
423 "AL subtraction calculates either the regular difference image or the score image.")
424 if self.doWriteScoreExp
and not self.useScoreImageDetection:
426 "doWriteScoreExp=True and useScoreImageDetection=False "
427 "Score image is not calculated. "
428 "AL subtraction calculates either the regular difference image or the score image.")
429 if self.doAddMetrics
and not self.doSubtract:
430 raise ValueError(
"Subtraction must be enabled for kernel metrics calculation.")
431 if self.useGaussianForPreConvolution
and not self.useScoreImageDetection:
433 "useGaussianForPreConvolution=True and useScoreImageDetection=False "
434 "Gaussian PSF approximation exists only for AL subtraction w/ pre-convolution.")
437class ImageDifferenceTaskRunner(pipeBase.ButlerInitializedTaskRunner):
440 def getTargetList(parsedCmd, **kwargs):
441 return pipeBase.TaskRunner.getTargetList(parsedCmd, templateIdList=parsedCmd.templateId.idList,
445class ImageDifferenceTask(pipeBase.CmdLineTask, pipeBase.PipelineTask):
446 """Subtract an image from a template and measure the result
448 ConfigClass = ImageDifferenceConfig
449 RunnerClass = ImageDifferenceTaskRunner
450 _DefaultName = "imageDifference"
452 def __init__(self, butler=None, **kwargs):
453 """!Construct an ImageDifference Task
455 @param[
in] butler Butler object to use
in constructing reference object loaders
457 super().__init__(**kwargs)
458 self.makeSubtask("getTemplate")
460 self.makeSubtask(
"subtract")
462 if self.config.subtract.name ==
'al' and self.config.doDecorrelation:
463 self.makeSubtask(
"decorrelate")
465 if self.config.doScaleTemplateVariance
or self.config.doScaleDiffimVariance:
466 self.makeSubtask(
"scaleVariance")
468 if self.config.doUseRegister:
469 self.makeSubtask(
"register")
470 self.schema = afwTable.SourceTable.makeMinimalSchema()
472 if self.config.doSelectSources:
473 self.makeSubtask(
"sourceSelector")
474 if self.config.kernelSourcesFromRef:
475 self.makeSubtask(
'refObjLoader', butler=butler)
476 self.makeSubtask(
"astrometer", refObjLoader=self.refObjLoader)
479 if self.config.doDetection:
480 self.makeSubtask(
"detection", schema=self.schema)
481 if self.config.doMeasurement:
482 self.makeSubtask(
"measurement", schema=self.schema,
483 algMetadata=self.algMetadata)
484 if self.config.doApCorr:
485 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
486 if self.config.doForcedMeasurement:
487 self.schema.addField(
488 "ip_diffim_forced_PsfFlux_instFlux",
"D",
489 "Forced PSF flux measured on the direct image.",
491 self.schema.addField(
492 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
493 "Forced PSF flux error measured on the direct image.",
495 self.schema.addField(
496 "ip_diffim_forced_PsfFlux_area",
"F",
497 "Forced PSF flux effective area of PSF.",
499 self.schema.addField(
500 "ip_diffim_forced_PsfFlux_flag",
"Flag",
501 "Forced PSF flux general failure flag.")
502 self.schema.addField(
503 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
504 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
505 self.schema.addField(
506 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
507 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
508 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
509 if self.config.doMatchSources:
510 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
511 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
512 if self.config.doSkySources:
513 self.makeSubtask(
"skySources")
514 self.skySourceKey = self.schema.addField(
"sky_source", type=
"Flag", doc=
"Sky objects.")
518 self.outputSchema.getTable().setMetadata(self.algMetadata)
521 def makeIdFactory(expId, expBits):
522 """Create IdFactory instance for unique 64 bit diaSource id-s.
530 Number of used bits in ``expId``.
534 The diasource id-s consists of the ``expId`` stored fixed
in the highest value
535 ``expBits`` of the 64-bit integer plus (bitwise
or) a generated sequence number
in the
536 low value end of the integer.
542 return ExposureIdInfo(expId, expBits).makeSourceIdFactory()
544 @lsst.utils.inheritDoc(pipeBase.PipelineTask)
545 def runQuantum(self, butlerQC: pipeBase.ButlerQuantumContext,
546 inputRefs: pipeBase.InputQuantizedConnection,
547 outputRefs: pipeBase.OutputQuantizedConnection):
548 inputs = butlerQC.get(inputRefs)
549 self.log.
info(
"Processing %s", butlerQC.quantum.dataId)
551 finalizedPsfApCorrCatalog = inputs.get(
"finalizedPsfApCorrCatalog",
None)
552 exposure = self.prepareCalibratedExposure(
554 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog
557 expId, expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
559 idFactory = self.makeIdFactory(expId=expId, expBits=expBits)
560 if self.config.coaddName ==
'dcr':
561 templateExposures = inputRefs.dcrCoadds
563 templateExposures = inputRefs.coaddExposures
564 templateStruct = self.getTemplate.runQuantum(
565 exposure, butlerQC, inputRefs.skyMap, templateExposures
568 self.checkTemplateIsSufficient(templateStruct.exposure)
570 outputs = self.run(exposure=exposure,
571 templateExposure=templateStruct.exposure,
574 if outputs.diaSources
is None:
575 del outputs.diaSources
576 butlerQC.put(outputs, outputRefs)
579 def runDataRef(self, sensorRef, templateIdList=None):
580 """Subtract an image from a template coadd and measure the result.
582 Data I/O wrapper around `run` using the butler in Gen2.
587 Sensor-level butler data reference, used
for the following data products:
594 - self.config.coaddName +
"Coadd_skyMap"
595 - self.config.coaddName +
"Coadd"
596 Input
or output, depending on config:
597 - self.config.coaddName +
"Diff_subtractedExp"
598 Output, depending on config:
599 - self.config.coaddName +
"Diff_matchedExp"
600 - self.config.coaddName +
"Diff_src"
604 results : `lsst.pipe.base.Struct`
605 Returns the Struct by `run`.
607 subtractedExposureName = self.config.coaddName + "Diff_differenceExp"
608 subtractedExposure =
None
610 calexpBackgroundExposure =
None
611 self.log.
info(
"Processing %s", sensorRef.dataId)
616 idFactory = self.makeIdFactory(expId=
int(sensorRef.get(
"ccdExposureId")),
617 expBits=sensorRef.get(
"ccdExposureId_bits"))
618 if self.config.doAddCalexpBackground:
619 calexpBackgroundExposure = sensorRef.get(
"calexpBackground")
622 exposure = sensorRef.get(
"calexp", immediate=
True)
625 template = self.getTemplate.runDataRef(exposure, sensorRef, templateIdList=templateIdList)
627 if sensorRef.datasetExists(
"src"):
628 self.log.
info(
"Source selection via src product")
630 selectSources = sensorRef.get(
"src")
632 if not self.config.doSubtract
and self.config.doDetection:
634 subtractedExposure = sensorRef.get(subtractedExposureName)
637 results = self.run(exposure=exposure,
638 selectSources=selectSources,
639 templateExposure=template.exposure,
640 templateSources=template.sources,
642 calexpBackgroundExposure=calexpBackgroundExposure,
643 subtractedExposure=subtractedExposure)
645 if self.config.doWriteSources
and results.diaSources
is not None:
646 sensorRef.put(results.diaSources, self.config.coaddName +
"Diff_diaSrc")
647 if self.config.doWriteWarpedExp:
648 sensorRef.put(results.warpedExposure, self.config.coaddName +
"Diff_warpedExp")
649 if self.config.doWriteMatchedExp:
650 sensorRef.put(results.matchedExposure, self.config.coaddName +
"Diff_matchedExp")
651 if self.config.doAddMetrics
and self.config.doSelectSources:
652 sensorRef.put(results.selectSources, self.config.coaddName +
"Diff_kernelSrc")
653 if self.config.doWriteSubtractedExp:
654 sensorRef.put(results.subtractedExposure, subtractedExposureName)
655 if self.config.doWriteScoreExp:
656 sensorRef.put(results.scoreExposure, self.config.coaddName +
"Diff_scoreExp")
659 def prepareCalibratedExposure(self, exposure, finalizedPsfApCorrCatalog=None):
660 """Prepare a calibrated exposure and apply finalized psf if so configured.
665 Input exposure to adjust calibrations.
667 Exposure catalog with finalized psf models
and aperture correction
668 maps to be applied
if config.doApplyFinalizedPsf=
True. Catalog uses
669 the detector id
for the catalog id, sorted on id
for fast lookup.
674 Exposure
with adjusted calibrations.
676 detectorId = exposure.getInfo().getDetector().getId()
678 if finalizedPsfApCorrCatalog
is not None:
679 row = finalizedPsfApCorrCatalog.find(detectorId)
681 self.log.
warning(
"Detector id %s not found in finalizedPsfApCorrCatalog; "
682 "Using original psf.", detectorId)
685 apCorrMap = row.getApCorrMap()
686 if psf
is None or apCorrMap
is None:
687 self.log.
warning(
"Detector id %s has None for psf/apCorrMap in "
688 "finalizedPsfApCorrCatalog; Using original psf.", detectorId)
691 exposure.info.setApCorrMap(apCorrMap)
696 def run(self, exposure=None, selectSources=None, templateExposure=None, templateSources=None,
697 idFactory=None, calexpBackgroundExposure=None, subtractedExposure=None):
698 """PSF matches, subtract two images and perform detection on the difference image.
702 exposure : `lsst.afw.image.ExposureF`, optional
703 The science exposure, the minuend in the image subtraction.
704 Can be
None only
if ``config.doSubtract==
False``.
706 Identified sources on the science exposure. This catalog
is used to
707 select sources
in order to perform the AL PSF matching on stamp images
708 around them. The selection steps depend on config options
and whether
709 ``templateSources``
and ``matchingSources`` specified.
710 templateExposure : `lsst.afw.image.ExposureF`, optional
711 The template to be subtracted
from ``exposure``
in the image subtraction.
712 ``templateExposure``
is modified
in place
if ``config.doScaleTemplateVariance==
True``.
713 The template exposure should cover the same sky area
as the science exposure.
714 It
is either a stich of patches of a coadd skymap image
or a calexp
715 of the same pointing
as the science exposure. Can be
None only
716 if ``config.doSubtract==
False``
and ``subtractedExposure``
is not None.
718 Identified sources on the template exposure.
720 Generator object to assign ids to detected sources
in the difference image.
721 calexpBackgroundExposure : `lsst.afw.image.ExposureF`, optional
722 Background exposure to be added back to the science exposure
723 if ``config.doAddCalexpBackground==
True``
724 subtractedExposure : `lsst.afw.image.ExposureF`, optional
725 If ``config.doSubtract==
False``
and ``config.doDetection==
True``,
726 performs the post subtraction source detection only on this exposure.
727 Otherwise should be
None.
731 results : `lsst.pipe.base.Struct`
732 ``subtractedExposure`` : `lsst.afw.image.ExposureF`
734 ``scoreExposure`` : `lsst.afw.image.ExposureF`
or `
None`
735 The zogy score exposure,
if calculated.
736 ``matchedExposure`` : `lsst.afw.image.ExposureF`
737 The matched PSF exposure.
738 ``subtractRes`` : `lsst.pipe.base.Struct`
739 The returned result structure of the ImagePsfMatchTask subtask.
741 The catalog of detected sources.
743 The input source catalog
with optionally added Qa information.
747 The following major steps are included:
749 - warp template coadd to match WCS of image
750 - PSF match image to warped template
751 - subtract image
from PSF-matched, warped template
755 For details about the image subtraction configuration modes
759 controlSources =
None
760 subtractedExposure =
None
765 exposureOrig = exposure
767 if self.config.doAddCalexpBackground:
768 mi = exposure.getMaskedImage()
769 mi += calexpBackgroundExposure.getImage()
771 if not exposure.hasPsf():
772 raise pipeBase.TaskError(
"Exposure has no psf")
773 sciencePsf = exposure.getPsf()
775 if self.config.doSubtract:
776 if self.config.doScaleTemplateVariance:
777 self.log.
info(
"Rescaling template variance")
778 templateVarFactor = self.scaleVariance.
run(
779 templateExposure.getMaskedImage())
780 self.log.
info(
"Template variance scaling factor: %.2f", templateVarFactor)
781 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
782 self.metadata.add(
"psfMatchingAlgorithm", self.config.subtract.name)
784 if self.config.subtract.name ==
'zogy':
785 subtractRes = self.subtract.
run(exposure, templateExposure, doWarping=
True)
786 scoreExposure = subtractRes.scoreExp
787 subtractedExposure = subtractRes.diffExp
788 subtractRes.subtractedExposure = subtractedExposure
789 subtractRes.matchedExposure =
None
791 elif self.config.subtract.name ==
'al':
794 sciAvgPos = sciencePsf.getAveragePosition()
795 scienceSigmaOrig = sciencePsf.computeShape(sciAvgPos).getDeterminantRadius()
797 templatePsf = templateExposure.getPsf()
798 templateAvgPos = templatePsf.getAveragePosition()
799 templateSigma = templatePsf.computeShape(templateAvgPos).getDeterminantRadius()
807 if self.config.useScoreImageDetection:
808 self.log.
warning(
"AL likelihood image: pre-convolution of PSF is not implemented.")
811 srcMI = exposure.maskedImage
812 exposure = exposure.clone()
814 if self.config.useGaussianForPreConvolution:
816 "AL likelihood image: Using Gaussian (sigma=%.2f) PSF estimation "
817 "for science image pre-convolution", scienceSigmaOrig)
819 kWidth, kHeight = sciencePsf.getLocalKernel().getDimensions()
824 "AL likelihood image: Using the science image PSF for pre-convolution.")
826 afwMath.convolve(exposure.maskedImage, srcMI, preConvPsf.getLocalKernel(), convControl)
827 scienceSigmaPost = scienceSigmaOrig*math.sqrt(2)
829 scienceSigmaPost = scienceSigmaOrig
834 if self.config.doSelectSources:
835 if selectSources
is None:
836 self.log.
warning(
"Src product does not exist; running detection, measurement,"
839 selectSources = self.subtract.getSelectSources(
841 sigma=scienceSigmaPost,
842 doSmooth=
not self.config.useScoreImageDetection,
846 if self.config.doAddMetrics:
850 referenceFwhmPix=scienceSigmaPost*FwhmPerSigma,
851 targetFwhmPix=templateSigma*FwhmPerSigma))
859 selectSources = kcQa.addToSchema(selectSources)
860 if self.config.kernelSourcesFromRef:
862 astromRet = self.astrometer.loadAndMatch(exposure=exposure, sourceCat=selectSources)
863 matches = astromRet.matches
864 elif templateSources:
867 mc.findOnlyClosest =
False
871 raise RuntimeError(
"doSelectSources=True and kernelSourcesFromRef=False,"
872 "but template sources not available. Cannot match science "
873 "sources with template sources. Run process* on data from "
874 "which templates are built.")
876 kernelSources = self.sourceSelector.
run(selectSources, exposure=exposure,
877 matches=matches).sourceCat
878 random.shuffle(kernelSources, random.random)
879 controlSources = kernelSources[::self.config.controlStepSize]
880 kernelSources = [k
for i, k
in enumerate(kernelSources)
881 if i % self.config.controlStepSize]
883 if self.config.doSelectDcrCatalog:
887 redSources = redSelector.selectStars(exposure, selectSources, matches=matches).starCat
888 controlSources.extend(redSources)
892 grMax=self.sourceSelector.config.grMin))
893 blueSources = blueSelector.selectStars(exposure, selectSources,
894 matches=matches).starCat
895 controlSources.extend(blueSources)
897 if self.config.doSelectVariableCatalog:
900 varSources = varSelector.selectStars(exposure, selectSources, matches=matches).starCat
901 controlSources.extend(varSources)
903 self.log.
info(
"Selected %d / %d sources for Psf matching (%d for control sample)",
904 len(kernelSources), len(selectSources), len(controlSources))
908 if self.config.doUseRegister:
909 self.log.
info(
"Registering images")
911 if templateSources
is None:
915 templateSources = self.subtract.getSelectSources(
924 wcsResults = self.fitAstrometry(templateSources, templateExposure, selectSources)
925 warpedExp = self.register.
warpExposure(templateExposure, wcsResults.wcs,
926 exposure.getWcs(), exposure.getBBox())
927 templateExposure = warpedExp
932 if self.config.doDebugRegister:
934 srcToMatch = {x.second.getId(): x.first
for x
in matches}
936 refCoordKey = wcsResults.matches[0].first.getTable().getCoordKey()
937 inCentroidKey = wcsResults.matches[0].second.getTable().getCentroidSlot().getMeasKey()
938 sids = [m.first.getId()
for m
in wcsResults.matches]
939 positions = [m.first.get(refCoordKey)
for m
in wcsResults.matches]
940 residuals = [m.first.get(refCoordKey).getOffsetFrom(wcsResults.wcs.pixelToSky(
941 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
942 allresids = dict(zip(sids, zip(positions, residuals)))
944 cresiduals = [m.first.get(refCoordKey).getTangentPlaneOffset(
945 wcsResults.wcs.pixelToSky(
946 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
947 colors = numpy.array([-2.5*numpy.log10(srcToMatch[x].get(
"g"))
948 + 2.5*numpy.log10(srcToMatch[x].get(
"r"))
949 for x
in sids
if x
in srcToMatch.keys()])
950 dlong = numpy.array([r[0].asArcseconds()
for s, r
in zip(sids, cresiduals)
951 if s
in srcToMatch.keys()])
952 dlat = numpy.array([r[1].asArcseconds()
for s, r
in zip(sids, cresiduals)
953 if s
in srcToMatch.keys()])
954 idx1 = numpy.where(colors < self.sourceSelector.config.grMin)
955 idx2 = numpy.where((colors >= self.sourceSelector.config.grMin)
956 & (colors <= self.sourceSelector.config.grMax))
957 idx3 = numpy.where(colors > self.sourceSelector.config.grMax)
958 rms1Long = IqrToSigma*(
959 (numpy.percentile(dlong[idx1], 75) - numpy.percentile(dlong[idx1], 25)))
960 rms1Lat = IqrToSigma*(numpy.percentile(dlat[idx1], 75)
961 - numpy.percentile(dlat[idx1], 25))
962 rms2Long = IqrToSigma*(
963 (numpy.percentile(dlong[idx2], 75) - numpy.percentile(dlong[idx2], 25)))
964 rms2Lat = IqrToSigma*(numpy.percentile(dlat[idx2], 75)
965 - numpy.percentile(dlat[idx2], 25))
966 rms3Long = IqrToSigma*(
967 (numpy.percentile(dlong[idx3], 75) - numpy.percentile(dlong[idx3], 25)))
968 rms3Lat = IqrToSigma*(numpy.percentile(dlat[idx3], 75)
969 - numpy.percentile(dlat[idx3], 25))
970 self.log.
info(
"Blue star offsets'': %.3f %.3f, %.3f %.3f",
971 numpy.median(dlong[idx1]), rms1Long,
972 numpy.median(dlat[idx1]), rms1Lat)
973 self.log.
info(
"Green star offsets'': %.3f %.3f, %.3f %.3f",
974 numpy.median(dlong[idx2]), rms2Long,
975 numpy.median(dlat[idx2]), rms2Lat)
976 self.log.
info(
"Red star offsets'': %.3f %.3f, %.3f %.3f",
977 numpy.median(dlong[idx3]), rms3Long,
978 numpy.median(dlat[idx3]), rms3Lat)
980 self.metadata.add(
"RegisterBlueLongOffsetMedian", numpy.median(dlong[idx1]))
981 self.metadata.add(
"RegisterGreenLongOffsetMedian", numpy.median(dlong[idx2]))
982 self.metadata.add(
"RegisterRedLongOffsetMedian", numpy.median(dlong[idx3]))
983 self.metadata.add(
"RegisterBlueLongOffsetStd", rms1Long)
984 self.metadata.add(
"RegisterGreenLongOffsetStd", rms2Long)
985 self.metadata.add(
"RegisterRedLongOffsetStd", rms3Long)
987 self.metadata.add(
"RegisterBlueLatOffsetMedian", numpy.median(dlat[idx1]))
988 self.metadata.add(
"RegisterGreenLatOffsetMedian", numpy.median(dlat[idx2]))
989 self.metadata.add(
"RegisterRedLatOffsetMedian", numpy.median(dlat[idx3]))
990 self.metadata.add(
"RegisterBlueLatOffsetStd", rms1Lat)
991 self.metadata.add(
"RegisterGreenLatOffsetStd", rms2Lat)
992 self.metadata.add(
"RegisterRedLatOffsetStd", rms3Lat)
999 self.log.
info(
"Subtracting images")
1000 subtractRes = self.subtract.subtractExposures(
1001 templateExposure=templateExposure,
1002 scienceExposure=exposure,
1003 candidateList=kernelSources,
1004 convolveTemplate=self.config.convolveTemplate,
1005 doWarping=
not self.config.doUseRegister
1007 if self.config.useScoreImageDetection:
1008 scoreExposure = subtractRes.subtractedExposure
1010 subtractedExposure = subtractRes.subtractedExposure
1012 if self.config.doDetection:
1013 self.log.
info(
"Computing diffim PSF")
1016 if subtractedExposure
is not None and not subtractedExposure.hasPsf():
1017 if self.config.convolveTemplate:
1018 subtractedExposure.setPsf(exposure.getPsf())
1020 subtractedExposure.setPsf(templateExposure.getPsf())
1027 if self.config.doDecorrelation
and self.config.doSubtract:
1028 preConvKernel =
None
1029 if self.config.useGaussianForPreConvolution:
1030 preConvKernel = preConvPsf.getLocalKernel()
1031 if self.config.useScoreImageDetection:
1032 scoreExposure = self.decorrelate.
run(exposureOrig, subtractRes.warpedExposure,
1034 subtractRes.psfMatchingKernel,
1035 spatiallyVarying=self.config.doSpatiallyVarying,
1036 preConvKernel=preConvKernel,
1037 templateMatched=
True,
1038 preConvMode=
True).correctedExposure
1041 subtractedExposure = self.decorrelate.
run(exposureOrig, subtractRes.warpedExposure,
1043 subtractRes.psfMatchingKernel,
1044 spatiallyVarying=self.config.doSpatiallyVarying,
1046 templateMatched=self.config.convolveTemplate,
1047 preConvMode=
False).correctedExposure
1050 if self.config.doDetection:
1051 self.log.
info(
"Running diaSource detection")
1059 if self.config.useScoreImageDetection:
1061 self.log.
info(
"Detection, diffim rescaling and measurements are "
1062 "on AL likelihood or Zogy score image.")
1063 detectionExposure = scoreExposure
1066 detectionExposure = subtractedExposure
1069 if self.config.doScaleDiffimVariance:
1070 self.log.
info(
"Rescaling diffim variance")
1071 diffimVarFactor = self.scaleVariance.
run(detectionExposure.getMaskedImage())
1072 self.log.
info(
"Diffim variance scaling factor: %.2f", diffimVarFactor)
1073 self.metadata.add(
"scaleDiffimVarianceFactor", diffimVarFactor)
1076 mask = detectionExposure.getMaskedImage().getMask()
1077 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
1079 table = afwTable.SourceTable.make(self.schema, idFactory)
1080 table.setMetadata(self.algMetadata)
1081 results = self.detection.
run(
1083 exposure=detectionExposure,
1084 doSmooth=
not self.config.useScoreImageDetection
1087 if self.config.doMerge:
1088 fpSet = results.fpSets.positive
1089 fpSet.merge(results.fpSets.negative, self.config.growFootprint,
1090 self.config.growFootprint,
False)
1092 fpSet.makeSources(diaSources)
1093 self.log.
info(
"Merging detections into %d sources", len(diaSources))
1095 diaSources = results.sources
1097 if self.config.doSkySources:
1098 skySourceFootprints = self.skySources.
run(
1099 mask=detectionExposure.mask,
1100 seed=detectionExposure.info.id)
1101 if skySourceFootprints:
1102 for foot
in skySourceFootprints:
1103 s = diaSources.addNew()
1104 s.setFootprint(foot)
1105 s.set(self.skySourceKey,
True)
1107 if self.config.doMeasurement:
1108 newDipoleFitting = self.config.doDipoleFitting
1109 self.log.
info(
"Running diaSource measurement: newDipoleFitting=%r", newDipoleFitting)
1110 if not newDipoleFitting:
1112 self.measurement.
run(diaSources, detectionExposure)
1115 if self.config.doSubtract
and 'matchedExposure' in subtractRes.getDict():
1116 self.measurement.
run(diaSources, detectionExposure, exposure,
1117 subtractRes.matchedExposure)
1119 self.measurement.
run(diaSources, detectionExposure, exposure)
1120 if self.config.doApCorr:
1121 self.applyApCorr.
run(
1123 apCorrMap=detectionExposure.getInfo().getApCorrMap()
1126 if self.config.doForcedMeasurement:
1129 forcedSources = self.forcedMeasurement.generateMeasCat(
1130 exposure, diaSources, detectionExposure.getWcs())
1131 self.forcedMeasurement.
run(forcedSources, exposure, diaSources, detectionExposure.getWcs())
1133 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
1134 "ip_diffim_forced_PsfFlux_instFlux",
True)
1135 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
1136 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
1137 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
1138 "ip_diffim_forced_PsfFlux_area",
True)
1139 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
1140 "ip_diffim_forced_PsfFlux_flag",
True)
1141 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
1142 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
1143 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
1144 "ip_diffim_forced_PsfFlux_flag_edge",
True)
1145 for diaSource, forcedSource
in zip(diaSources, forcedSources):
1146 diaSource.assign(forcedSource, mapper)
1149 if self.config.doMatchSources:
1150 if selectSources
is not None:
1152 matchRadAsec = self.config.diaSourceMatchRadius
1153 matchRadPixel = matchRadAsec/exposure.getWcs().getPixelScale().asArcseconds()
1156 srcMatchDict = dict([(srcMatch.second.getId(), srcMatch.first.getId())
for
1157 srcMatch
in srcMatches])
1158 self.log.
info(
"Matched %d / %d diaSources to sources",
1159 len(srcMatchDict), len(diaSources))
1161 self.log.
warning(
"Src product does not exist; cannot match with diaSources")
1166 refAstromConfig.matcher.maxMatchDistArcSec = matchRadAsec
1167 refAstrometer =
AstrometryTask(self.refObjLoader, config=refAstromConfig)
1168 astromRet = refAstrometer.run(exposure=exposure, sourceCat=diaSources)
1169 refMatches = astromRet.matches
1170 if refMatches
is None:
1171 self.log.
warning(
"No diaSource matches with reference catalog")
1174 self.log.
info(
"Matched %d / %d diaSources to reference catalog",
1175 len(refMatches), len(diaSources))
1176 refMatchDict = dict([(refMatch.second.getId(), refMatch.first.getId())
for
1177 refMatch
in refMatches])
1180 for diaSource
in diaSources:
1181 sid = diaSource.getId()
1182 if sid
in srcMatchDict:
1183 diaSource.set(
"srcMatchId", srcMatchDict[sid])
1184 if sid
in refMatchDict:
1185 diaSource.set(
"refMatchId", refMatchDict[sid])
1187 if self.config.doAddMetrics
and self.config.doSelectSources:
1188 self.log.
info(
"Evaluating metrics and control sample")
1191 for cell
in subtractRes.kernelCellSet.getCellList():
1192 for cand
in cell.begin(
False):
1193 kernelCandList.append(cand)
1196 basisList = kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelList()
1197 nparam = len(kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelParameters())
1200 diffimTools.sourceTableToCandidateList(controlSources,
1201 subtractRes.warpedExposure, exposure,
1202 self.config.subtract.kernel.active,
1203 self.config.subtract.kernel.active.detectionConfig,
1204 self.log, doBuild=
True, basisList=basisList))
1206 KernelCandidateQa.apply(kernelCandList, subtractRes.psfMatchingKernel,
1207 subtractRes.backgroundModel, dof=nparam)
1208 KernelCandidateQa.apply(controlCandList, subtractRes.psfMatchingKernel,
1209 subtractRes.backgroundModel)
1211 if self.config.doDetection:
1212 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids, diaSources)
1214 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids)
1216 self.runDebug(exposure, subtractRes, selectSources, kernelSources, diaSources)
1217 return pipeBase.Struct(
1218 subtractedExposure=subtractedExposure,
1219 scoreExposure=scoreExposure,
1220 warpedExposure=subtractRes.warpedExposure,
1221 matchedExposure=subtractRes.matchedExposure,
1222 subtractRes=subtractRes,
1223 diaSources=diaSources,
1224 selectSources=selectSources
1227 def fitAstrometry(self, templateSources, templateExposure, selectSources):
1228 """Fit the relative astrometry between templateSources and selectSources
1233 Remove this method. It originally fit a new WCS to the template before calling register.run
1234 because our TAN-SIP fitter behaved badly for points far
from CRPIX, but that
's been fixed.
1235 It remains because a subtask overrides it.
1237 results = self.register.run(templateSources, templateExposure.getWcs(),
1238 templateExposure.getBBox(), selectSources)
1241 def runDebug(self, exposure, subtractRes, selectSources, kernelSources, diaSources):
1242 """Make debug plots and displays.
1246 Test and update
for current debug display
and slot names
1256 disp = afwDisplay.getDisplay(frame=lsstDebug.frame)
1257 if not maskTransparency:
1258 maskTransparency = 0
1259 disp.setMaskTransparency(maskTransparency)
1261 if display
and showSubtracted:
1262 disp.mtv(subtractRes.subtractedExposure, title=
"Subtracted image")
1263 mi = subtractRes.subtractedExposure.getMaskedImage()
1264 x0, y0 = mi.getX0(), mi.getY0()
1265 with disp.Buffering():
1266 for s
in diaSources:
1267 x, y = s.getX() - x0, s.getY() - y0
1268 ctype =
"red" if s.get(
"flags_negative")
else "yellow"
1269 if (s.get(
"base_PixelFlags_flag_interpolatedCenter")
1270 or s.get(
"base_PixelFlags_flag_saturatedCenter")
1271 or s.get(
"base_PixelFlags_flag_crCenter")):
1273 elif (s.get(
"base_PixelFlags_flag_interpolated")
1274 or s.get(
"base_PixelFlags_flag_saturated")
1275 or s.get(
"base_PixelFlags_flag_cr")):
1279 disp.dot(ptype, x, y, size=4, ctype=ctype)
1280 lsstDebug.frame += 1
1282 if display
and showPixelResiduals
and selectSources:
1283 nonKernelSources = []
1284 for source
in selectSources:
1285 if source
not in kernelSources:
1286 nonKernelSources.append(source)
1288 diUtils.plotPixelResiduals(exposure,
1289 subtractRes.warpedExposure,
1290 subtractRes.subtractedExposure,
1291 subtractRes.kernelCellSet,
1292 subtractRes.psfMatchingKernel,
1293 subtractRes.backgroundModel,
1295 self.subtract.config.kernel.active.detectionConfig,
1297 diUtils.plotPixelResiduals(exposure,
1298 subtractRes.warpedExposure,
1299 subtractRes.subtractedExposure,
1300 subtractRes.kernelCellSet,
1301 subtractRes.psfMatchingKernel,
1302 subtractRes.backgroundModel,
1304 self.subtract.config.kernel.active.detectionConfig,
1306 if display
and showDiaSources:
1308 isFlagged = [flagChecker(x)
for x
in diaSources]
1309 isDipole = [x.get(
"ip_diffim_ClassificationDipole_value")
for x
in diaSources]
1310 diUtils.showDiaSources(diaSources, subtractRes.subtractedExposure, isFlagged, isDipole,
1311 frame=lsstDebug.frame)
1312 lsstDebug.frame += 1
1314 if display
and showDipoles:
1315 DipoleAnalysis().displayDipoles(subtractRes.subtractedExposure, diaSources,
1316 frame=lsstDebug.frame)
1317 lsstDebug.frame += 1
1320 """Raise NoWorkFound if template coverage < requiredTemplateFraction
1324 templateExposure : `lsst.afw.image.ExposureF`
1325 The template exposure to check
1330 Raised if fraction of good pixels, defined
as not having NO_DATA
1331 set,
is less then the configured requiredTemplateFraction
1335 pixNoData = numpy.count_nonzero(templateExposure.mask.array
1336 & templateExposure.mask.getPlaneBitMask(
'NO_DATA'))
1337 pixGood = templateExposure.getBBox().getArea() - pixNoData
1338 self.log.
info(
"template has %d good pixels (%.1f%%)", pixGood,
1339 100*pixGood/templateExposure.getBBox().getArea())
1341 if pixGood/templateExposure.getBBox().getArea() < self.config.requiredTemplateFraction:
1342 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%) Not attempting subtraction. "
1343 "To force subtraction, set config requiredTemplateFraction=0." % (
1344 100*pixGood/templateExposure.getBBox().getArea(),
1345 100*self.config.requiredTemplateFraction))
1346 raise pipeBase.NoWorkFound(message)
1348 def _getConfigName(self):
1349 """Return the name of the config dataset
1351 return "%sDiff_config" % (self.config.coaddName,)
1353 def _getMetadataName(self):
1354 """Return the name of the metadata dataset
1356 return "%sDiff_metadata" % (self.config.coaddName,)
1359 """Return a dict of empty catalogs for each catalog dataset produced by this task."""
1360 return {self.config.coaddName +
"Diff_diaSrc": self.outputSchema}
1363 def _makeArgumentParser(cls):
1364 """Create an argument parser
1366 parser = pipeBase.ArgumentParser(name=cls._DefaultName)
1367 parser.add_id_argument("--id",
"calexp", help=
"data ID, e.g. --id visit=12345 ccd=1,2")
1368 parser.add_id_argument(
"--templateId",
"calexp", doMakeDataRefList=
True,
1369 help=
"Template data ID in case of calexp template,"
1370 " e.g. --templateId visit=6789")
1375 defaultTemplates={
"coaddName":
"goodSeeing"}
1377 inputTemplate = pipeBase.connectionTypes.Input(
1378 doc=(
"Warped template produced by GetMultiTractCoaddTemplate"),
1379 dimensions=(
"instrument",
"visit",
"detector"),
1380 storageClass=
"ExposureF",
1381 name=
"{fakesType}{coaddName}Diff_templateExp{warpTypeSuffix}",
1384 def __init__(self, *, config=None):
1385 super().__init__(config=config)
1388 if "coaddExposures" in self.inputs:
1389 self.inputs.remove(
"coaddExposures")
1390 if "dcrCoadds" in self.inputs:
1391 self.inputs.remove(
"dcrCoadds")
1395 pipelineConnections=ImageDifferenceFromTemplateConnections):
1400 ConfigClass = ImageDifferenceFromTemplateConfig
1401 _DefaultName =
"imageDifference"
1403 @lsst.utils.inheritDoc(pipeBase.PipelineTask)
1405 inputs = butlerQC.get(inputRefs)
1406 self.log.
info(
"Processing %s", butlerQC.quantum.dataId)
1407 self.checkTemplateIsSufficient(inputs[
'inputTemplate'])
1408 expId, expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
1410 idFactory = self.makeIdFactory(expId=expId, expBits=expBits)
1412 finalizedPsfApCorrCatalog = inputs.get(
"finalizedPsfApCorrCatalog",
None)
1413 exposure = self.prepareCalibratedExposure(
1415 finalizedPsfApCorrCatalog=finalizedPsfApCorrCatalog
1418 outputs = self.run(exposure=exposure,
1419 templateExposure=inputs[
'inputTemplate'],
1420 idFactory=idFactory)
1423 if outputs.diaSources
is None:
1424 del outputs.diaSources
1425 butlerQC.put(outputs, outputRefs)
1429 winter2013WcsShift = pexConfig.Field(dtype=float, default=0.0,
1430 doc=
"Shift stars going into RegisterTask by this amount")
1431 winter2013WcsRms = pexConfig.Field(dtype=float, default=0.0,
1432 doc=
"Perturb stars going into RegisterTask by this amount")
1435 ImageDifferenceConfig.setDefaults(self)
1436 self.getTemplate.retarget(GetCalexpAsTemplateTask)
1440 """!Image difference Task used in the Winter 2013 data challege.
1441 Enables testing the effects of registration shifts and scatter.
1443 For use
with winter 2013 simulated images:
1444 Use --templateId visit=88868666
for sparse data
1445 --templateId visit=22222200
for dense data (g)
1446 --templateId visit=11111100
for dense data (i)
1448 ConfigClass = Winter2013ImageDifferenceConfig
1449 _DefaultName = "winter2013ImageDifference"
1452 ImageDifferenceTask.__init__(self, **kwargs)
1455 """Fit the relative astrometry between templateSources and selectSources"""
1456 if self.config.winter2013WcsShift > 0.0:
1458 self.config.winter2013WcsShift)
1459 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1460 for source
in templateSources:
1461 centroid = source.get(cKey)
1462 source.set(cKey, centroid + offset)
1463 elif self.config.winter2013WcsRms > 0.0:
1464 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1465 for source
in templateSources:
1466 offset =
geom.Extent2D(self.config.winter2013WcsRms*numpy.random.normal(),
1467 self.config.winter2013WcsRms*numpy.random.normal())
1468 centroid = source.get(cKey)
1469 source.set(cKey, centroid + offset)
1471 results = self.register.
run(templateSources, templateExposure.getWcs(),
1472 templateExposure.getBBox(), selectSources)
Parameters to control convolution.
Custom catalog class for ExposureRecord/Table.
A polymorphic functor base class for generating record IDs for a table.
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, dataIds, **kwargs)
def makeKernelBasisList(config, targetFwhmPix=None, referenceFwhmPix=None, basisDegGauss=None, basisSigmaGauss=None, metadata=None)
def checkTemplateIsSufficient(templateExposure, logger, requiredTemplateFraction=0.)
Fit spatial kernel using approximate fluxes for candidates, and solving a linear system of equations.
def getSchemaCatalogs(self)