36 from lsst.meas.algorithms import SourceDetectionTask, SingleGaussianPsf, ObjectSizeStarSelectorTask
37 from lsst.ip.diffim import (DipoleAnalysis, SourceFlagChecker, KernelCandidateF, makeKernelBasisList,
38 KernelCandidateQa, DiaCatalogSourceSelectorTask, DiaCatalogSourceSelectorConfig,
39 GetCoaddAsTemplateTask, GetCalexpAsTemplateTask, DipoleFitTask,
40 DecorrelateALKernelSpatialTask, subtractAlgorithmRegistry)
45 FwhmPerSigma = 2*math.sqrt(2*math.log(2))
50 """Config for ImageDifferenceTask 52 doAddCalexpBackground = pexConfig.Field(dtype=bool, default=
True,
53 doc=
"Add background to calexp before processing it. " 54 "Useful as ipDiffim does background matching.")
55 doUseRegister = pexConfig.Field(dtype=bool, default=
True,
56 doc=
"Use image-to-image registration to align template with " 58 doDebugRegister = pexConfig.Field(dtype=bool, default=
False,
59 doc=
"Writing debugging data for doUseRegister")
60 doSelectSources = pexConfig.Field(dtype=bool, default=
True,
61 doc=
"Select stars to use for kernel fitting")
62 doSelectDcrCatalog = pexConfig.Field(dtype=bool, default=
False,
63 doc=
"Select stars of extreme color as part of the control sample")
64 doSelectVariableCatalog = pexConfig.Field(dtype=bool, default=
False,
65 doc=
"Select stars that are variable to be part " 66 "of the control sample")
67 doSubtract = pexConfig.Field(dtype=bool, default=
True, doc=
"Compute subtracted exposure?")
68 doPreConvolve = pexConfig.Field(dtype=bool, default=
True,
69 doc=
"Convolve science image by its PSF before PSF-matching?")
70 useGaussianForPreConvolution = pexConfig.Field(dtype=bool, default=
True,
71 doc=
"Use a simple gaussian PSF model for pre-convolution " 72 "(else use fit PSF)? Ignored if doPreConvolve false.")
73 doDetection = pexConfig.Field(dtype=bool, default=
True, doc=
"Detect sources?")
74 doDecorrelation = pexConfig.Field(dtype=bool, default=
False,
75 doc=
"Perform diffim decorrelation to undo pixel correlation due to A&L " 76 "kernel convolution? If True, also update the diffim PSF.")
77 doMerge = pexConfig.Field(dtype=bool, default=
True,
78 doc=
"Merge positive and negative diaSources with grow radius " 79 "set by growFootprint")
80 doMatchSources = pexConfig.Field(dtype=bool, default=
True,
81 doc=
"Match diaSources with input calexp sources and ref catalog sources")
82 doMeasurement = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure diaSources?")
83 doDipoleFitting = pexConfig.Field(dtype=bool, default=
True, doc=
"Measure dipoles using new algorithm?")
84 doForcedMeasurement = pexConfig.Field(dtype=bool, default=
True,
85 doc=
"Force photometer diaSource locations on PVI?")
86 doWriteSubtractedExp = pexConfig.Field(dtype=bool, default=
True, doc=
"Write difference exposure?")
87 doWriteMatchedExp = pexConfig.Field(dtype=bool, default=
False,
88 doc=
"Write warped and PSF-matched template coadd exposure?")
89 doWriteSources = pexConfig.Field(dtype=bool, default=
True, doc=
"Write sources?")
90 doAddMetrics = pexConfig.Field(dtype=bool, default=
True,
91 doc=
"Add columns to the source table to hold analysis metrics?")
93 coaddName = pexConfig.Field(
94 doc=
"coadd name: typically one of deep, goodSeeing, or dcr",
98 convolveTemplate = pexConfig.Field(
99 doc=
"Which image gets convolved (default = template)",
103 refObjLoader = pexConfig.ConfigurableField(
104 target=LoadIndexedReferenceObjectsTask,
105 doc=
"reference object loader",
107 astrometer = pexConfig.ConfigurableField(
108 target=AstrometryTask,
109 doc=
"astrometry task; used to match sources to reference objects, but not to fit a WCS",
111 sourceSelector = pexConfig.ConfigurableField(
112 target=ObjectSizeStarSelectorTask,
113 doc=
"Source selection algorithm",
115 subtract = subtractAlgorithmRegistry.makeField(
"Subtraction Algorithm", default=
"al")
116 decorrelate = pexConfig.ConfigurableField(
117 target=DecorrelateALKernelSpatialTask,
118 doc=
"Decorrelate effects of A&L kernel convolution on image difference, only if doSubtract is True. " 119 "If this option is enabled, then detection.thresholdValue should be set to 5.0 (rather than the " 122 doSpatiallyVarying = pexConfig.Field(
125 doc=
"If using Zogy or A&L decorrelation, perform these on a grid across the " 126 "image in order to allow for spatial variations" 128 detection = pexConfig.ConfigurableField(
129 target=SourceDetectionTask,
130 doc=
"Low-threshold detection for final measurement",
132 measurement = pexConfig.ConfigurableField(
133 target=DipoleFitTask,
134 doc=
"Enable updated dipole fitting method",
136 forcedMeasurement = pexConfig.ConfigurableField(
137 target=ForcedMeasurementTask,
138 doc=
"Subtask to force photometer PVI at diaSource location.",
140 getTemplate = pexConfig.ConfigurableField(
141 target=GetCoaddAsTemplateTask,
142 doc=
"Subtask to retrieve template exposure and sources",
144 controlStepSize = pexConfig.Field(
145 doc=
"What step size (every Nth one) to select a control sample from the kernelSources",
149 controlRandomSeed = pexConfig.Field(
150 doc=
"Random seed for shuffing the control sample",
154 register = pexConfig.ConfigurableField(
156 doc=
"Task to enable image-to-image image registration (warping)",
158 kernelSourcesFromRef = pexConfig.Field(
159 doc=
"Select sources to measure kernel from reference catalog if True, template if false",
163 templateSipOrder = pexConfig.Field(dtype=int, default=2,
164 doc=
"Sip Order for fitting the Template Wcs " 165 "(default is too high, overfitting)")
167 growFootprint = pexConfig.Field(dtype=int, default=2,
168 doc=
"Grow positive and negative footprints by this amount before merging")
170 diaSourceMatchRadius = pexConfig.Field(dtype=float, default=0.5,
171 doc=
"Match radius (in arcseconds) " 172 "for DiaSource to Source association")
177 self.
subtract[
'al'].kernel.name =
"AL" 178 self.
subtract[
'al'].kernel.active.fitForBackground =
True 179 self.
subtract[
'al'].kernel.active.spatialKernelOrder = 1
180 self.
subtract[
'al'].kernel.active.spatialBgOrder = 0
187 self.
detection.thresholdPolarity =
"both" 189 self.
detection.reEstimateBackground =
False 190 self.
detection.thresholdType =
"pixel_stdev" 196 self.
measurement.algorithms.names.add(
'base_PeakLikelihoodFlux')
200 "id":
"objectId",
"parent":
"parentObjectId",
"coord_ra":
"coord_ra",
"coord_dec":
"coord_dec"}
208 pexConfig.Config.validate(self)
210 raise ValueError(
"Cannot run source measurement without source detection.")
212 raise ValueError(
"Cannot run source merging without source detection.")
214 raise ValueError(
"doUseRegister=True and doSelectSources=False. " 215 "Cannot run RegisterTask without selecting sources.")
218 raise ValueError(
"Mis-matched coaddName and getTemplate.coaddName in the config.")
225 return pipeBase.TaskRunner.getTargetList(parsedCmd, templateIdList=parsedCmd.templateId.idList,
230 """Subtract an image from a template and measure the result 232 ConfigClass = ImageDifferenceConfig
233 RunnerClass = ImageDifferenceTaskRunner
234 _DefaultName =
"imageDifference" 237 """!Construct an ImageDifference Task 239 @param[in] butler Butler object to use in constructing reference object loaders 241 pipeBase.CmdLineTask.__init__(self, **kwargs)
242 self.makeSubtask(
"getTemplate")
244 self.makeSubtask(
"subtract")
246 if self.config.subtract.name ==
'al' and self.config.doDecorrelation:
247 self.makeSubtask(
"decorrelate")
249 if self.config.doUseRegister:
250 self.makeSubtask(
"register")
251 self.
schema = afwTable.SourceTable.makeMinimalSchema()
253 if self.config.doSelectSources:
254 self.makeSubtask(
"sourceSelector")
255 self.makeSubtask(
'refObjLoader', butler=butler)
256 self.makeSubtask(
"astrometer", refObjLoader=self.refObjLoader)
259 if self.config.doDetection:
260 self.makeSubtask(
"detection", schema=self.
schema)
261 if self.config.doMeasurement:
262 self.makeSubtask(
"measurement", schema=self.
schema,
264 if self.config.doForcedMeasurement:
266 "ip_diffim_forced_PsfFlux_instFlux",
"D",
267 "Forced PSF flux measured on the direct image.")
269 "ip_diffim_forced_PsfFlux_instFluxErr",
"D",
270 "Forced PSF flux error measured on the direct image.")
272 "ip_diffim_forced_PsfFlux_area",
"F",
273 "Forced PSF flux effective area of PSF.",
276 "ip_diffim_forced_PsfFlux_flag",
"Flag",
277 "Forced PSF flux general failure flag.")
279 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
"Flag",
280 "Forced PSF flux not enough non-rejected pixels in data to attempt the fit.")
282 "ip_diffim_forced_PsfFlux_flag_edge",
"Flag",
283 "Forced PSF flux object was too close to the edge of the image to use the full PSF model.")
284 self.makeSubtask(
"forcedMeasurement", refSchema=self.
schema)
285 if self.config.doMatchSources:
286 self.
schema.addField(
"refMatchId",
"L",
"unique id of reference catalog match")
287 self.
schema.addField(
"srcMatchId",
"L",
"unique id of source match")
291 """Subtract an image from a template coadd and measure the result 294 - warp template coadd to match WCS of image 295 - PSF match image to warped template 296 - subtract image from PSF-matched, warped template 297 - persist difference image 301 @param sensorRef: sensor-level butler data reference, used for the following data products: 307 - self.config.coaddName + "Coadd_skyMap" 308 - self.config.coaddName + "Coadd" 309 Input or output, depending on config: 310 - self.config.coaddName + "Diff_subtractedExp" 311 Output, depending on config: 312 - self.config.coaddName + "Diff_matchedExp" 313 - self.config.coaddName + "Diff_src" 315 @return pipe_base Struct containing these fields: 316 - subtractedExposure: exposure after subtracting template; 317 the unpersisted version if subtraction not run but detection run 318 None if neither subtraction nor detection run (i.e. nothing useful done) 319 - subtractRes: results of subtraction task; None if subtraction not run 320 - sources: detected and possibly measured sources; None if detection not run 322 self.log.info(
"Processing %s" % (sensorRef.dataId))
325 subtractedExposure =
None 329 controlSources =
None 335 expBits = sensorRef.get(
"ccdExposureId_bits")
336 expId = int(sensorRef.get(
"ccdExposureId"))
337 idFactory = afwTable.IdFactory.makeSource(expId, 64 - expBits)
340 exposure = sensorRef.get(
"calexp", immediate=
True)
341 if self.config.doAddCalexpBackground:
342 mi = exposure.getMaskedImage()
343 mi += sensorRef.get(
"calexpBackground").getImage()
344 if not exposure.hasPsf():
345 raise pipeBase.TaskError(
"Exposure has no psf")
346 sciencePsf = exposure.getPsf()
348 subtractedExposureName = self.config.coaddName +
"Diff_differenceExp" 349 templateExposure =
None 350 templateSources =
None 351 if self.config.doSubtract:
352 template = self.getTemplate.run(exposure, sensorRef, templateIdList=templateIdList)
353 templateExposure = template.exposure
354 templateSources = template.sources
356 if self.config.subtract.name ==
'zogy':
357 subtractRes = self.subtract.subtractExposures(templateExposure, exposure,
359 spatiallyVarying=self.config.doSpatiallyVarying,
360 doPreConvolve=self.config.doPreConvolve)
361 subtractedExposure = subtractRes.subtractedExposure
363 elif self.config.subtract.name ==
'al':
365 scienceSigmaOrig = sciencePsf.computeShape().getDeterminantRadius()
368 templateSigma = templateExposure.getPsf().computeShape().getDeterminantRadius()
375 if self.config.doPreConvolve:
376 convControl = afwMath.ConvolutionControl()
378 srcMI = exposure.getMaskedImage()
379 destMI = srcMI.Factory(srcMI.getDimensions())
381 if self.config.useGaussianForPreConvolution:
383 kWidth, kHeight = sciencePsf.getLocalKernel().getDimensions()
388 afwMath.convolve(destMI, srcMI, preConvPsf.getLocalKernel(), convControl)
389 exposure.setMaskedImage(destMI)
390 scienceSigmaPost = scienceSigmaOrig*math.sqrt(2)
392 scienceSigmaPost = scienceSigmaOrig
395 if self.config.doSelectSources:
396 if not sensorRef.datasetExists(
"src"):
397 self.log.warn(
"Src product does not exist; running detection, measurement, selection")
399 selectSources = self.subtract.getSelectSources(
401 sigma=scienceSigmaPost,
402 doSmooth=
not self.doPreConvolve,
406 self.log.info(
"Source selection via src product")
408 selectSources = sensorRef.get(
"src")
411 nparam = len(makeKernelBasisList(self.subtract.config.kernel.active,
412 referenceFwhmPix=scienceSigmaPost*FwhmPerSigma,
413 targetFwhmPix=templateSigma*FwhmPerSigma))
415 if self.config.doAddMetrics:
417 kcQa = KernelCandidateQa(nparam)
418 selectSources = kcQa.addToSchema(selectSources)
420 if self.config.kernelSourcesFromRef:
422 astromRet = self.astrometer.loadAndMatch(exposure=exposure, sourceCat=selectSources)
423 matches = astromRet.matches
424 elif templateSources:
426 mc = afwTable.MatchControl()
427 mc.findOnlyClosest =
False 428 matches = afwTable.matchRaDec(templateSources, selectSources, 1.0*afwGeom.arcseconds,
431 raise RuntimeError(
"doSelectSources=True and kernelSourcesFromRef=False," 432 "but template sources not available. Cannot match science " 433 "sources with template sources. Run process* on data from " 434 "which templates are built.")
436 kernelSources = self.sourceSelector.run(selectSources, exposure=exposure,
437 matches=matches).sourceCat
439 random.shuffle(kernelSources, random.random)
440 controlSources = kernelSources[::self.config.controlStepSize]
441 kernelSources = [k
for i, k
in enumerate(kernelSources)
442 if i % self.config.controlStepSize]
444 if self.config.doSelectDcrCatalog:
445 redSelector = DiaCatalogSourceSelectorTask(
446 DiaCatalogSourceSelectorConfig(grMin=self.sourceSelector.config.grMax,
448 redSources = redSelector.selectStars(exposure, selectSources, matches=matches).starCat
449 controlSources.extend(redSources)
451 blueSelector = DiaCatalogSourceSelectorTask(
452 DiaCatalogSourceSelectorConfig(grMin=-99.999,
453 grMax=self.sourceSelector.config.grMin))
454 blueSources = blueSelector.selectStars(exposure, selectSources,
455 matches=matches).starCat
456 controlSources.extend(blueSources)
458 if self.config.doSelectVariableCatalog:
459 varSelector = DiaCatalogSourceSelectorTask(
460 DiaCatalogSourceSelectorConfig(includeVariable=
True))
461 varSources = varSelector.selectStars(exposure, selectSources, matches=matches).starCat
462 controlSources.extend(varSources)
464 self.log.info(
"Selected %d / %d sources for Psf matching (%d for control sample)" 465 % (len(kernelSources), len(selectSources), len(controlSources)))
467 if self.config.doUseRegister:
468 self.log.info(
"Registering images")
470 if templateSources
is None:
473 templateSources = self.subtract.getSelectSources(
482 wcsResults = self.
fitAstrometry(templateSources, templateExposure, selectSources)
483 warpedExp = self.register.warpExposure(templateExposure, wcsResults.wcs,
484 exposure.getWcs(), exposure.getBBox())
485 templateExposure = warpedExp
490 if self.config.doDebugRegister:
492 srcToMatch = {x.second.getId(): x.first
for x
in matches}
494 refCoordKey = wcsResults.matches[0].first.getTable().getCoordKey()
495 inCentroidKey = wcsResults.matches[0].second.getTable().getCentroidKey()
496 sids = [m.first.getId()
for m
in wcsResults.matches]
497 positions = [m.first.get(refCoordKey)
for m
in wcsResults.matches]
498 residuals = [m.first.get(refCoordKey).getOffsetFrom(wcsResults.wcs.pixelToSky(
499 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
500 allresids = dict(zip(sids, zip(positions, residuals)))
502 cresiduals = [m.first.get(refCoordKey).getTangentPlaneOffset(
503 wcsResults.wcs.pixelToSky(
504 m.second.get(inCentroidKey)))
for m
in wcsResults.matches]
505 colors = numpy.array([-2.5*numpy.log10(srcToMatch[x].get(
"g")) +
506 2.5*numpy.log10(srcToMatch[x].get(
"r")) 507 for x
in sids
if x
in srcToMatch.keys()])
508 dlong = numpy.array([r[0].asArcseconds()
for s, r
in zip(sids, cresiduals)
509 if s
in srcToMatch.keys()])
510 dlat = numpy.array([r[1].asArcseconds()
for s, r
in zip(sids, cresiduals)
511 if s
in srcToMatch.keys()])
512 idx1 = numpy.where(colors < self.sourceSelector.config.grMin)
513 idx2 = numpy.where((colors >= self.sourceSelector.config.grMin) &
514 (colors <= self.sourceSelector.config.grMax))
515 idx3 = numpy.where(colors > self.sourceSelector.config.grMax)
516 rms1Long = IqrToSigma*(
517 (numpy.percentile(dlong[idx1], 75) - numpy.percentile(dlong[idx1], 25)))
518 rms1Lat = IqrToSigma*(numpy.percentile(dlat[idx1], 75) -
519 numpy.percentile(dlat[idx1], 25))
520 rms2Long = IqrToSigma*(
521 (numpy.percentile(dlong[idx2], 75) - numpy.percentile(dlong[idx2], 25)))
522 rms2Lat = IqrToSigma*(numpy.percentile(dlat[idx2], 75) -
523 numpy.percentile(dlat[idx2], 25))
524 rms3Long = IqrToSigma*(
525 (numpy.percentile(dlong[idx3], 75) - numpy.percentile(dlong[idx3], 25)))
526 rms3Lat = IqrToSigma*(numpy.percentile(dlat[idx3], 75) -
527 numpy.percentile(dlat[idx3], 25))
528 self.log.info(
"Blue star offsets'': %.3f %.3f, %.3f %.3f" %
529 (numpy.median(dlong[idx1]), rms1Long,
530 numpy.median(dlat[idx1]), rms1Lat))
531 self.log.info(
"Green star offsets'': %.3f %.3f, %.3f %.3f" %
532 (numpy.median(dlong[idx2]), rms2Long,
533 numpy.median(dlat[idx2]), rms2Lat))
534 self.log.info(
"Red star offsets'': %.3f %.3f, %.3f %.3f" %
535 (numpy.median(dlong[idx3]), rms3Long,
536 numpy.median(dlat[idx3]), rms3Lat))
538 self.metadata.add(
"RegisterBlueLongOffsetMedian", numpy.median(dlong[idx1]))
539 self.metadata.add(
"RegisterGreenLongOffsetMedian", numpy.median(dlong[idx2]))
540 self.metadata.add(
"RegisterRedLongOffsetMedian", numpy.median(dlong[idx3]))
541 self.metadata.add(
"RegisterBlueLongOffsetStd", rms1Long)
542 self.metadata.add(
"RegisterGreenLongOffsetStd", rms2Long)
543 self.metadata.add(
"RegisterRedLongOffsetStd", rms3Long)
545 self.metadata.add(
"RegisterBlueLatOffsetMedian", numpy.median(dlat[idx1]))
546 self.metadata.add(
"RegisterGreenLatOffsetMedian", numpy.median(dlat[idx2]))
547 self.metadata.add(
"RegisterRedLatOffsetMedian", numpy.median(dlat[idx3]))
548 self.metadata.add(
"RegisterBlueLatOffsetStd", rms1Lat)
549 self.metadata.add(
"RegisterGreenLatOffsetStd", rms2Lat)
550 self.metadata.add(
"RegisterRedLatOffsetStd", rms3Lat)
557 self.log.info(
"Subtracting images")
558 subtractRes = self.subtract.subtractExposures(
559 templateExposure=templateExposure,
560 scienceExposure=exposure,
561 candidateList=kernelSources,
562 convolveTemplate=self.config.convolveTemplate,
563 doWarping=
not self.config.doUseRegister
565 subtractedExposure = subtractRes.subtractedExposure
567 if self.config.doWriteMatchedExp:
568 sensorRef.put(subtractRes.matchedExposure, self.config.coaddName +
"Diff_matchedExp")
570 if self.config.doDetection:
571 self.log.info(
"Computing diffim PSF")
572 if subtractedExposure
is None:
573 subtractedExposure = sensorRef.get(subtractedExposureName)
576 if not subtractedExposure.hasPsf():
577 if self.config.convolveTemplate:
578 subtractedExposure.setPsf(exposure.getPsf())
580 if templateExposure
is None:
581 template = self.getTemplate.run(exposure, sensorRef,
582 templateIdList=templateIdList)
583 subtractedExposure.setPsf(template.exposure.getPsf())
588 if self.config.doDecorrelation
and self.config.doSubtract:
590 if preConvPsf
is not None:
591 preConvKernel = preConvPsf.getLocalKernel()
592 decorrResult = self.decorrelate.run(exposure, templateExposure,
594 subtractRes.psfMatchingKernel,
595 spatiallyVarying=self.config.doSpatiallyVarying,
596 preConvKernel=preConvKernel)
597 subtractedExposure = decorrResult.correctedExposure
601 if self.config.doDetection:
602 self.log.info(
"Running diaSource detection")
604 mask = subtractedExposure.getMaskedImage().getMask()
605 mask &= ~(mask.getPlaneBitMask(
"DETECTED") | mask.getPlaneBitMask(
"DETECTED_NEGATIVE"))
607 table = afwTable.SourceTable.make(self.
schema, idFactory)
609 results = self.detection.makeSourceCatalog(
611 exposure=subtractedExposure,
612 doSmooth=
not self.config.doPreConvolve
615 if self.config.doMerge:
616 fpSet = results.fpSets.positive
617 fpSet.merge(results.fpSets.negative, self.config.growFootprint,
618 self.config.growFootprint,
False)
619 diaSources = afwTable.SourceCatalog(table)
620 fpSet.makeSources(diaSources)
621 self.log.info(
"Merging detections into %d sources" % (len(diaSources)))
623 diaSources = results.sources
625 if self.config.doMeasurement:
626 newDipoleFitting = self.config.doDipoleFitting
627 self.log.info(
"Running diaSource measurement: newDipoleFitting=%r", newDipoleFitting)
628 if not newDipoleFitting:
630 self.measurement.run(diaSources, subtractedExposure)
633 if self.config.doSubtract
and 'matchedExposure' in subtractRes.getDict():
634 self.measurement.run(diaSources, subtractedExposure, exposure,
635 subtractRes.matchedExposure)
637 self.measurement.run(diaSources, subtractedExposure, exposure)
639 if self.config.doForcedMeasurement:
642 forcedSources = self.forcedMeasurement.generateMeasCat(
643 exposure, diaSources, subtractedExposure.getWcs())
644 self.forcedMeasurement.run(forcedSources, exposure, diaSources, subtractedExposure.getWcs())
645 mapper = afwTable.SchemaMapper(forcedSources.schema, diaSources.schema)
646 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFlux")[0],
647 "ip_diffim_forced_PsfFlux_instFlux",
True)
648 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_instFluxErr")[0],
649 "ip_diffim_forced_PsfFlux_instFluxErr",
True)
650 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_area")[0],
651 "ip_diffim_forced_PsfFlux_area",
True)
652 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag")[0],
653 "ip_diffim_forced_PsfFlux_flag",
True)
654 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_noGoodPixels")[0],
655 "ip_diffim_forced_PsfFlux_flag_noGoodPixels",
True)
656 mapper.addMapping(forcedSources.schema.find(
"base_PsfFlux_flag_edge")[0],
657 "ip_diffim_forced_PsfFlux_flag_edge",
True)
658 for diaSource, forcedSource
in zip(diaSources, forcedSources):
659 diaSource.assign(forcedSource, mapper)
662 if self.config.doMatchSources:
663 if sensorRef.datasetExists(
"src"):
665 matchRadAsec = self.config.diaSourceMatchRadius
666 matchRadPixel = matchRadAsec/exposure.getWcs().pixelScale().asArcseconds()
668 srcMatches = afwTable.matchXy(sensorRef.get(
"src"), diaSources, matchRadPixel)
669 srcMatchDict = dict([(srcMatch.second.getId(), srcMatch.first.getId())
for 670 srcMatch
in srcMatches])
671 self.log.info(
"Matched %d / %d diaSources to sources" % (len(srcMatchDict),
674 self.log.warn(
"Src product does not exist; cannot match with diaSources")
678 refAstromConfig = AstrometryConfig()
679 refAstromConfig.matcher.maxMatchDistArcSec = matchRadAsec
680 refAstrometer = AstrometryTask(refAstromConfig)
681 astromRet = refAstrometer.run(exposure=exposure, sourceCat=diaSources)
682 refMatches = astromRet.matches
683 if refMatches
is None:
684 self.log.warn(
"No diaSource matches with reference catalog")
687 self.log.info(
"Matched %d / %d diaSources to reference catalog" % (len(refMatches),
689 refMatchDict = dict([(refMatch.second.getId(), refMatch.first.getId())
for 690 refMatch
in refMatches])
693 for diaSource
in diaSources:
694 sid = diaSource.getId()
695 if sid
in srcMatchDict:
696 diaSource.set(
"srcMatchId", srcMatchDict[sid])
697 if sid
in refMatchDict:
698 diaSource.set(
"refMatchId", refMatchDict[sid])
700 if diaSources
is not None and self.config.doWriteSources:
701 sensorRef.put(diaSources, self.config.coaddName +
"Diff_diaSrc")
703 if self.config.doAddMetrics
and self.config.doSelectSources:
704 self.log.info(
"Evaluating metrics and control sample")
707 for cell
in subtractRes.kernelCellSet.getCellList():
708 for cand
in cell.begin(
False):
709 kernelCandList.append(cand)
712 basisList = kernelCandList[0].getKernel(KernelCandidateF.ORIG).getKernelList()
715 diffimTools.sourceTableToCandidateList(controlSources,
716 subtractRes.warpedExposure, exposure,
717 self.config.subtract.kernel.active,
718 self.config.subtract.kernel.active.detectionConfig,
719 self.log, doBuild=
True, basisList=basisList))
721 kcQa.apply(kernelCandList, subtractRes.psfMatchingKernel, subtractRes.backgroundModel,
723 kcQa.apply(controlCandList, subtractRes.psfMatchingKernel, subtractRes.backgroundModel)
725 if self.config.doDetection:
726 kcQa.aggregate(selectSources, self.metadata, allresids, diaSources)
728 kcQa.aggregate(selectSources, self.metadata, allresids)
730 sensorRef.put(selectSources, self.config.coaddName +
"Diff_kernelSrc")
732 if self.config.doWriteSubtractedExp:
733 sensorRef.put(subtractedExposure, subtractedExposureName)
735 self.
runDebug(exposure, subtractRes, selectSources, kernelSources, diaSources)
736 return pipeBase.Struct(
737 subtractedExposure=subtractedExposure,
738 subtractRes=subtractRes,
743 """Fit the relative astrometry between templateSources and selectSources 745 @todo remove this method. It originally fit a new WCS to the template before calling register.run 746 because our TAN-SIP fitter behaved badly for points far from CRPIX, but that's been fixed. 747 It remains because a subtask overrides it. 749 results = self.register.run(templateSources, templateExposure.getWcs(),
750 templateExposure.getBBox(), selectSources)
753 def runDebug(self, exposure, subtractRes, selectSources, kernelSources, diaSources):
754 """@todo Test and update for current debug display and slot names 764 disp = afwDisplay.getDisplay(frame=lsstDebug.frame)
765 if not maskTransparency:
767 disp.setMaskTransparency(maskTransparency)
769 if display
and showSubtracted:
770 disp.mtv(subtractRes.subtractedExposure, title=
"Subtracted image")
771 mi = subtractRes.subtractedExposure.getMaskedImage()
772 x0, y0 = mi.getX0(), mi.getY0()
773 with disp.Buffering():
775 x, y = s.getX() - x0, s.getY() - y0
776 ctype =
"red" if s.get(
"flags_negative")
else "yellow" 777 if (s.get(
"base_PixelFlags_flag_interpolatedCenter")
or 778 s.get(
"base_PixelFlags_flag_saturatedCenter")
or 779 s.get(
"base_PixelFlags_flag_crCenter")):
781 elif (s.get(
"base_PixelFlags_flag_interpolated")
or 782 s.get(
"base_PixelFlags_flag_saturated")
or 783 s.get(
"base_PixelFlags_flag_cr")):
787 disp.dot(ptype, x, y, size=4, ctype=ctype)
790 if display
and showPixelResiduals
and selectSources:
791 nonKernelSources = []
792 for source
in selectSources:
793 if source
not in kernelSources:
794 nonKernelSources.append(source)
796 diUtils.plotPixelResiduals(exposure,
797 subtractRes.warpedExposure,
798 subtractRes.subtractedExposure,
799 subtractRes.kernelCellSet,
800 subtractRes.psfMatchingKernel,
801 subtractRes.backgroundModel,
803 self.subtract.config.kernel.active.detectionConfig,
805 diUtils.plotPixelResiduals(exposure,
806 subtractRes.warpedExposure,
807 subtractRes.subtractedExposure,
808 subtractRes.kernelCellSet,
809 subtractRes.psfMatchingKernel,
810 subtractRes.backgroundModel,
812 self.subtract.config.kernel.active.detectionConfig,
814 if display
and showDiaSources:
815 flagChecker = SourceFlagChecker(diaSources)
816 isFlagged = [flagChecker(x)
for x
in diaSources]
817 isDipole = [x.get(
"ip_diffim_ClassificationDipole_value")
for x
in diaSources]
818 diUtils.showDiaSources(diaSources, subtractRes.subtractedExposure, isFlagged, isDipole,
819 frame=lsstDebug.frame)
822 if display
and showDipoles:
823 DipoleAnalysis().displayDipoles(subtractRes.subtractedExposure, diaSources,
824 frame=lsstDebug.frame)
827 def _getConfigName(self):
828 """Return the name of the config dataset 830 return "%sDiff_config" % (self.config.coaddName,)
832 def _getMetadataName(self):
833 """Return the name of the metadata dataset 835 return "%sDiff_metadata" % (self.config.coaddName,)
838 """Return a dict of empty catalogs for each catalog dataset produced by this task.""" 839 diaSrc = afwTable.SourceCatalog(self.
schema)
841 return {self.config.coaddName +
"Diff_diaSrc": diaSrc}
844 def _makeArgumentParser(cls):
845 """Create an argument parser 848 parser.add_id_argument(
"--id",
"calexp", help=
"data ID, e.g. --id visit=12345 ccd=1,2")
849 parser.add_id_argument(
"--templateId",
"calexp", doMakeDataRefList=
True,
850 help=
"Template data ID in case of calexp template," 851 " e.g. --templateId visit=6789")
856 winter2013WcsShift = pexConfig.Field(dtype=float, default=0.0,
857 doc=
"Shift stars going into RegisterTask by this amount")
858 winter2013WcsRms = pexConfig.Field(dtype=float, default=0.0,
859 doc=
"Perturb stars going into RegisterTask by this amount")
862 ImageDifferenceConfig.setDefaults(self)
867 """!Image difference Task used in the Winter 2013 data challege. 868 Enables testing the effects of registration shifts and scatter. 870 For use with winter 2013 simulated images: 871 Use --templateId visit=88868666 for sparse data 872 --templateId visit=22222200 for dense data (g) 873 --templateId visit=11111100 for dense data (i) 875 ConfigClass = Winter2013ImageDifferenceConfig
876 _DefaultName =
"winter2013ImageDifference" 879 ImageDifferenceTask.__init__(self, **kwargs)
882 """Fit the relative astrometry between templateSources and selectSources""" 883 if self.config.winter2013WcsShift > 0.0:
884 offset = afwGeom.Extent2D(self.config.winter2013WcsShift,
885 self.config.winter2013WcsShift)
886 cKey = templateSources[0].getTable().getCentroidKey()
887 for source
in templateSources:
888 centroid = source.get(cKey)
889 source.set(cKey, centroid + offset)
890 elif self.config.winter2013WcsRms > 0.0:
891 cKey = templateSources[0].getTable().getCentroidKey()
892 for source
in templateSources:
893 offset = afwGeom.Extent2D(self.config.winter2013WcsRms*numpy.random.normal(),
894 self.config.winter2013WcsRms*numpy.random.normal())
895 centroid = source.get(cKey)
896 source.set(cKey, centroid + offset)
898 results = self.register.run(templateSources, templateExposure.getWcs(),
899 templateExposure.getBBox(), selectSources)
def __init__(self, butler=None, kwargs)
Construct an ImageDifference Task.
def fitAstrometry(self, templateSources, templateExposure, selectSources)
def runDataRef(self, sensorRef, templateIdList=None)
def __init__(self, kwargs)
def getSchemaCatalogs(self)
def runDebug(self, exposure, subtractRes, selectSources, kernelSources, diaSources)
Image difference Task used in the Winter 2013 data challege.
def fitAstrometry(self, templateSources, templateExposure, selectSources)
def getTargetList(parsedCmd, kwargs)