26 import lsst.pex.config 
as pexConfig
 
   30 from lsst.meas.base import SingleFrameMeasurementTask, SingleFrameMeasurementConfig, \
 
   31     SingleFramePluginConfig, SingleFramePlugin
 
   34 __all__ = (
"DipoleMeasurementConfig", 
"DipoleMeasurementTask", 
"DipoleAnalysis", 
"DipoleDeblender",
 
   35            "SourceFlagChecker", 
"ClassificationDipoleConfig", 
"ClassificationDipolePlugin")
 
   39     """Configuration for classification of detected diaSources as dipole or not""" 
   40     minSn = pexConfig.Field(
 
   41         doc=
"Minimum quadrature sum of positive+negative lobe S/N to be considered a dipole",
 
   42         dtype=float, default=np.sqrt(2) * 5.0,
 
   44     maxFluxRatio = pexConfig.Field(
 
   45         doc=
"Maximum flux ratio in either lobe to be considered a dipole",
 
   46         dtype=float, default=0.65
 
   50 @
register(
"ip_diffim_ClassificationDipole")
 
   52     """A plugin to classify whether a diaSource is a dipole. 
   55     ConfigClass = ClassificationDipoleConfig
 
   66     def __init__(self, config, name, schema, metadata):
 
   67         SingleFramePlugin.__init__(self, config, name, schema, metadata)
 
   70                                               doc=
"Set to 1 for dipoles, else 0.")
 
   71         self.
keyFlag = schema.addField(name + 
"_flag", type=
"Flag", doc=
"Set to 1 for any fatal failure.")
 
   75         negFlux = np.abs(measRecord.get(
"ip_diffim_PsfDipoleFlux_neg_instFlux"))
 
   76         negFluxFlag = measRecord.get(
"ip_diffim_PsfDipoleFlux_neg_flag")
 
   77         posFlux = np.abs(measRecord.get(
"ip_diffim_PsfDipoleFlux_pos_instFlux"))
 
   78         posFluxFlag = measRecord.get(
"ip_diffim_PsfDipoleFlux_pos_flag")
 
   80         if negFluxFlag 
or posFluxFlag:
 
   84         totalFlux = negFlux + posFlux
 
   87         passesFluxNeg = (negFlux / totalFlux) < self.
config.maxFluxRatio
 
   88         passesFluxPos = (posFlux / totalFlux) < self.
config.maxFluxRatio
 
   89         if (passesSn 
and passesFluxPos 
and passesFluxNeg):
 
   96     def fail(self, measRecord, error=None):
 
   97         measRecord.set(self.
keyFlag, 
True)
 
  101     """Measurement of detected diaSources as dipoles""" 
  104         SingleFrameMeasurementConfig.setDefaults(self)
 
  109                         "ip_diffim_NaiveDipoleCentroid",
 
  110                         "ip_diffim_NaiveDipoleFlux",
 
  111                         "ip_diffim_PsfDipoleFlux",
 
  112                         "ip_diffim_ClassificationDipole",
 
  115         self.
slots.calibFlux = 
None 
  116         self.
slots.modelFlux = 
None 
  117         self.
slots.gaussianFlux = 
None 
  118         self.
slots.shape = 
None 
  119         self.
slots.centroid = 
"ip_diffim_NaiveDipoleCentroid" 
  124     """Measurement of Sources, specifically ones from difference images, for characterization as dipoles 
  128     sources : 'lsst.afw.table.SourceCatalog' 
  129         Sources that will be measured 
  130     badFlags : `list` of `dict` 
  131         A list of flags that will be used to determine if there was a measurement problem 
  135     The list of badFlags will be used to make a list of keys to check for measurement flags on.  By 
  136     default the centroid keys are added to this list 
  140     This class provides a default configuration for running Source measurement on image differences. 
  144         class DipoleMeasurementConfig(SingleFrameMeasurementConfig): 
  145             "Measurement of detected diaSources as dipoles" 
  146             def setDefaults(self): 
  147                 SingleFrameMeasurementConfig.setDefaults(self) 
  148                 self.plugins = ["base_CircularApertureFlux", 
  152                                 "ip_diffim_NaiveDipoleCentroid", 
  153                                 "ip_diffim_NaiveDipoleFlux", 
  154                                 "ip_diffim_PsfDipoleFlux", 
  155                                 "ip_diffim_ClassificationDipole", 
  157                 self.slots.calibFlux = None 
  158                 self.slots.modelFlux = None 
  159                 self.slots.instFlux = None 
  160                 self.slots.shape = None 
  161                 self.slots.centroid = "ip_diffim_NaiveDipoleCentroid" 
  162                 self.doReplaceWithNoise = False 
  164     These plugins enabled by default allow the user to test the hypothesis that the Source is a dipole. 
  165     This includes a set of measurements derived from intermediate base classes 
  166     DipoleCentroidAlgorithm and DipoleFluxAlgorithm. 
  167     Their respective algorithm control classes are defined in 
  168     DipoleCentroidControl and DipoleFluxControl. 
  169     Each centroid and flux measurement will have _neg (negative) 
  170     and _pos (positive lobe) fields. 
  172     The first set of measurements uses a "naive" alrogithm 
  173     for centroid and flux measurements, implemented in 
  174     NaiveDipoleCentroidControl and NaiveDipoleFluxControl. 
  175     The algorithm uses a naive 3x3 weighted moment around 
  176     the nominal centroids of each peak in the Source Footprint.  These algorithms fill the table fields 
  177     ip_diffim_NaiveDipoleCentroid* and ip_diffim_NaiveDipoleFlux* 
  179     The second set of measurements undertakes a joint-Psf model on the negative 
  180     and positive lobe simultaneously. This fit simultaneously solves for the negative and positive 
  181     lobe centroids and fluxes using non-linear least squares minimization. 
  182     The fields are stored in table elements ip_diffim_PsfDipoleFlux*. 
  184     Because this Task is just a config for SingleFrameMeasurementTask, the same result may be acheived by 
  185     manually editing the config and running SingleFrameMeasurementTask. For example: 
  189         config = SingleFrameMeasurementConfig() 
  190         config.plugins.names = ["base_PsfFlux", 
  191                                 "ip_diffim_PsfDipoleFlux", 
  192                                 "ip_diffim_NaiveDipoleFlux", 
  193                                 "ip_diffim_NaiveDipoleCentroid", 
  194                                 "ip_diffim_ClassificationDipole", 
  195                                 "base_CircularApertureFlux", 
  198         config.slots.calibFlux = None 
  199         config.slots.modelFlux = None 
  200         config.slots.instFlux = None 
  201         config.slots.shape = None 
  202         config.slots.centroid = "ip_diffim_NaiveDipoleCentroid" 
  203         config.doReplaceWithNoise = False 
  205         schema = afwTable.SourceTable.makeMinimalSchema() 
  206         task = SingleFrameMeasurementTask(schema, config=config)- 
  210     The ``lsst.pipe.base.cmdLineTask.CmdLineTask`` command line task interface supports a 
  211     flag-d/--debug to import debug.py from your PYTHONPATH.  The relevant contents of debug.py 
  212     for this Task include: 
  219             di = lsstDebug.getInfo(name) 
  220             if name == "lsst.ip.diffim.dipoleMeasurement": 
  221                 di.display = True                 # enable debug output 
  222                 di.maskTransparency = 90          # display mask transparency 
  223                 di.displayDiaSources = True       # show exposure with dipole results 
  225         lsstDebug.Info = DebugInfo 
  228         config.slots.calibFlux = None 
  229         config.slots.modelFlux = None 
  230         config.slots.gaussianFlux = None 
  231         config.slots.shape = None 
  232         config.slots.centroid = "ip_diffim_NaiveDipoleCentroid" 
  233         config.doReplaceWithNoise = False 
  235     This code is dipoleMeasTask.py in the examples directory, and can be run as e.g. 
  239         examples/dipoleMeasTask.py 
  240         examples/dipoleMeasTask.py --debug 
  241         examples/dipoleMeasTask.py --debug --image /path/to/image.fits 
  245     Start the processing by parsing the command line, where the user has the option of 
  246     enabling debugging output and/or sending their own image for demonstration 
  247     (in case they have not downloaded the afwdata package). 
  251         if __name__ == "__main__": 
  253             parser = argparse.ArgumentParser( 
  254                 description="Demonstrate the use of SourceDetectionTask and DipoleMeasurementTask") 
  255             parser.add_argument('--debug', '-d', action="store_true", help="Load debug.py?", default=False) 
  256             parser.add_argument("--image", "-i", help="User defined image", default=None) 
  257             args = parser.parse_args() 
  261                     debug.lsstDebug.frame = 2 
  262                 except ImportError as e: 
  263                     print(e, file=sys.stderr) 
  266     The processing occurs in the run function.  We first extract an exposure from disk or afwdata, displaying 
  272             exposure = loadData(args.image) 
  274                 afwDisplay.Display(frame=1).mtv(exposure) 
  276     Create a default source schema that we will append fields to as we add more algorithms: 
  280         schema = afwTable.SourceTable.makeMinimalSchema() 
  282     Create the detection and measurement Tasks, with some minor tweaking of their configs: 
  286             # Create the detection task 
  287         config = SourceDetectionTask.ConfigClass() 
  288         config.thresholdPolarity = "both" 
  289         config.background.isNanSafe = True 
  290         config.thresholdValue = 3 
  291         detectionTask = SourceDetectionTask(config=config, schema=schema) 
  292         # And the measurement Task 
  293         config = DipoleMeasurementTask.ConfigClass() 
  294         config.plugins.names.remove('base_SkyCoord') 
  295         algMetadata = dafBase.PropertyList() 
  296         measureTask = DipoleMeasurementTask(schema, algMetadata, config=config) 
  298     Having fully initialied the schema, we create a Source table from it: 
  302         # Create the output table 
  303         tab = afwTable.SourceTable.make(schema) 
  310         results = detectionTask.run(tab, exposure) 
  312     Because we are looking for dipoles, we need to merge the positive and negative detections: 
  316         # Merge the positve and negative sources 
  317         fpSet = results.fpSets.positive 
  319         fpSet.merge(results.fpSets.negative, growFootprint, growFootprint, False) 
  320         diaSources = afwTable.SourceCatalog(tab) 
  321         fpSet.makeSources(diaSources) 
  322         print("Merged %s Sources into %d diaSources (from %d +ve, %d -ve)" % (len(results.sources), 
  324                                                                           results.fpSets.numPos, 
  325                                                                           results.fpSets.numNeg)) 
  327     Finally, perform measurement (both standard and dipole-specialized) on the merged sources: 
  331         measureTask.run(diaSources, exposure) 
  333     Optionally display debugging information: 
  337         # Display dipoles if debug enabled 
  339             dpa = DipoleAnalysis() 
  340             dpa.displayDipoles(exposure, diaSources) 
  343     ConfigClass = DipoleMeasurementConfig
 
  344     _DefaultName = 
"dipoleMeasurement" 
  352     """Functor class to check whether a diaSource has flags set that should cause it to be labeled bad.""" 
  355         self.
badFlags = [
'base_PixelFlags_flag_edge', 
'base_PixelFlags_flag_interpolatedCenter',
 
  356                          'base_PixelFlags_flag_saturatedCenter']
 
  357         if badFlags 
is not None:
 
  358             for flag 
in badFlags:
 
  360         self.
keys = [sources.getSchema().find(name).key 
for name 
in self.
badFlags]
 
  361         self.
keys.
append(sources.table.getCentroidFlagKey())
 
  364         """Call the source flag checker on a single Source 
  369             Source that will be examined 
  378     """Functor class that provides (S/N, position, orientation) of measured dipoles""" 
  384         """Parse information returned from dipole measurement 
  388         source : `lsst.afw.table.SourceRecord` 
  389             The source that will be examined""" 
  390         return self.getSn(source), self.getCentroid(source), self.getOrientation(source)
 
  393         """Get the total signal-to-noise of the dipole; total S/N is from positive and negative lobe 
  397         source : `lsst.afw.table.SourceRecord` 
  398             The source that will be examined""" 
  400         posflux = source.get(
"ip_diffim_PsfDipoleFlux_pos_instFlux")
 
  401         posfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_pos_instFluxErr")
 
  402         negflux = source.get(
"ip_diffim_PsfDipoleFlux_neg_instFlux")
 
  403         negfluxErr = source.get(
"ip_diffim_PsfDipoleFlux_neg_instFluxErr")
 
  406         if (posflux < 0) 
is (negflux < 0):
 
  409         return np.sqrt((posflux/posfluxErr)**2 + (negflux/negfluxErr)**2)
 
  412         """Get the centroid of the dipole; average of positive and negative lobe 
  416         source : `lsst.afw.table.SourceRecord` 
  417             The source that will be examined""" 
  419         negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
 
  420         negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
 
  421         posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
 
  422         posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
 
  423         if (np.isinf(negCenX) 
or np.isinf(negCenY) 
or np.isinf(posCenX) 
or np.isinf(posCenY)):
 
  427                               0.5*(negCenY+posCenY))
 
  431         """Calculate the orientation of dipole; vector from negative to positive lobe 
  435         source : `lsst.afw.table.SourceRecord` 
  436             The source that will be examined""" 
  438         negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
 
  439         negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
 
  440         posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
 
  441         posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
 
  442         if (np.isinf(negCenX) 
or np.isinf(negCenY) 
or np.isinf(posCenX) 
or np.isinf(posCenY)):
 
  445         dx, dy = posCenX-negCenX, posCenY-negCenY
 
  446         angle = 
geom.Angle(np.arctan2(dx, dy), geom.radians)
 
  450         """Display debugging information on the detected dipoles 
  454         exposure : `lsst.afw.image.Exposure` 
  455             Image the dipoles were measured on 
  456         sources : `lsst.afw.table.SourceCatalog` 
  457             The set of diaSources that were measured""" 
  463         if not maskTransparency:
 
  464             maskTransparency = 90
 
  465         disp = afwDisplay.Display(frame=lsstDebug.frame)
 
  466         disp.setMaskTransparency(maskTransparency)
 
  469         if display 
and displayDiaSources:
 
  470             with disp.Buffering():
 
  471                 for source 
in sources:
 
  472                     cenX, cenY = source.get(
"ipdiffim_DipolePsfFlux_centroid")
 
  473                     if np.isinf(cenX) 
or np.isinf(cenY):
 
  474                         cenX, cenY = source.getCentroid()
 
  476                     isdipole = source.get(
"ip_diffim_ClassificationDipole_value")
 
  477                     if isdipole 
and np.isfinite(isdipole):
 
  479                         ctype = afwDisplay.GREEN
 
  482                         ctype = afwDisplay.RED
 
  484                     disp.dot(
"o", cenX, cenY, size=2, ctype=ctype)
 
  486                     negCenX = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_x")
 
  487                     negCenY = source.get(
"ip_diffim_PsfDipoleFlux_neg_centroid_y")
 
  488                     posCenX = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_x")
 
  489                     posCenY = source.get(
"ip_diffim_PsfDipoleFlux_pos_centroid_y")
 
  490                     if (np.isinf(negCenX) 
or np.isinf(negCenY) 
or np.isinf(posCenX) 
or np.isinf(posCenY)):
 
  493                     disp.line([(negCenX, negCenY), (posCenX, posCenY)], ctype=afwDisplay.YELLOW)
 
  499     """Functor to deblend a source as a dipole, and return a new source with deblended footprints. 
  501        This necessarily overrides some of the functionality from 
  502        meas_algorithms/python/lsst/meas/algorithms/deblend.py since we 
  503        need a single source that contains the blended peaks, not 
  504        multiple children sources.  This directly calls the core 
  505        deblending code deblendBaseline.deblend (optionally _fitPsf for 
  508        Not actively being used, but there is a unit test for it in 
  517         self.
log = Log.getLogger(
'ip.diffim.DipoleDeblender')
 
  521         fp = source.getFootprint()
 
  522         peaks = fp.getPeaks()
 
  523         peaksF = [pk.getF() 
for pk 
in peaks]
 
  526         fmask.setXY0(fbb.getMinX(), fbb.getMinY())
 
  527         fp.spans.setMask(fmask, 1)
 
  529         psf = exposure.getPsf()
 
  530         psfSigPix = psf.computeShape().getDeterminantRadius()
 
  532         subimage = afwImage.ExposureF(exposure, bbox=fbb, deep=
True)
 
  533         cpsf = deblendBaseline.CachingPsf(psf)
 
  537             return source.getTable().copyRecord(source)
 
  540         speaks = [(p.getPeakValue(), p) 
for p 
in peaks]
 
  542         dpeaks = [speaks[0][1], speaks[-1][1]]
 
  551             fpres = deblendBaseline.deblend(fp, exposure.getMaskedImage(), psf, psfFwhmPix,
 
  560             fpres = deblendBaseline.DeblenderResult(fp, exposure.getMaskedImage(), psf, psfFwhmPix, self.
log)
 
  562             for pki, (pk, pkres, pkF) 
in enumerate(zip(dpeaks, fpres.deblendedParents[0].peaks, peaksF)):
 
  564                 deblendBaseline._fitPsf(fp, fmask, pk, pkF, pkres, fbb, dpeaks, peaksF, self.
log,
 
  566                                         subimage.getMaskedImage().getImage(),
 
  567                                         subimage.getMaskedImage().getVariance(),
 
  570         deblendedSource = source.getTable().copyRecord(source)
 
  571         deblendedSource.setParent(source.getId())
 
  572         peakList = deblendedSource.getFootprint().getPeaks()
 
  575         for i, peak 
in enumerate(fpres.deblendedParents[0].peaks):
 
  576             if peak.psfFitFlux > 0:
 
  580             c = peak.psfFitCenter
 
  581             self.
log.
info(
"deblended.centroid.dipole.psf.%s %f %f",
 
  583             self.
log.
info(
"deblended.chi2dof.dipole.%s %f",
 
  584                           suffix, peak.psfFitChisq / peak.psfFitDof)
 
  585             self.
log.
info(
"deblended.flux.dipole.psf.%s %f",
 
  586                           suffix, peak.psfFitFlux * np.sum(peak.templateImage.getArray()))
 
  587             peakList.append(peak.peak)
 
  588         return deblendedSource