38 from lsst.meas.algorithms import SourceDetectionTask, SingleGaussianPsf, ObjectSizeStarSelectorTask
39 from lsst.ip.diffim import (DipoleAnalysis, SourceFlagChecker, KernelCandidateF, makeKernelBasisList,
40 KernelCandidateQa, DiaCatalogSourceSelectorTask, DiaCatalogSourceSelectorConfig,
41 GetCoaddAsTemplateTask, GetCalexpAsTemplateTask, DipoleFitTask,
42 DecorrelateALKernelSpatialTask, subtractAlgorithmRegistry)
47 from lsst.obs.base
import ExposureIdInfo
49 __all__ = [
"ImageDifferenceConfig",
"ImageDifferenceTask"]
50 FwhmPerSigma = 2*math.sqrt(2*math.log(2))
55 dimensions=(
"instrument",
"visit",
"detector",
"skymap"),
56 defaultTemplates={
"coaddName":
"deep",
61 exposure = pipeBase.connectionTypes.Input(
62 doc=
"Input science exposure to subtract from.",
63 dimensions=(
"instrument",
"visit",
"detector"),
64 storageClass=
"ExposureF",
65 name=
"{fakesType}calexp"
76 skyMap = pipeBase.connectionTypes.Input(
77 doc=
"Input definition of geometry/bbox and projection/wcs for template exposures",
78 name=BaseSkyMap.SKYMAP_DATASET_TYPE_NAME,
79 dimensions=(
"skymap", ),
80 storageClass=
"SkyMap",
82 coaddExposures = pipeBase.connectionTypes.Input(
83 doc=
"Input template to match and subtract from the exposure",
84 dimensions=(
"tract",
"patch",
"skymap",
"band"),
85 storageClass=
"ExposureF",
86 name=
"{fakesType}{coaddName}Coadd{warpTypeSuffix}",
90 dcrCoadds = pipeBase.connectionTypes.Input(
91 doc=
"Input DCR template to match and subtract from the exposure",
92 name=
"{fakesType}dcrCoadd{warpTypeSuffix}",
93 storageClass=
"ExposureF",
94 dimensions=(
"tract",
"patch",
"skymap",
"band",
"subfilter"),
98 outputSchema = pipeBase.connectionTypes.InitOutput(
99 doc=
"Schema (as an example catalog) for output DIASource catalog.",
100 storageClass=
"SourceCatalog",
101 name=
"{fakesType}{coaddName}Diff_diaSrc_schema",
104 subtractedExposure = pipeBase.connectionTypes.Output(
105 doc=
"Output AL difference or Zogy proper difference image",
106 dimensions=(
"instrument",
"visit",
"detector"),
107 storageClass=
"ExposureF",
108 name=
"{fakesType}{coaddName}Diff_differenceExp",
110 scoreExposure = pipeBase.connectionTypes.Output(
111 doc=
"Output Zogy score (likelihood) image",
112 dimensions=(
"instrument",
"visit",
"detector"),
113 storageClass=
"ExposureF",
114 name=
"{fakesType}{coaddName}Diff_scoreExp",
116 warpedExposure = pipeBase.connectionTypes.Output(
117 doc=
"Warped template used to create `subtractedExposure`.",
118 dimensions=(
"instrument",
"visit",
"detector"),
119 storageClass=
"ExposureF",
120 name=
"{fakesType}{coaddName}Diff_warpedExp",
122 matchedExposure = pipeBase.connectionTypes.Output(
123 doc=
"Warped template used to create `subtractedExposure`.",
124 dimensions=(
"instrument",
"visit",
"detector"),
125 storageClass=
"ExposureF",
126 name=
"{fakesType}{coaddName}Diff_matchedExp",
128 diaSources = pipeBase.connectionTypes.Output(
129 doc=
"Output detected diaSources on the difference image",
130 dimensions=(
"instrument",
"visit",
"detector"),
131 storageClass=
"SourceCatalog",
132 name=
"{fakesType}{coaddName}Diff_diaSrc",
135 def __init__(self, *, config=None):
136 super().__init__(config=config)
137 if config.coaddName ==
'dcr':
138 self.inputs.remove(
"coaddExposures")
140 self.inputs.remove(
"dcrCoadds")
141 if not config.doWriteSubtractedExp:
142 self.outputs.remove(
"subtractedExposure")
143 if not config.doWriteScoreExp:
144 self.outputs.remove(
"scoreExposure")
145 if not config.doWriteWarpedExp:
146 self.outputs.remove(
"warpedExposure")
147 if not config.doWriteMatchedExp:
148 self.outputs.remove(
"matchedExposure")
149 if not config.doWriteSources:
150 self.outputs.remove(
"diaSources")
156 class ImageDifferenceConfig(pipeBase.PipelineTaskConfig,
157 pipelineConnections=ImageDifferenceTaskConnections):
158 """Config for ImageDifferenceTask
160 doAddCalexpBackground = pexConfig.Field(dtype=bool, default=
False,
161 doc=
"Add background to calexp before processing it. "
162 "Useful as ipDiffim does background matching.")
163 doUseRegister = pexConfig.Field(dtype=bool, default=
False,
164 doc=
"Re-compute astrometry on the template. "
165 "Use image-to-image registration to align template with "
166 "science image (AL only).")
167 doDebugRegister = pexConfig.Field(dtype=bool, default=
False,
168 doc=
"Writing debugging data for doUseRegister")
169 doSelectSources = pexConfig.Field(dtype=bool, default=
False,
170 doc=
"Select stars to use for kernel fitting (AL only)")
171 doSelectDcrCatalog = pexConfig.Field(dtype=bool, default=
False,
172 doc=
"Select stars of extreme color as part "
173 "of the control sample (AL only)")
174 doSelectVariableCatalog = pexConfig.Field(dtype=bool, default=
False,
175 doc=
"Select stars that are variable to be part "
176 "of the control sample (AL only)")
177 doSubtract = pexConfig.Field(dtype=bool, default=
True, doc=
"Compute subtracted exposure?")
178 doPreConvolve = pexConfig.Field(dtype=bool, default=
False,
179 doc=
"Convolve science image by its PSF before PSF-matching (AL only)."
180 " The difference image becomes a likelihood image.")
181 useScoreImageDetection = pexConfig.Field(
182 dtype=bool, default=
False, doc=
"Calculate and detect sources on the Zogy score image (Zogy only).")
183 doWriteScoreExp = pexConfig.Field(
184 dtype=bool, default=
False, doc=
"Write score exposure (Zogy only) ?")
185 doScaleTemplateVariance = pexConfig.Field(dtype=bool, default=
False,
186 doc=
"Scale variance of the template before PSF matching")
187 doScaleDiffimVariance = pexConfig.Field(dtype=bool, default=
True,
188 doc=
"Scale variance of the diffim before PSF matching. "
189 "You may do either this or template variance scaling, "
190 "or neither. (Doing both is a waste of CPU.)")
191 useGaussianForPreConvolution = pexConfig.Field(dtype=bool, default=
True,
192 doc=
"Use a simple gaussian PSF model for pre-convolution "
193 "(else use fit PSF)? Ignored if doPreConvolve false.")
194 doDetection = pexConfig.Field(dtype=bool, default=
True, doc=
"Detect sources?")
195 doDecorrelation = pexConfig.Field(dtype=bool, default=
True,
196 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L "
197 "kernel convolution (AL only)? If True, also update the diffim PSF.")
198 doMerge = pexConfig.Field(dtype=bool, default=
True,
199 doc=
"Merge positive and negative diaSources with grow radius "
200 "set by growFootprint")
201 doMatchSources = pexConfig.Field(dtype=bool, default=
False,
202 doc=
"Match diaSources with input calexp sources and ref catalog sources")
203 doMeasurement = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure diaSources?")
204 doDipoleFitting = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure dipoles using new algorithm?")
205 doForcedMeasurement = pexConfig.Field(
208 doc=
"Force photometer diaSource locations on PVI?")
209 doWriteSubtractedExp = pexConfig.Field(
210 dtype=bool, default=
True, doc=
"Write difference exposure (AL and Zogy) ?")
211 doWriteWarpedExp = pexConfig.Field(
212 dtype=bool, default=
False, doc=
"Write WCS, warped template coadd exposure?")
213 doWriteMatchedExp = pexConfig.Field(dtype=bool, default=
False,
214 doc=
"Write warped and PSF-matched template coadd exposure?")
215 doWriteSources = pexConfig.Field(dtype=bool, default=
True, doc=
"Write sources?")
216 doAddMetrics = pexConfig.Field(dtype=bool, default=
False,
217 doc=
"Add columns to the source table to hold analysis metrics?")
219 coaddName = pexConfig.Field(
220 doc=
"coadd name: typically one of deep, goodSeeing, or dcr",
224 convolveTemplate = pexConfig.Field(
225 doc=
"Which image gets convolved (default = template)",
229 refObjLoader = pexConfig.ConfigurableField(
230 target=LoadIndexedReferenceObjectsTask,
231 doc=
"reference object loader",
233 astrometer = pexConfig.ConfigurableField(
234 target=AstrometryTask,
235 doc=
"astrometry task; used to match sources to reference objects, but not to fit a WCS",
237 sourceSelector = pexConfig.ConfigurableField(
238 target=ObjectSizeStarSelectorTask,
239 doc=
"Source selection algorithm",
241 subtract = subtractAlgorithmRegistry.makeField(
"Subtraction Algorithm", default=
"al")
242 decorrelate = pexConfig.ConfigurableField(
243 target=DecorrelateALKernelSpatialTask,
244 doc=
"Decorrelate effects of A&L kernel convolution on image difference, only if doSubtract is True. "
245 "If this option is enabled, then detection.thresholdValue should be set to 5.0 (rather than the "
249 doSpatiallyVarying = pexConfig.Field(
252 doc=
"Perform A&L decorrelation on a grid across the "
253 "image in order to allow for spatial variations. Zogy does not use this option."
255 detection = pexConfig.ConfigurableField(
256 target=SourceDetectionTask,
257 doc=
"Low-threshold detection for final measurement",
259 measurement = pexConfig.ConfigurableField(
260 target=DipoleFitTask,
261 doc=
"Enable updated dipole fitting method",
263 doApCorr = lsst.pex.config.Field(
266 doc=
"Run subtask to apply aperture corrections"
268 applyApCorr = lsst.pex.config.ConfigurableField(
269 target=ApplyApCorrTask,
270 doc=
"Subtask to apply aperture corrections"
272 forcedMeasurement = pexConfig.ConfigurableField(
273 target=ForcedMeasurementTask,
274 doc=
"Subtask to force photometer PVI at diaSource location.",
276 getTemplate = pexConfig.ConfigurableField(
277 target=GetCoaddAsTemplateTask,
278 doc=
"Subtask to retrieve template exposure and sources",
280 scaleVariance = pexConfig.ConfigurableField(
281 target=ScaleVarianceTask,
282 doc=
"Subtask to rescale the variance of the template "
283 "to the statistically expected level"
285 controlStepSize = pexConfig.Field(
286 doc=
"What step size (every Nth one) to select a control sample from the kernelSources",
290 controlRandomSeed = pexConfig.Field(
291 doc=
"Random seed for shuffing the control sample",
295 register = pexConfig.ConfigurableField(
297 doc=
"Task to enable image-to-image image registration (warping)",
299 kernelSourcesFromRef = pexConfig.Field(
300 doc=
"Select sources to measure kernel from reference catalog if True, template if false",
304 templateSipOrder = pexConfig.Field(
305 dtype=int, default=2,
306 doc=
"Sip Order for fitting the Template Wcs (default is too high, overfitting)"
308 growFootprint = pexConfig.Field(
309 dtype=int, default=2,
310 doc=
"Grow positive and negative footprints by this amount before merging"
312 diaSourceMatchRadius = pexConfig.Field(
313 dtype=float, default=0.5,
314 doc=
"Match radius (in arcseconds) for DiaSource to Source association"
316 requiredTemplateFraction = pexConfig.Field(
317 dtype=float, default=0.1,
318 doc=
"Do not attempt to run task if template covers less than this fraction of pixels."
319 "Setting to 0 will always attempt image subtraction"
322 def setDefaults(self):
325 self.subtract[
'al'].kernel.name =
"AL"
326 self.subtract[
'al'].kernel.active.fitForBackground =
True
327 self.subtract[
'al'].kernel.active.spatialKernelOrder = 1
328 self.subtract[
'al'].kernel.active.spatialBgOrder = 2
331 self.detection.thresholdPolarity =
"both"
332 self.detection.thresholdValue = 5.0
333 self.detection.reEstimateBackground =
False
334 self.detection.thresholdType =
"pixel_stdev"
340 self.measurement.algorithms.names.add(
'base_PeakLikelihoodFlux')
341 self.measurement.plugins.names |= [
'base_LocalPhotoCalib',
344 self.forcedMeasurement.plugins = [
"base_TransformedCentroid",
"base_PsfFlux"]
345 self.forcedMeasurement.copyColumns = {
346 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
347 self.forcedMeasurement.slots.centroid =
"base_TransformedCentroid"
348 self.forcedMeasurement.slots.shape =
None
351 random.seed(self.controlRandomSeed)
354 pexConfig.Config.validate(self)
355 if not self.doSubtract
and not self.doDetection:
356 raise ValueError(
"Either doSubtract or doDetection must be enabled.")
357 if self.doMeasurement
and not self.doDetection:
358 raise ValueError(
"Cannot run source measurement without source detection.")
359 if self.doMerge
and not self.doDetection:
360 raise ValueError(
"Cannot run source merging without source detection.")
361 if self.doUseRegister
and not self.doSelectSources:
362 raise ValueError(
"doUseRegister=True and doSelectSources=False. "
363 "Cannot run RegisterTask without selecting sources.")
364 if hasattr(self.getTemplate,
"coaddName"):
365 if self.getTemplate.coaddName != self.coaddName:
366 raise ValueError(
"Mis-matched coaddName and getTemplate.coaddName in the config.")
367 if self.doScaleDiffimVariance
and self.doScaleTemplateVariance:
368 raise ValueError(
"Scaling the diffim variance and scaling the template variance "
369 "are both set. Please choose one or the other.")
371 if self.subtract.name ==
'zogy':
372 if self.doWriteScoreExp
and not self.useScoreImageDetection:
373 raise ValueError(
"doWriteScoreExp=True and useScoreImageDetection=False "
374 "is not supported. Score image is not calculated.")
375 if self.doWriteMatchedExp:
376 raise ValueError(
"doWriteMatchedExp=True Matched exposure is not "
377 "calculated in zogy subtraction.")
378 if self.doAddMetrics:
379 raise ValueError(
"doAddMetrics=True Kernel metrics does not exist in zogy subtraction.")
380 if self.doDecorrelation:
382 "doDecorrelation=True The decorrelation afterburner does not exist in zogy subtraction.")
383 if self.doPreConvolve:
385 "doPreConvolve=True Pre-convolution is not a zogy option.")
386 if self.doSelectSources:
388 "doSelectSources=True Selecting sources for PSF matching is not a zogy option.")
390 if self.useScoreImageDetection:
391 raise ValueError(
"useScoreImageDetection=True Score exposure does not "
392 "exist in AL subtraction.")
393 if self.doWriteScoreExp:
394 raise ValueError(
"doWriteScoreExp=True Score exposure does not exist in AL subtraction.")
395 if self.doAddMetrics
and not self.doSubtract:
396 raise ValueError(
"Subtraction must be enabled for kernel metrics calculation.")
397 if self.doPreConvolve
and self.doDecorrelation:
398 raise NotImplementedError(
399 "doPreConvolve=True and doDecorrelation=True "
400 "The decorrelation afterburner cannot handle pre-convolved exposures.")
403 class ImageDifferenceTaskRunner(pipeBase.ButlerInitializedTaskRunner):
406 def getTargetList(parsedCmd, **kwargs):
407 return pipeBase.TaskRunner.getTargetList(parsedCmd, templateIdList=parsedCmd.templateId.idList,
411 class ImageDifferenceTask(pipeBase.CmdLineTask, pipeBase.PipelineTask):
412 """Subtract an image from a template and measure the result
414 ConfigClass = ImageDifferenceConfig
415 RunnerClass = ImageDifferenceTaskRunner
416 _DefaultName =
"imageDifference"
418 def __init__(self, butler=None, **kwargs):
419 """!Construct an ImageDifference Task
421 @param[in] butler Butler object to use in constructing reference object loaders
423 super().__init__(**kwargs)
424 self.makeSubtask(
"getTemplate")
426 self.makeSubtask(
"subtract")
428 if self.config.subtract.name ==
'al' and self.config.doDecorrelation:
429 self.makeSubtask(
"decorrelate")
431 if self.config.doScaleTemplateVariance
or self.config.doScaleDiffimVariance:
432 self.makeSubtask(
"scaleVariance")
434 if self.config.doUseRegister:
435 self.makeSubtask(
"register")
436 self.schema = afwTable.SourceTable.makeMinimalSchema()
438 if self.config.doSelectSources:
439 self.makeSubtask(
"sourceSelector")
440 if self.config.kernelSourcesFromRef:
441 self.makeSubtask(
'refObjLoader', butler=butler)
442 self.makeSubtask(
"astrometer", refObjLoader=self.refObjLoader)
444 self.algMetadata = dafBase.PropertyList()
445 if self.config.doDetection:
446 self.makeSubtask(
"detection", schema=self.schema)
447 if self.config.doMeasurement:
448 self.makeSubtask(
"measurement", schema=self.schema,
449 algMetadata=self.algMetadata)
450 if self.config.doApCorr:
451 self.makeSubtask(
"applyApCorr", schema=self.measurement.schema)
452 if self.config.doForcedMeasurement:
453 self.schema.addField(
454 "ip_diffim_forced_PsfFlux_instFlux",
"D",
455 "Forced PSF flux measured on the direct image.",
457 self.schema.addField(
458 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
459 "Forced PSF flux error measured on the direct image.",
461 self.schema.addField(
462 "ip_diffim_forced_PsfFlux_area",
"F",
463 "Forced PSF flux effective area of PSF.",
465 self.schema.addField(
466 "ip_diffim_forced_PsfFlux_flag",
"Flag",
467 "Forced PSF flux general failure flag.")
468 self.schema.addField(
469 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
470 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
471 self.schema.addField(
472 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
473 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
474 self.makeSubtask(
"forcedMeasurement", refSchema=self.schema)
475 if self.config.doMatchSources:
476 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
477 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
480 self.outputSchema = afwTable.SourceCatalog(self.schema)
481 self.outputSchema.getTable().setMetadata(self.algMetadata)
484 def makeIdFactory(expId, expBits):
485 """Create IdFactory instance for unique 64 bit diaSource id-s.
493 Number of used bits in ``expId``.
497 The diasource id-s consists of the ``expId`` stored fixed in the highest value
498 ``expBits`` of the 64-bit integer plus (bitwise or) a generated sequence number in the
499 low value end of the integer.
503 idFactory: `lsst.afw.table.IdFactory`
505 return ExposureIdInfo(expId, expBits).makeSourceIdFactory()
507 @lsst.utils.inheritDoc(pipeBase.PipelineTask)
508 def runQuantum(self, butlerQC: pipeBase.ButlerQuantumContext,
509 inputRefs: pipeBase.InputQuantizedConnection,
510 outputRefs: pipeBase.OutputQuantizedConnection):
511 inputs = butlerQC.get(inputRefs)
512 self.log.info(f
"Processing {butlerQC.quantum.dataId}")
513 expId, expBits = butlerQC.quantum.dataId.pack(
"visit_detector",
515 idFactory = self.makeIdFactory(expId=expId, expBits=expBits)
516 if self.config.coaddName ==
'dcr':
517 templateExposures = inputRefs.dcrCoadds
519 templateExposures = inputRefs.coaddExposures
520 templateStruct = self.getTemplate.runQuantum(
521 inputs[
'exposure'], butlerQC, inputRefs.skyMap, templateExposures
524 if templateStruct.area/inputs[
'exposure'].getBBox().getArea() < self.config.requiredTemplateFraction:
525 message = (
"Insufficient Template Coverage. (%.1f%% < %.1f%%) Not attempting subtraction. "
526 "To force subtraction, set config requiredTemplateFraction=0." % (
527 100*templateStruct.area/inputs[
'exposure'].getBBox().getArea(),
528 100*self.config.requiredTemplateFraction))
529 raise pipeBase.NoWorkFound(message)
531 outputs = self.run(exposure=inputs[
'exposure'],
532 templateExposure=templateStruct.exposure,
535 if outputs.diaSources
is None:
536 del outputs.diaSources
537 butlerQC.put(outputs, outputRefs)
540 def runDataRef(self, sensorRef, templateIdList=None):
541 """Subtract an image from a template coadd and measure the result.
543 Data I/O wrapper around `run` using the butler in Gen2.
547 sensorRef : `lsst.daf.persistence.ButlerDataRef`
548 Sensor-level butler data reference, used for the following data products:
555 - self.config.coaddName + "Coadd_skyMap"
556 - self.config.coaddName + "Coadd"
557 Input or output, depending on config:
558 - self.config.coaddName + "Diff_subtractedExp"
559 Output, depending on config:
560 - self.config.coaddName + "Diff_matchedExp"
561 - self.config.coaddName + "Diff_src"
565 results : `lsst.pipe.base.Struct`
566 Returns the Struct by `run`.
568 subtractedExposureName = self.config.coaddName +
"Diff_differenceExp"
569 subtractedExposure =
None
571 calexpBackgroundExposure =
None
572 self.log.info(
"Processing %s" % (sensorRef.dataId))
577 idFactory = self.makeIdFactory(expId=int(sensorRef.get(
"ccdExposureId")),
578 expBits=sensorRef.get(
"ccdExposureId_bits"))
579 if self.config.doAddCalexpBackground:
580 calexpBackgroundExposure = sensorRef.get(
"calexpBackground")
583 exposure = sensorRef.get(
"calexp", immediate=
True)
586 template = self.getTemplate.runDataRef(exposure, sensorRef, templateIdList=templateIdList)
588 if sensorRef.datasetExists(
"src"):
589 self.log.info(
"Source selection via src product")
591 selectSources = sensorRef.get(
"src")
593 if not self.config.doSubtract
and self.config.doDetection:
595 subtractedExposure = sensorRef.get(subtractedExposureName)
598 results = self.run(exposure=exposure,
599 selectSources=selectSources,
600 templateExposure=template.exposure,
601 templateSources=template.sources,
603 calexpBackgroundExposure=calexpBackgroundExposure,
604 subtractedExposure=subtractedExposure)
606 if self.config.doWriteSources
and results.diaSources
is not None:
607 sensorRef.put(results.diaSources, self.config.coaddName +
"Diff_diaSrc")
608 if self.config.doWriteWarpedExp:
609 sensorRef.put(results.warpedExposure, self.config.coaddName +
"Diff_warpedExp")
610 if self.config.doWriteMatchedExp:
611 sensorRef.put(results.matchedExposure, self.config.coaddName +
"Diff_matchedExp")
612 if self.config.doAddMetrics
and self.config.doSelectSources:
613 sensorRef.put(results.selectSources, self.config.coaddName +
"Diff_kernelSrc")
614 if self.config.doWriteSubtractedExp:
615 sensorRef.put(results.subtractedExposure, subtractedExposureName)
616 if self.config.doWriteScoreExp:
617 sensorRef.put(results.scoreExposure, self.config.coaddName +
"Diff_scoreExp")
621 def run(self, exposure=None, selectSources=None, templateExposure=None, templateSources=None,
622 idFactory=None, calexpBackgroundExposure=None, subtractedExposure=None):
623 """PSF matches, subtract two images and perform detection on the difference image.
627 exposure : `lsst.afw.image.ExposureF`, optional
628 The science exposure, the minuend in the image subtraction.
629 Can be None only if ``config.doSubtract==False``.
630 selectSources : `lsst.afw.table.SourceCatalog`, optional
631 Identified sources on the science exposure. This catalog is used to
632 select sources in order to perform the AL PSF matching on stamp images
633 around them. The selection steps depend on config options and whether
634 ``templateSources`` and ``matchingSources`` specified.
635 templateExposure : `lsst.afw.image.ExposureF`, optional
636 The template to be subtracted from ``exposure`` in the image subtraction.
637 ``templateExposure`` is modified in place if ``config.doScaleTemplateVariance==True``.
638 The template exposure should cover the same sky area as the science exposure.
639 It is either a stich of patches of a coadd skymap image or a calexp
640 of the same pointing as the science exposure. Can be None only
641 if ``config.doSubtract==False`` and ``subtractedExposure`` is not None.
642 templateSources : `lsst.afw.table.SourceCatalog`, optional
643 Identified sources on the template exposure.
644 idFactory : `lsst.afw.table.IdFactory`
645 Generator object to assign ids to detected sources in the difference image.
646 calexpBackgroundExposure : `lsst.afw.image.ExposureF`, optional
647 Background exposure to be added back to the science exposure
648 if ``config.doAddCalexpBackground==True``
649 subtractedExposure : `lsst.afw.image.ExposureF`, optional
650 If ``config.doSubtract==False`` and ``config.doDetection==True``,
651 performs the post subtraction source detection only on this exposure.
652 Otherwise should be None.
656 results : `lsst.pipe.base.Struct`
657 ``subtractedExposure`` : `lsst.afw.image.ExposureF`
659 ``scoreExposure`` : `lsst.afw.image.ExposureF` or `None`
660 The zogy score exposure, if calculated.
661 ``matchedExposure`` : `lsst.afw.image.ExposureF`
662 The matched PSF exposure.
663 ``subtractRes`` : `lsst.pipe.base.Struct`
664 The returned result structure of the ImagePsfMatchTask subtask.
665 ``diaSources`` : `lsst.afw.table.SourceCatalog`
666 The catalog of detected sources.
667 ``selectSources`` : `lsst.afw.table.SourceCatalog`
668 The input source catalog with optionally added Qa information.
672 The following major steps are included:
674 - warp template coadd to match WCS of image
675 - PSF match image to warped template
676 - subtract image from PSF-matched, warped template
680 For details about the image subtraction configuration modes
681 see `lsst.ip.diffim`.
684 controlSources =
None
689 exposureOrig = exposure
691 if self.config.doAddCalexpBackground:
692 mi = exposure.getMaskedImage()
693 mi += calexpBackgroundExposure.getImage()
695 if not exposure.hasPsf():
696 raise pipeBase.TaskError(
"Exposure has no psf")
697 sciencePsf = exposure.getPsf()
699 if self.config.doSubtract:
700 if self.config.doScaleTemplateVariance:
701 self.log.info(
"Rescaling template variance")
702 templateVarFactor = self.scaleVariance.
run(
703 templateExposure.getMaskedImage())
704 self.log.info(
"Template variance scaling factor: %.2f" % templateVarFactor)
705 self.metadata.add(
"scaleTemplateVarianceFactor", templateVarFactor)
707 if self.config.subtract.name ==
'zogy':
708 subtractRes = self.subtract.
run(exposure, templateExposure, doWarping=
True)
709 scoreExposure = subtractRes.scoreExp
710 subtractedExposure = subtractRes.diffExp
711 subtractRes.subtractedExposure = subtractedExposure
712 subtractRes.matchedExposure =
None
714 elif self.config.subtract.name ==
'al':
716 scienceSigmaOrig = sciencePsf.computeShape().getDeterminantRadius()
717 templateSigma = templateExposure.getPsf().computeShape().getDeterminantRadius()
725 if self.config.doPreConvolve:
726 convControl = afwMath.ConvolutionControl()
728 srcMI = exposure.maskedImage
729 exposure = exposure.clone()
731 if self.config.useGaussianForPreConvolution:
733 kWidth, kHeight = sciencePsf.getLocalKernel().getDimensions()
738 afwMath.convolve(exposure.maskedImage, srcMI, preConvPsf.getLocalKernel(), convControl)
739 scienceSigmaPost = scienceSigmaOrig*math.sqrt(2)
741 scienceSigmaPost = scienceSigmaOrig
746 if self.config.doSelectSources:
747 if selectSources
is None:
748 self.log.warn(
"Src product does not exist; running detection, measurement, selection")
750 selectSources = self.subtract.getSelectSources(
752 sigma=scienceSigmaPost,
753 doSmooth=
not self.config.doPreConvolve,
757 if self.config.doAddMetrics:
760 nparam = len(makeKernelBasisList(self.subtract.config.kernel.active,
761 referenceFwhmPix=scienceSigmaPost*FwhmPerSigma,
762 targetFwhmPix=templateSigma*FwhmPerSigma))
769 kcQa = KernelCandidateQa(nparam)
770 selectSources = kcQa.addToSchema(selectSources)
771 if self.config.kernelSourcesFromRef:
773 astromRet = self.astrometer.loadAndMatch(exposure=exposure, sourceCat=selectSources)
774 matches = astromRet.matches
775 elif templateSources:
777 mc = afwTable.MatchControl()
778 mc.findOnlyClosest =
False
779 matches = afwTable.matchRaDec(templateSources, selectSources, 1.0*geom.arcseconds,
782 raise RuntimeError(
"doSelectSources=True and kernelSourcesFromRef=False,"
783 "but template sources not available. Cannot match science "
784 "sources with template sources. Run process* on data from "
785 "which templates are built.")
787 kernelSources = self.sourceSelector.
run(selectSources, exposure=exposure,
788 matches=matches).sourceCat
789 random.shuffle(kernelSources, random.random)
790 controlSources = kernelSources[::self.config.controlStepSize]
791 kernelSources = [k
for i, k
in enumerate(kernelSources)
792 if i % self.config.controlStepSize]
794 if self.config.doSelectDcrCatalog:
795 redSelector = DiaCatalogSourceSelectorTask(
796 DiaCatalogSourceSelectorConfig(grMin=self.sourceSelector.config.grMax,
798 redSources = redSelector.selectStars(exposure, selectSources, matches=matches).starCat
799 controlSources.extend(redSources)
801 blueSelector = DiaCatalogSourceSelectorTask(
802 DiaCatalogSourceSelectorConfig(grMin=-99.999,
803 grMax=self.sourceSelector.config.grMin))
804 blueSources = blueSelector.selectStars(exposure, selectSources,
805 matches=matches).starCat
806 controlSources.extend(blueSources)
808 if self.config.doSelectVariableCatalog:
809 varSelector = DiaCatalogSourceSelectorTask(
810 DiaCatalogSourceSelectorConfig(includeVariable=
True))
811 varSources = varSelector.selectStars(exposure, selectSources, matches=matches).starCat
812 controlSources.extend(varSources)
814 self.log.info(
"Selected %d / %d sources for Psf matching (%d for control sample)"
815 % (len(kernelSources), len(selectSources), len(controlSources)))
819 if self.config.doUseRegister:
820 self.log.info(
"Registering images")
822 if templateSources
is None:
826 templateSigma = templateExposure.getPsf().computeShape().getDeterminantRadius()
827 templateSources = self.subtract.getSelectSources(
836 wcsResults = self.fitAstrometry(templateSources, templateExposure, selectSources)
837 warpedExp = self.register.warpExposure(templateExposure, wcsResults.wcs,
838 exposure.getWcs(), exposure.getBBox())
839 templateExposure = warpedExp
844 if self.config.doDebugRegister:
846 srcToMatch = {x.second.getId(): x.first
for x
in matches}
848 refCoordKey = wcsResults.matches[0].first.getTable().getCoordKey()
849 inCentroidKey = wcsResults.matches[0].second.getTable().getCentroidSlot().getMeasKey()
850 sids = [m.first.getId()
for m
in wcsResults.matches]
851 positions = [m.first.get(refCoordKey)
for m
in wcsResults.matches]
852 residuals = [m.first.get(refCoordKey).getOffsetFrom(wcsResults.wcs.pixelToSky(
853 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
854 allresids = dict(zip(sids, zip(positions, residuals)))
856 cresiduals = [m.first.get(refCoordKey).getTangentPlaneOffset(
857 wcsResults.wcs.pixelToSky(
858 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
859 colors = numpy.array([-2.5*numpy.log10(srcToMatch[x].get(
"g"))
860 + 2.5*numpy.log10(srcToMatch[x].get(
"r"))
861 for x
in sids
if x
in srcToMatch.keys()])
862 dlong = numpy.array([r[0].asArcseconds()
for s, r
in zip(sids, cresiduals)
863 if s
in srcToMatch.keys()])
864 dlat = numpy.array([r[1].asArcseconds()
for s, r
in zip(sids, cresiduals)
865 if s
in srcToMatch.keys()])
866 idx1 = numpy.where(colors < self.sourceSelector.config.grMin)
867 idx2 = numpy.where((colors >= self.sourceSelector.config.grMin)
868 & (colors <= self.sourceSelector.config.grMax))
869 idx3 = numpy.where(colors > self.sourceSelector.config.grMax)
870 rms1Long = IqrToSigma*(
871 (numpy.percentile(dlong[idx1], 75) - numpy.percentile(dlong[idx1], 25)))
872 rms1Lat = IqrToSigma*(numpy.percentile(dlat[idx1], 75)
873 - numpy.percentile(dlat[idx1], 25))
874 rms2Long = IqrToSigma*(
875 (numpy.percentile(dlong[idx2], 75) - numpy.percentile(dlong[idx2], 25)))
876 rms2Lat = IqrToSigma*(numpy.percentile(dlat[idx2], 75)
877 - numpy.percentile(dlat[idx2], 25))
878 rms3Long = IqrToSigma*(
879 (numpy.percentile(dlong[idx3], 75) - numpy.percentile(dlong[idx3], 25)))
880 rms3Lat = IqrToSigma*(numpy.percentile(dlat[idx3], 75)
881 - numpy.percentile(dlat[idx3], 25))
882 self.log.info(
"Blue star offsets'': %.3f %.3f, %.3f %.3f" %
883 (numpy.median(dlong[idx1]), rms1Long,
884 numpy.median(dlat[idx1]), rms1Lat))
885 self.log.info(
"Green star offsets'': %.3f %.3f, %.3f %.3f" %
886 (numpy.median(dlong[idx2]), rms2Long,
887 numpy.median(dlat[idx2]), rms2Lat))
888 self.log.info(
"Red star offsets'': %.3f %.3f, %.3f %.3f" %
889 (numpy.median(dlong[idx3]), rms3Long,
890 numpy.median(dlat[idx3]), rms3Lat))
892 self.metadata.add(
"RegisterBlueLongOffsetMedian", numpy.median(dlong[idx1]))
893 self.metadata.add(
"RegisterGreenLongOffsetMedian", numpy.median(dlong[idx2]))
894 self.metadata.add(
"RegisterRedLongOffsetMedian", numpy.median(dlong[idx3]))
895 self.metadata.add(
"RegisterBlueLongOffsetStd", rms1Long)
896 self.metadata.add(
"RegisterGreenLongOffsetStd", rms2Long)
897 self.metadata.add(
"RegisterRedLongOffsetStd", rms3Long)
899 self.metadata.add(
"RegisterBlueLatOffsetMedian", numpy.median(dlat[idx1]))
900 self.metadata.add(
"RegisterGreenLatOffsetMedian", numpy.median(dlat[idx2]))
901 self.metadata.add(
"RegisterRedLatOffsetMedian", numpy.median(dlat[idx3]))
902 self.metadata.add(
"RegisterBlueLatOffsetStd", rms1Lat)
903 self.metadata.add(
"RegisterGreenLatOffsetStd", rms2Lat)
904 self.metadata.add(
"RegisterRedLatOffsetStd", rms3Lat)
911 self.log.info(
"Subtracting images")
912 subtractRes = self.subtract.subtractExposures(
913 templateExposure=templateExposure,
914 scienceExposure=exposure,
915 candidateList=kernelSources,
916 convolveTemplate=self.config.convolveTemplate,
917 doWarping=
not self.config.doUseRegister
919 subtractedExposure = subtractRes.subtractedExposure
921 if self.config.doDetection:
922 self.log.info(
"Computing diffim PSF")
925 if not subtractedExposure.hasPsf():
926 if self.config.convolveTemplate:
927 subtractedExposure.setPsf(exposure.getPsf())
929 subtractedExposure.setPsf(templateExposure.getPsf())
936 if self.config.doDecorrelation
and self.config.doSubtract:
938 if preConvPsf
is not None:
939 preConvKernel = preConvPsf.getLocalKernel()
940 decorrResult = self.decorrelate.
run(exposureOrig, subtractRes.warpedExposure,
942 subtractRes.psfMatchingKernel,
943 spatiallyVarying=self.config.doSpatiallyVarying,
944 preConvKernel=preConvKernel,
945 templateMatched=self.config.convolveTemplate)
946 subtractedExposure = decorrResult.correctedExposure
950 if self.config.doDetection:
951 self.log.info(
"Running diaSource detection")
959 if self.config.useScoreImageDetection:
961 self.log.info(
"Detection, diffim rescaling and measurements are on Zogy score image.")
962 detectionExposure = scoreExposure
963 detectOnLikelihood =
True
966 detectionExposure = subtractedExposure
967 detectOnLikelihood =
False
968 if self.config.doPreConvolve:
970 self.log.info(
"Detection, diffim rescaling and measurements are on AL pre-convolved "
971 "difference (likelihood) image.")
972 detectOnLikelihood =
True
974 self.log.info(
"Detection, diffim rescaling and measurements are on "
975 "(proper) difference image.")
978 if self.config.doScaleDiffimVariance:
979 self.log.info(
"Rescaling diffim variance")
980 diffimVarFactor = self.scaleVariance.
run(detectionExposure.getMaskedImage())
981 self.log.info(
"Diffim variance scaling factor: %.2f" % diffimVarFactor)
982 self.metadata.add(
"scaleDiffimVarianceFactor", diffimVarFactor)
985 mask = detectionExposure.getMaskedImage().getMask()
986 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
988 table = afwTable.SourceTable.make(self.schema, idFactory)
989 table.setMetadata(self.algMetadata)
990 results = self.detection.
run(
992 exposure=detectionExposure,
993 doSmooth=
not detectOnLikelihood
996 if self.config.doMerge:
997 fpSet = results.fpSets.positive
998 fpSet.merge(results.fpSets.negative, self.config.growFootprint,
999 self.config.growFootprint,
False)
1000 diaSources = afwTable.SourceCatalog(table)
1001 fpSet.makeSources(diaSources)
1002 self.log.info(
"Merging detections into %d sources" % (len(diaSources)))
1004 diaSources = results.sources
1006 if self.config.doMeasurement:
1007 newDipoleFitting = self.config.doDipoleFitting
1008 self.log.info(
"Running diaSource measurement: newDipoleFitting=%r", newDipoleFitting)
1009 if not newDipoleFitting:
1011 self.measurement.
run(diaSources, detectionExposure)
1014 if self.config.doSubtract
and 'matchedExposure' in subtractRes.getDict():
1015 self.measurement.
run(diaSources, detectionExposure, exposure,
1016 subtractRes.matchedExposure)
1018 self.measurement.
run(diaSources, detectionExposure, exposure)
1019 if self.config.doApCorr:
1020 self.applyApCorr.
run(
1022 apCorrMap=detectionExposure.getInfo().getApCorrMap()
1025 if self.config.doForcedMeasurement:
1028 forcedSources = self.forcedMeasurement.generateMeasCat(
1029 exposure, diaSources, detectionExposure.getWcs())
1030 self.forcedMeasurement.
run(forcedSources, exposure, diaSources, detectionExposure.getWcs())
1031 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
1032 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
1033 "ip_diffim_forced_PsfFlux_instFlux",
True)
1034 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
1035 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
1036 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
1037 "ip_diffim_forced_PsfFlux_area",
True)
1038 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
1039 "ip_diffim_forced_PsfFlux_flag",
True)
1040 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
1041 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
1042 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
1043 "ip_diffim_forced_PsfFlux_flag_edge",
True)
1044 for diaSource, forcedSource
in zip(diaSources, forcedSources):
1045 diaSource.assign(forcedSource, mapper)
1048 if self.config.doMatchSources:
1049 if selectSources
is not None:
1051 matchRadAsec = self.config.diaSourceMatchRadius
1052 matchRadPixel = matchRadAsec/exposure.getWcs().getPixelScale().asArcseconds()
1054 srcMatches = afwTable.matchXy(selectSources, diaSources, matchRadPixel)
1055 srcMatchDict = dict([(srcMatch.second.getId(), srcMatch.first.getId())
for
1056 srcMatch
in srcMatches])
1057 self.log.info(
"Matched %d / %d diaSources to sources" % (len(srcMatchDict),
1060 self.log.warn(
"Src product does not exist; cannot match with diaSources")
1064 refAstromConfig = AstrometryConfig()
1065 refAstromConfig.matcher.maxMatchDistArcSec = matchRadAsec
1066 refAstrometer = AstrometryTask(refAstromConfig)
1067 astromRet = refAstrometer.run(exposure=exposure, sourceCat=diaSources)
1068 refMatches = astromRet.matches
1069 if refMatches
is None:
1070 self.log.warn(
"No diaSource matches with reference catalog")
1073 self.log.info(
"Matched %d / %d diaSources to reference catalog" % (len(refMatches),
1075 refMatchDict = dict([(refMatch.second.getId(), refMatch.first.getId())
for
1076 refMatch
in refMatches])
1079 for diaSource
in diaSources:
1080 sid = diaSource.getId()
1081 if sid
in srcMatchDict:
1082 diaSource.set(
"srcMatchId", srcMatchDict[sid])
1083 if sid
in refMatchDict:
1084 diaSource.set(
"refMatchId", refMatchDict[sid])
1086 if self.config.doAddMetrics
and self.config.doSelectSources:
1087 self.log.info(
"Evaluating metrics and control sample")
1090 for cell
in subtractRes.kernelCellSet.getCellList():
1091 for cand
in cell.begin(
False):
1092 kernelCandList.append(cand)
1095 basisList = kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelList()
1096 nparam = len(kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelParameters())
1099 diffimTools.sourceTableToCandidateList(controlSources,
1100 subtractRes.warpedExposure, exposure,
1101 self.config.subtract.kernel.active,
1102 self.config.subtract.kernel.active.detectionConfig,
1103 self.log, doBuild=
True, basisList=basisList))
1105 KernelCandidateQa.apply(kernelCandList, subtractRes.psfMatchingKernel,
1106 subtractRes.backgroundModel, dof=nparam)
1107 KernelCandidateQa.apply(controlCandList, subtractRes.psfMatchingKernel,
1108 subtractRes.backgroundModel)
1110 if self.config.doDetection:
1111 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids, diaSources)
1113 KernelCandidateQa.aggregate(selectSources, self.metadata, allresids)
1115 self.runDebug(exposure, subtractRes, selectSources, kernelSources, diaSources)
1116 return pipeBase.Struct(
1117 subtractedExposure=subtractedExposure,
1118 scoreExposure=scoreExposure,
1119 warpedExposure=subtractRes.warpedExposure,
1120 matchedExposure=subtractRes.matchedExposure,
1121 subtractRes=subtractRes,
1122 diaSources=diaSources,
1123 selectSources=selectSources
1126 def fitAstrometry(self, templateSources, templateExposure, selectSources):
1127 """Fit the relative astrometry between templateSources and selectSources
1132 Remove this method. It originally fit a new WCS to the template before calling register.run
1133 because our TAN-SIP fitter behaved badly for points far from CRPIX, but that's been fixed.
1134 It remains because a subtask overrides it.
1136 results = self.register.
run(templateSources, templateExposure.getWcs(),
1137 templateExposure.getBBox(), selectSources)
1140 def runDebug(self, exposure, subtractRes, selectSources, kernelSources, diaSources):
1141 """Make debug plots and displays.
1145 Test and update for current debug display and slot names
1155 disp = afwDisplay.getDisplay(frame=lsstDebug.frame)
1156 if not maskTransparency:
1157 maskTransparency = 0
1158 disp.setMaskTransparency(maskTransparency)
1160 if display
and showSubtracted:
1161 disp.mtv(subtractRes.subtractedExposure, title=
"Subtracted image")
1162 mi = subtractRes.subtractedExposure.getMaskedImage()
1163 x0, y0 = mi.getX0(), mi.getY0()
1164 with disp.Buffering():
1165 for s
in diaSources:
1166 x, y = s.getX() - x0, s.getY() - y0
1167 ctype =
"red" if s.get(
"flags_negative")
else "yellow"
1168 if (s.get(
"base_PixelFlags_flag_interpolatedCenter")
1169 or s.get(
"base_PixelFlags_flag_saturatedCenter")
1170 or s.get(
"base_PixelFlags_flag_crCenter")):
1172 elif (s.get(
"base_PixelFlags_flag_interpolated")
1173 or s.get(
"base_PixelFlags_flag_saturated")
1174 or s.get(
"base_PixelFlags_flag_cr")):
1178 disp.dot(ptype, x, y, size=4, ctype=ctype)
1179 lsstDebug.frame += 1
1181 if display
and showPixelResiduals
and selectSources:
1182 nonKernelSources = []
1183 for source
in selectSources:
1184 if source
not in kernelSources:
1185 nonKernelSources.append(source)
1187 diUtils.plotPixelResiduals(exposure,
1188 subtractRes.warpedExposure,
1189 subtractRes.subtractedExposure,
1190 subtractRes.kernelCellSet,
1191 subtractRes.psfMatchingKernel,
1192 subtractRes.backgroundModel,
1194 self.subtract.config.kernel.active.detectionConfig,
1196 diUtils.plotPixelResiduals(exposure,
1197 subtractRes.warpedExposure,
1198 subtractRes.subtractedExposure,
1199 subtractRes.kernelCellSet,
1200 subtractRes.psfMatchingKernel,
1201 subtractRes.backgroundModel,
1203 self.subtract.config.kernel.active.detectionConfig,
1205 if display
and showDiaSources:
1206 flagChecker = SourceFlagChecker(diaSources)
1207 isFlagged = [flagChecker(x)
for x
in diaSources]
1208 isDipole = [x.get(
"ip_diffim_ClassificationDipole_value")
for x
in diaSources]
1209 diUtils.showDiaSources(diaSources, subtractRes.subtractedExposure, isFlagged, isDipole,
1210 frame=lsstDebug.frame)
1211 lsstDebug.frame += 1
1213 if display
and showDipoles:
1214 DipoleAnalysis().displayDipoles(subtractRes.subtractedExposure, diaSources,
1215 frame=lsstDebug.frame)
1216 lsstDebug.frame += 1
1218 def _getConfigName(self):
1219 """Return the name of the config dataset
1221 return "%sDiff_config" % (self.config.coaddName,)
1223 def _getMetadataName(self):
1224 """Return the name of the metadata dataset
1226 return "%sDiff_metadata" % (self.config.coaddName,)
1228 def getSchemaCatalogs(self):
1229 """Return a dict of empty catalogs for each catalog dataset produced by this task."""
1230 return {self.config.coaddName +
"Diff_diaSrc": self.outputSchema}
1233 def _makeArgumentParser(cls):
1234 """Create an argument parser
1236 parser = pipeBase.ArgumentParser(name=cls._DefaultName)
1237 parser.add_id_argument(
"--id",
"calexp", help=
"data ID, e.g. --id visit=12345 ccd=1,2")
1238 parser.add_id_argument(
"--templateId",
"calexp", doMakeDataRefList=
True,
1239 help=
"Template data ID in case of calexp template,"
1240 " e.g. --templateId visit=6789")
1244 class Winter2013ImageDifferenceConfig(ImageDifferenceConfig):
1245 winter2013WcsShift = pexConfig.Field(dtype=float, default=0.0,
1246 doc=
"Shift stars going into RegisterTask by this amount")
1247 winter2013WcsRms = pexConfig.Field(dtype=float, default=0.0,
1248 doc=
"Perturb stars going into RegisterTask by this amount")
1250 def setDefaults(self):
1251 ImageDifferenceConfig.setDefaults(self)
1252 self.getTemplate.retarget(GetCalexpAsTemplateTask)
1255 class Winter2013ImageDifferenceTask(ImageDifferenceTask):
1256 """!Image difference Task used in the Winter 2013 data challege.
1257 Enables testing the effects of registration shifts and scatter.
1259 For use with winter 2013 simulated images:
1260 Use --templateId visit=88868666 for sparse data
1261 --templateId visit=22222200 for dense data (g)
1262 --templateId visit=11111100 for dense data (i)
1264 ConfigClass = Winter2013ImageDifferenceConfig
1265 _DefaultName =
"winter2013ImageDifference"
1267 def __init__(self, **kwargs):
1268 ImageDifferenceTask.__init__(self, **kwargs)
1270 def fitAstrometry(self, templateSources, templateExposure, selectSources):
1271 """Fit the relative astrometry between templateSources and selectSources"""
1272 if self.config.winter2013WcsShift > 0.0:
1274 self.config.winter2013WcsShift)
1275 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1276 for source
in templateSources:
1277 centroid = source.get(cKey)
1278 source.set(cKey, centroid + offset)
1279 elif self.config.winter2013WcsRms > 0.0:
1280 cKey = templateSources[0].getTable().getCentroidSlot().getMeasKey()
1281 for source
in templateSources:
1282 offset =
geom.Extent2D(self.config.winter2013WcsRms*numpy.random.normal(),
1283 self.config.winter2013WcsRms*numpy.random.normal())
1284 centroid = source.get(cKey)
1285 source.set(cKey, centroid + offset)
1287 results = self.register.
run(templateSources, templateExposure.getWcs(),
1288 templateExposure.getBBox(), selectSources)
def run(self, skyInfo, tempExpRefList, imageScalerList, weightList, altMaskList=None, mask=None, supplementaryData=None)