37 starSelectorRegistry, PsfAttributes, SingleGaussianPsf
38 from lsst.ip.diffim import ImagePsfMatchTask, DipoleMeasurementTask, DipoleAnalysis, \
39 SourceFlagChecker, KernelCandidateF, cast_KernelCandidateF, makeKernelBasisList, \
40 KernelCandidateQa, DiaCatalogSourceSelector, DiaCatalogSourceSelectorConfig
43 FwhmPerSigma = 2 * math.sqrt(2 * math.log(2))
47 """Config for ImageDifferenceTask
49 doAddCalexpBackground = pexConfig.Field(dtype=bool, default=
True,
50 doc=
"Add background to calexp before processing it. Useful as ipDiffim does background matching.")
51 doUseRegister = pexConfig.Field(dtype=bool, default=
True,
52 doc=
"Use image-to-image registration to align template with science image")
53 doDebugRegister = pexConfig.Field(dtype=bool, default=
False,
54 doc=
"Writing debugging data for doUseRegister")
55 doSelectSources = pexConfig.Field(dtype=bool, default=
True,
56 doc=
"Select stars to use for kernel fitting")
57 doSelectDcrCatalog = pexConfig.Field(dtype=bool, default=
False,
58 doc=
"Select stars of extreme color as part of the control sample")
59 doSelectVariableCatalog = pexConfig.Field(dtype=bool, default=
False,
60 doc=
"Select stars that are variable to be part of the control sample")
61 doSubtract = pexConfig.Field(dtype=bool, default=
True, doc=
"Compute subtracted exposure?")
62 doPreConvolve = pexConfig.Field(dtype=bool, default=
True,
63 doc=
"Convolve science image by its PSF before PSF-matching?")
64 useGaussianForPreConvolution = pexConfig.Field(dtype=bool, default=
True,
65 doc=
"Use a simple gaussian PSF model for pre-convolution (else use fit PSF)? "
66 "Ignored if doPreConvolve false.")
67 doDetection = pexConfig.Field(dtype=bool, default=
True, doc=
"Detect sources?")
68 doMerge = pexConfig.Field(dtype=bool, default=
True,
69 doc=
"Merge positive and negative diaSources with grow radius set by growFootprint")
70 doMatchSources = pexConfig.Field(dtype=bool, default=
True,
71 doc=
"Match diaSources with input calexp sources and ref catalog sources")
72 doMeasurement = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure diaSources?")
73 doWriteSubtractedExp = pexConfig.Field(dtype=bool, default=
True, doc=
"Write difference exposure?")
74 doWriteMatchedExp = pexConfig.Field(dtype=bool, default=
False,
75 doc=
"Write warped and PSF-matched template coadd exposure?")
76 doWriteSources = pexConfig.Field(dtype=bool, default=
True, doc=
"Write sources?")
77 doAddMetrics = pexConfig.Field(dtype=bool, default=
True,
78 doc=
"Add columns to the source table to hold analysis metrics?")
80 coaddName = pexConfig.Field(
81 doc=
"coadd name: typically one of deep or goodSeeing",
85 convolveTemplate = pexConfig.Field(
86 doc=
"Which image gets convolved (default = template)",
90 sourceSelector = starSelectorRegistry.makeField(
"Source selection algorithm", default=
"diacatalog")
91 subtract = pexConfig.ConfigurableField(
92 target=ImagePsfMatchTask,
93 doc=
"Warp and PSF match template to exposure, then subtract",
95 detection = pexConfig.ConfigurableField(
96 target=SourceDetectionTask,
97 doc=
"Low-threshold detection for final measurement",
99 dipoleMeasurement = pexConfig.ConfigurableField(
100 target=DipoleMeasurementTask,
101 doc=
"Final source measurement on low-threshold detections; dipole fitting enabled.",
103 measurement = pexConfig.ConfigurableField(
104 target=SourceMeasurementTask,
105 doc=
"Final source measurement on low-threshold detections; dipole fitting NOT enabled",
107 controlStepSize = pexConfig.Field(
108 doc=
"What step size (every Nth one) to select a control sample from the kernelSources",
112 controlRandomSeed = pexConfig.Field(
113 doc=
"Random seed for shuffing the control sample",
117 register = pexConfig.ConfigurableField(
119 doc=
"Task to enable image-to-image image registration (warping)",
122 templateBorderSize = pexConfig.Field(dtype=int, default=10,
123 doc=
"Number of pixels to grow the requested template image to account for warping")
125 templateSipOrder = pexConfig.Field(dtype=int, default=2,
126 doc=
"Sip Order for fitting the Template Wcs (default is too high, overfitting)")
128 growFootprint = pexConfig.Field(dtype=int, default=2,
129 doc=
"Grow positive and negative footprints by this amount before merging")
131 diaSourceMatchRadius = pexConfig.Field(dtype=float, default=0.5,
132 doc=
"Match radius (in arcseconds) for DiaSource to Source association")
134 maxDiaSourcesToMeasure = pexConfig.Field(dtype=int, default=200,
135 doc=
"Do not measure more than this many sources with dipoleMeasurement, use measurement")
139 self.sourceSelector.name =
"diacatalog"
144 self.detection.thresholdPolarity =
"both"
145 self.detection.reEstimateBackground =
False
146 self.detection.thresholdType =
"pixel_stdev"
152 self.measurement.algorithms.names.add(
"flux.peakLikelihood")
153 self.dipoleMeasurement.algorithms.names.add(
"flux.peakLikelihood")
159 pexConfig.Config.validate(self)
161 raise ValueError(
"Cannot run source measurement without source detection.")
163 raise ValueError(
"Cannot run source merging without source detection.")
166 """Subtract an image from a template coadd and measure the result
168 ConfigClass = ImageDifferenceConfig
169 _DefaultName =
"imageDifference"
172 pipeBase.CmdLineTask.__init__(self, **kwargs)
173 self.makeSubtask(
"subtract")
175 if self.config.doUseRegister:
176 self.makeSubtask(
"register")
178 if self.config.doSelectSources:
180 self.
schema = afwTable.SourceTable.makeMinimalSchema()
182 if self.config.doDetection:
183 self.makeSubtask(
"detection", schema=self.
schema)
184 if self.config.doMeasurement:
185 self.makeSubtask(
"dipoleMeasurement", schema=self.
schema, algMetadata=self.
algMetadata)
186 self.makeSubtask(
"measurement", schema=afwTable.SourceTable.makeMinimalSchema(),
188 self.schema.addField(self.dipoleMeasurement._ClassificationFlag,
"F",
189 "probability of being a dipole")
191 if self.config.doMatchSources:
192 self.schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
193 self.schema.addField(
"srcMatchId",
"L",
"unique id of source match")
197 """Subtract an image from a template coadd and measure the result
200 - warp template coadd to match WCS of image
201 - PSF match image to warped template
202 - subtract image from PSF-matched, warped template
203 - persist difference image
207 @param sensorRef: sensor-level butler data reference, used for the following data products:
213 - self.config.coaddName + "Coadd_skyMap"
214 - self.config.coaddName + "Coadd"
215 Input or output, depending on config:
216 - self.config.coaddName + "Diff_subtractedExp"
217 Output, depending on config:
218 - self.config.coaddName + "Diff_matchedExp"
219 - self.config.coaddName + "Diff_src"
221 @return pipe_base Struct containing these fields:
222 - subtractedExposure: exposure after subtracting template;
223 the unpersisted version if subtraction not run but detection run
224 None if neither subtraction nor detection run (i.e. nothing useful done)
225 - subtractRes: results of subtraction task; None if subtraction not run
226 - sources: detected and possibly measured sources; None if detection not run
228 self.log.info(
"Processing %s" % (sensorRef.dataId))
231 subtractedExposure =
None
235 controlSources =
None
241 expBits = sensorRef.get(
"ccdExposureId_bits")
242 expId = long(sensorRef.get(
"ccdExposureId"))
243 idFactory = afwTable.IdFactory.makeSource(expId, 64 - expBits)
246 exposure = sensorRef.get(
"calexp", immediate=
True)
247 if self.config.doAddCalexpBackground:
248 mi = exposure.getMaskedImage()
249 mi += sensorRef.get(
"calexpBackground").getImage()
250 if not exposure.hasPsf():
251 raise pipeBase.TaskError(
"Exposure has no psf")
252 sciencePsf = exposure.getPsf()
254 subtractedExposureName = self.config.coaddName +
"Diff_differenceExp"
255 templateExposure =
None
256 templateSources =
None
257 if self.config.doSubtract:
258 templateExposure, templateSources = self.
getTemplate(exposure, sensorRef)
263 scienceSigmaOrig = psfAttr.computeGaussianWidth(psfAttr.ADAPTIVE_MOMENT)
267 psfAttr = PsfAttributes(templateExposure.getPsf(),
afwGeom.Point2I(ctr))
268 templateSigma = psfAttr.computeGaussianWidth(psfAttr.ADAPTIVE_MOMENT)
274 if self.config.doPreConvolve:
277 srcMI = exposure.getMaskedImage()
278 destMI = srcMI.Factory(srcMI.getDimensions())
280 if self.config.useGaussianForPreConvolution:
282 kWidth, kHeight = sciencePsf.getLocalKernel().getDimensions()
283 preConvPsf = SingleGaussianPsf(kWidth, kHeight, scienceSigmaOrig)
288 exposure.setMaskedImage(destMI)
289 scienceSigmaPost = scienceSigmaOrig * math.sqrt(2)
291 scienceSigmaPost = scienceSigmaOrig
294 if self.config.doSelectSources:
295 if not sensorRef.datasetExists(
"src"):
296 self.log.warn(
"Src product does not exist; running detection, measurement, selection")
298 selectSources = self.subtract.getSelectSources(
300 sigma = scienceSigmaPost,
301 doSmooth =
not self.doPreConvolve,
302 idFactory = idFactory,
305 self.log.info(
"Source selection via src product")
307 selectSources = sensorRef.get(
"src")
311 referenceFwhmPix=scienceSigmaPost * FwhmPerSigma,
312 targetFwhmPix=templateSigma * FwhmPerSigma))
314 if self.config.doAddMetrics:
316 kcQa = KernelCandidateQa(nparam)
317 selectSources = kcQa.addToSchema(selectSources)
319 astrometer = measAstrom.Astrometry(measAstrom.MeasAstromConfig())
320 astromRet = astrometer.useKnownWcs(selectSources, exposure=exposure)
321 matches = astromRet.matches
322 kernelSources = self.sourceSelector.selectSources(exposure, selectSources, matches=matches)
323 random.shuffle(kernelSources, random.random)
324 controlSources = kernelSources[::self.config.controlStepSize]
325 kernelSources = [k
for i,k
in enumerate(kernelSources)
if i % self.config.controlStepSize]
327 if self.config.doSelectDcrCatalog:
328 redSelector = DiaCatalogSourceSelector(
329 DiaCatalogSourceSelectorConfig(grMin=self.sourceSelector.config.grMax, grMax=99.999))
330 redSources = redSelector.selectSources(exposure, selectSources, matches=matches)
331 controlSources.extend(redSources)
333 blueSelector = DiaCatalogSourceSelector(
334 DiaCatalogSourceSelectorConfig(grMin=-99.999, grMax=self.sourceSelector.config.grMin))
335 blueSources = blueSelector.selectSources(exposure, selectSources, matches=matches)
336 controlSources.extend(blueSources)
338 if self.config.doSelectVariableCatalog:
339 varSelector = DiaCatalogSourceSelector(
340 DiaCatalogSourceSelectorConfig(includeVariable=
True))
341 varSources = varSelector.selectSources(exposure, selectSources, matches=matches)
342 controlSources.extend(varSources)
344 self.log.info(
"Selected %d / %d sources for Psf matching (%d for control sample)"
345 % (len(kernelSources), len(selectSources), len(controlSources)))
347 if self.config.doUseRegister:
348 self.log.info(
"Registering images")
350 if templateSources
is None:
353 templateSources = self.subtract.getSelectSources(
362 wcsResults = self.
fitAstrometry(templateSources, templateExposure, selectSources)
363 warpedExp = self.register.warpExposure(templateExposure, wcsResults.wcs,
364 exposure.getWcs(), exposure.getBBox())
365 templateExposure = warpedExp
370 if self.config.doDebugRegister:
372 srcToMatch = {x.second.getId() : x.first
for x
in matches}
374 refCoordKey = wcsResults.matches[0].first.getTable().getCoordKey()
375 inCentroidKey = wcsResults.matches[0].second.getTable().getCentroidKey()
376 sids = [m.first.getId()
for m
in wcsResults.matches]
377 positions = [m.first.get(refCoordKey)
for m
in wcsResults.matches]
378 residuals = [m.first.get(refCoordKey).getOffsetFrom(wcsResults.wcs.pixelToSky(
379 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
380 allresids = dict(zip(sids, zip(positions, residuals)))
382 cresiduals = [m.first.get(refCoordKey).getTangentPlaneOffset(
383 wcsResults.wcs.pixelToSky(
384 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
385 colors = numpy.array([-2.5*numpy.log10(srcToMatch[x].get(
"g"))
386 + 2.5*numpy.log10(srcToMatch[x].get(
"r"))
387 for x
in sids
if x
in srcToMatch.keys()])
388 dlong = numpy.array([r[0].asArcseconds()
for s,r
in zip(sids, cresiduals)
389 if s
in srcToMatch.keys()])
390 dlat = numpy.array([r[1].asArcseconds()
for s,r
in zip(sids, cresiduals)
391 if s
in srcToMatch.keys()])
392 idx1 = numpy.where(colors<self.sourceSelector.config.grMin)
393 idx2 = numpy.where((colors>=self.sourceSelector.config.grMin)&
394 (colors<=self.sourceSelector.config.grMax))
395 idx3 = numpy.where(colors>self.sourceSelector.config.grMax)
396 rms1Long = IqrToSigma*(numpy.percentile(dlong[idx1],75)-numpy.percentile(dlong[idx1],25))
397 rms1Lat = IqrToSigma*(numpy.percentile(dlat[idx1],75)-numpy.percentile(dlat[idx1],25))
398 rms2Long = IqrToSigma*(numpy.percentile(dlong[idx2],75)-numpy.percentile(dlong[idx2],25))
399 rms2Lat = IqrToSigma*(numpy.percentile(dlat[idx2],75)-numpy.percentile(dlat[idx2],25))
400 rms3Long = IqrToSigma*(numpy.percentile(dlong[idx3],75)-numpy.percentile(dlong[idx3],25))
401 rms3Lat = IqrToSigma*(numpy.percentile(dlat[idx3],75)-numpy.percentile(dlat[idx3],25))
402 self.log.info(
"Blue star offsets'': %.3f %.3f, %.3f %.3f" % (numpy.median(dlong[idx1]),
404 numpy.median(dlat[idx1]),
406 self.log.info(
"Green star offsets'': %.3f %.3f, %.3f %.3f" % (numpy.median(dlong[idx2]),
408 numpy.median(dlat[idx2]),
410 self.log.info(
"Red star offsets'': %.3f %.3f, %.3f %.3f" % (numpy.median(dlong[idx3]),
412 numpy.median(dlat[idx3]),
415 self.metadata.add(
"RegisterBlueLongOffsetMedian", numpy.median(dlong[idx1]))
416 self.metadata.add(
"RegisterGreenLongOffsetMedian", numpy.median(dlong[idx2]))
417 self.metadata.add(
"RegisterRedLongOffsetMedian", numpy.median(dlong[idx3]))
418 self.metadata.add(
"RegisterBlueLongOffsetStd", rms1Long)
419 self.metadata.add(
"RegisterGreenLongOffsetStd", rms2Long)
420 self.metadata.add(
"RegisterRedLongOffsetStd", rms3Long)
422 self.metadata.add(
"RegisterBlueLatOffsetMedian", numpy.median(dlat[idx1]))
423 self.metadata.add(
"RegisterGreenLatOffsetMedian", numpy.median(dlat[idx2]))
424 self.metadata.add(
"RegisterRedLatOffsetMedian", numpy.median(dlat[idx3]))
425 self.metadata.add(
"RegisterBlueLatOffsetStd", rms1Lat)
426 self.metadata.add(
"RegisterGreenLatOffsetStd", rms2Lat)
427 self.metadata.add(
"RegisterRedLatOffsetStd", rms3Lat)
434 self.log.info(
"Subtracting images")
435 subtractRes = self.subtract.subtractExposures(
436 templateExposure=templateExposure,
437 scienceExposure=exposure,
438 scienceFwhmPix=scienceSigmaPost * FwhmPerSigma,
439 templateFwhmPix=templateSigma * FwhmPerSigma,
440 candidateList=kernelSources,
441 convolveTemplate=self.config.convolveTemplate,
442 doWarping=
not self.config.doUseRegister
444 subtractedExposure = subtractRes.subtractedExposure
446 if self.config.doWriteMatchedExp:
447 sensorRef.put(subtractRes.matchedExposure, self.config.coaddName +
"Diff_matchedExp")
449 if self.config.doDetection:
450 self.log.info(
"Running diaSource detection")
451 if subtractedExposure
is None:
452 subtractedExposure = sensorRef.get(subtractedExposureName)
455 if not subtractedExposure.hasPsf():
456 if self.config.convolveTemplate:
457 subtractedExposure.setPsf(exposure.getPsf())
459 if templateExposure
is None:
460 templateExposure, templateSources = self.
getTemplate(exposure, sensorRef)
461 subtractedExposure.setPsf(templateExposure.getPsf())
464 mask = subtractedExposure.getMaskedImage().getMask()
465 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
467 table = afwTable.SourceTable.make(self.
schema, idFactory)
469 results = self.detection.makeSourceCatalog(
471 exposure=subtractedExposure,
472 doSmooth=
not self.config.doPreConvolve
475 if self.config.doMerge:
476 fpSet = results.fpSets.positive
477 fpSet.merge(results.fpSets.negative, self.config.growFootprint,
478 self.config.growFootprint,
False)
480 fpSet.makeSources(diaSources)
481 self.log.info(
"Merging detections into %d sources" % (len(diaSources)))
483 diaSources = results.sources
485 if self.config.doMeasurement:
486 if len(diaSources) < self.config.maxDiaSourcesToMeasure:
487 self.log.info(
"Running diaSource dipole measurement")
488 self.dipoleMeasurement.run(subtractedExposure, diaSources)
490 self.log.info(
"Running diaSource measurement")
491 self.measurement.run(subtractedExposure, diaSources)
494 if self.config.doMatchSources:
495 if sensorRef.datasetExists(
"src"):
497 matchRadAsec = self.config.diaSourceMatchRadius
498 matchRadPixel = matchRadAsec / exposure.getWcs().pixelScale().asArcseconds()
500 srcMatches =
afwTable.matchXy(sensorRef.get(
"src"), diaSources, matchRadPixel,
True)
501 srcMatchDict = dict([(srcMatch.second.getId(), srcMatch.first.getId())
for
502 srcMatch
in srcMatches])
503 self.log.info(
"Matched %d / %d diaSources to sources" % (len(srcMatchDict),
506 self.log.warn(
"Src product does not exist; cannot match with diaSources")
510 astrometer = measAstrom.Astrometry(measAstrom.MeasAstromConfig(catalogMatchDist=matchRadAsec))
511 astromRet = astrometer.useKnownWcs(diaSources, exposure=exposure)
512 refMatches = astromRet.matches
513 if refMatches
is None:
514 self.log.warn(
"No diaSource matches with reference catalog")
517 self.log.info(
"Matched %d / %d diaSources to reference catalog" % (len(refMatches),
519 refMatchDict = dict([(refMatch.second.getId(), refMatch.first.getId())
for \
520 refMatch
in refMatches])
523 for diaSource
in diaSources:
524 sid = diaSource.getId()
525 if srcMatchDict.has_key(sid):
526 diaSource.set(
"srcMatchId", srcMatchDict[sid])
527 if refMatchDict.has_key(sid):
528 diaSource.set(
"refMatchId", refMatchDict[sid])
530 if diaSources
is not None and self.config.doWriteSources:
531 sensorRef.put(diaSources, self.config.coaddName +
"Diff_diaSrc")
533 if self.config.doAddMetrics
and self.config.doSelectSources:
534 self.log.info(
"Evaluating metrics and control sample")
537 for cell
in subtractRes.kernelCellSet.getCellList():
538 for cand
in cell.begin(
False):
539 kernelCandList.append(cast_KernelCandidateF(cand))
542 basisList = afwMath.cast_LinearCombinationKernel(
543 kernelCandList[0].getKernel(KernelCandidateF.ORIG)).getKernelList()
546 diffimTools.sourceTableToCandidateList(controlSources,
547 subtractRes.warpedExposure, exposure,
548 self.config.subtract.kernel.active,
549 self.config.subtract.kernel.active.detectionConfig,
550 self.log, doBuild=
True, basisList=basisList)
552 kcQa.apply(kernelCandList, subtractRes.psfMatchingKernel, subtractRes.backgroundModel,
554 kcQa.apply(controlCandList, subtractRes.psfMatchingKernel, subtractRes.backgroundModel)
556 if self.config.doDetection:
557 kcQa.aggregate(selectSources, self.metadata, allresids, diaSources)
559 kcQa.aggregate(selectSources, self.metadata, allresids)
561 sensorRef.put(selectSources, self.config.coaddName +
"Diff_kernelSrc")
563 if self.config.doWriteSubtractedExp:
564 sensorRef.put(subtractedExposure, subtractedExposureName)
566 self.
runDebug(exposure, subtractRes, selectSources, kernelSources, diaSources)
567 return pipeBase.Struct(
568 subtractedExposure=subtractedExposure,
569 subtractRes=subtractRes,
581 """Fit the relative astrometry between templateSources and selectSources"""
582 sipOrder = self.config.templateSipOrder
583 astrometer = measAstrom.Astrometry(measAstrom.MeasAstromConfig(sipOrder=sipOrder))
584 newWcs = astrometer.determineWcs(templateSources, templateExposure).getWcs()
585 results = self.register.run(templateSources, newWcs,
586 templateExposure.getBBox(), selectSources)
589 def runDebug(self, exposure, subtractRes, selectSources, kernelSources, diaSources):
598 if not maskTransparency:
600 ds9.setMaskTransparency(maskTransparency)
602 if display
and showSubtracted:
603 ds9.mtv(subtractRes.subtractedExposure, frame=lsstDebug.frame, title=
"Subtracted image")
604 mi = subtractRes.subtractedExposure.getMaskedImage()
605 x0, y0 = mi.getX0(), mi.getY0()
606 with ds9.Buffering():
608 x, y = s.getX() - x0, s.getY() - y0
609 ctype =
"red" if s.get(
"flags.negative")
else "yellow"
610 if (s.get(
"flags.pixel.interpolated.center")
or s.get(
"flags.pixel.saturated.center")
or
611 s.get(
"flags.pixel.cr.center")):
613 elif (s.get(
"flags.pixel.interpolated.any")
or s.get(
"flags.pixel.saturated.any")
or
614 s.get(
"flags.pixel.cr.any")):
618 ds9.dot(ptype, x, y, size=4, frame=lsstDebug.frame, ctype=ctype)
621 if display
and showPixelResiduals
and selectSources:
623 nonKernelSources = []
624 for source
in selectSources:
625 if not source
in kernelSources:
626 nonKernelSources.append(source)
628 diUtils.plotPixelResiduals(exposure,
629 subtractRes.warpedExposure,
630 subtractRes.subtractedExposure,
631 subtractRes.kernelCellSet,
632 subtractRes.psfMatchingKernel,
633 subtractRes.backgroundModel,
635 self.subtract.config.kernel.active.detectionConfig,
637 diUtils.plotPixelResiduals(exposure,
638 subtractRes.warpedExposure,
639 subtractRes.subtractedExposure,
640 subtractRes.kernelCellSet,
641 subtractRes.psfMatchingKernel,
642 subtractRes.backgroundModel,
644 self.subtract.config.kernel.active.detectionConfig,
646 if display
and showDiaSources:
648 flagChecker = SourceFlagChecker(diaSources)
649 isFlagged = [flagChecker(x)
for x
in diaSources]
650 isDipole = [x.get(
"classification.dipole")
for x
in diaSources]
651 diUtils.showDiaSources(diaSources, subtractRes.subtractedExposure, isFlagged, isDipole,
652 frame=lsstDebug.frame)
655 if display
and showDipoles:
656 DipoleAnalysis().displayDipoles(subtractRes.subtractedExposure, diaSources,
657 frame=lsstDebug.frame)
661 """Return a template coadd exposure that overlaps the exposure
663 @param[in] exposure: exposure
664 @param[in] sensorRef: a Butler data reference that can be used to obtain coadd data
666 @return coaddExposure: a template coadd exposure assembled out of patches
668 skyMap = sensorRef.get(datasetType=self.config.coaddName +
"Coadd_skyMap")
669 expWcs = exposure.getWcs()
671 expBoxD.grow(self.config.templateBorderSize)
672 ctrSkyPos = expWcs.pixelToSky(expBoxD.getCenter())
673 tractInfo = skyMap.findTract(ctrSkyPos)
674 self.log.info(
"Using skyMap tract %s" % (tractInfo.getId(),))
675 skyCorners = [expWcs.pixelToSky(pixPos)
for pixPos
in expBoxD.getCorners()]
676 patchList = tractInfo.findPatchList(skyCorners)
679 raise RuntimeError(
"No suitable tract found")
680 self.log.info(
"Assembling %s coadd patches" % (len(patchList),))
683 coaddWcs = tractInfo.getWcs()
685 for skyPos
in skyCorners:
686 coaddBBox.include(coaddWcs.skyToPixel(skyPos))
688 self.log.info(
"exposure dimensions=%s; coadd dimensions=%s" % \
689 (exposure.getDimensions(), coaddBBox.getDimensions()))
692 coaddExposure = afwImage.ExposureF(coaddBBox, coaddWcs)
693 edgeMask = afwImage.MaskU.getPlaneBitMask(
"EDGE")
694 coaddExposure.getMaskedImage().set(numpy.nan, edgeMask, numpy.nan)
699 for patchInfo
in patchList:
700 patchSubBBox = patchInfo.getOuterBBox()
701 patchSubBBox.clip(coaddBBox)
703 datasetType=self.config.coaddName +
"Coadd_sub",
705 tract=tractInfo.getId(),
706 patch=
"%s,%s" % (patchInfo.getIndex()[0], patchInfo.getIndex()[1]),
708 if patchSubBBox.isEmpty():
709 self.log.info(
"skip tract=%(tract)s; no overlapping pixels" % patchArgDict)
711 if not sensorRef.datasetExists(**patchArgDict):
712 self.log.warn(
"%(datasetType)s, tract=%(tract)s, patch=%(patch)s does not exist" \
717 self.log.info(
"Reading patch %s" % patchArgDict)
718 coaddPatch = sensorRef.get(patchArgDict, immediate=
True)
719 coaddView = afwImage.MaskedImageF(coaddExposure.getMaskedImage(), coaddPatch.getBBox())
720 coaddView <<= coaddPatch.getMaskedImage()
721 if coaddFilter
is None:
722 coaddFilter = coaddPatch.getFilter()
725 if coaddPsf
is None and coaddPatch.hasPsf():
726 coaddPsf = coaddPatch.getPsf()
728 if nPatchesFound == 0:
729 raise RuntimeError(
"No patches found!")
732 raise RuntimeError(
"No coadd Psf found!")
734 coaddExposure.setPsf(coaddPsf)
735 coaddExposure.setFilter(coaddFilter)
736 return coaddExposure, coaddSources
739 """Return the name of the config dataset
741 return "%sDiff_config" % (self.config.coaddName,)
744 """Return the name of the metadata dataset
746 return "%sDiff_metadata" % (self.config.coaddName,)
749 """Return a dict of empty catalogs for each catalog dataset produced by this task."""
752 return {self.config.coaddName +
"Diff_diaSrc": diaSrc}
756 """Create an argument parser
758 parser = pipeBase.ArgumentParser(name=cls._DefaultName)
759 parser.add_id_argument(
"--id",
"calexp", help=
"data ID, e.g. --id visit=12345 ccd=1,2")
763 winter2013TemplateId = pexConfig.Field(dtype=int, default=88868666,
764 doc=
"88868666 for sparse data; 22222200 (g) and 11111100 (i) for dense data")
765 winter2013WcsShift = pexConfig.Field(dtype=float, default=0.0,
766 doc=
"Shift stars going into RegisterTask by this amount")
767 winter2013WcsRms = pexConfig.Field(dtype=float, default=0.0,
768 doc=
"Perturb stars going into RegisterTask by this amount")
771 ConfigClass = Winter2013ImageDifferenceConfig
772 _DefaultName =
"winter2013ImageDifference"
775 ImageDifferenceTask.__init__(self, **kwargs)
778 """Return a template coadd exposure that overlaps the exposure
780 @param[in] exposure: exposure
781 @param[in] sensorRef: a Butler data reference that can be used to obtain coadd data
783 @return coaddExposure: a template coadd exposure assembled out of patches
786 self.log.warn(
"USING WINTER2013 : DEEP CALEXP AS TEMPLATE")
787 templateId = type(sensorRef.dataId)(sensorRef.dataId)
788 templateId[
"visit"] = self.config.winter2013TemplateId
789 template = sensorRef.getButler().get(datasetType=
"calexp", dataId=templateId)
790 if self.config.doAddCalexpBackground:
791 templateBg = sensorRef.getButler().get(datasetType=
"calexpBackground", dataId=templateId)
792 mi = template.getMaskedImage()
794 if not template.hasPsf():
795 raise pipeBase.TaskError(
"Template has no psf")
796 templateSources = sensorRef.getButler().get(datasetType=
"src", dataId=templateId)
797 return template, templateSources
801 """Fit the relative astrometry between templateSources and selectSources"""
802 if self.config.winter2013WcsShift > 0.0:
804 self.config.winter2013WcsShift)
805 cKey = templateSources[0].getTable().getCentroidKey()
806 for source
in templateSources:
807 centroid = source.get(cKey)
808 source.set(cKey, centroid+offset)
809 elif self.config.winter2013WcsRms > 0.0:
810 cKey = templateSources[0].getTable().getCentroidKey()
811 for source
in templateSources:
812 offset =
afwGeom.Extent2D(self.config.winter2013WcsRms*numpy.random.normal(),
813 self.config.winter2013WcsRms*numpy.random.normal())
814 centroid = source.get(cKey)
815 source.set(cKey, centroid+offset)
817 results = self.register.run(templateSources, templateExposure.getWcs(),
818 templateExposure.getBBox(), selectSources)
SourceMatchVector matchXy(SourceCatalog const &cat, double radius, bool symmetric=true)
Class for storing ordered metadata with comments.
Parameters to control convolution.
An integer coordinate rectangle.
Custom catalog class for record/table subclasses that are guaranteed to have an ID, and should generally be sorted by that ID.
void convolve(OutImageT &convolvedImage, InImageT const &inImage, KernelT const &kernel, bool doNormalize, bool doCopyEdge=false)
Old, deprecated version of convolve.
A floating-point coordinate rectangle geometry.