Coverage for python/lsst/ip/diffim/dipoleFitTask.py : 68%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
# # LSST Data Management System # Copyright 2008-2016 AURA/LSST. # # This product includes software developed by the # LSST Project (http://www.lsst.org/). # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the LSST License Statement and # the GNU General Public License along with this program. If not, # see <https://www.lsstcorp.org/LegalNotices/>. #
# Create a new measurement task (`DipoleFitTask`) that can handle all other SFM tasks but can # pass a separate pos- and neg- exposure/image to the `DipoleFitPlugin`s `run()` method.
"""!Configuration for DipoleFitPlugin """
dtype=float, default=False, doc="""Attempte dipole fit of all diaSources (otherwise just the ones consisting of overlapping positive and negative footprints)""")
dtype=float, default=5., doc="Assume dipole is not separated by more than maxSeparation * psfSigma")
dtype=float, default=0.5, doc="""Relative weighting of pre-subtraction images (higher -> greater influence of pre-sub. images on fit)""")
dtype=float, default=1e-7, doc="Fit tolerance")
dtype=int, default=1, doc="""Set whether and how to fit for linear gradient in pre-sub. images. Possible values: 0: do not fit background at all 1 (default): pre-fit the background using linear least squares and then do not fit it as part of the dipole fitting optimization 2: pre-fit the background using linear least squares (as in 1), and use the parameter estimates from that fit as starting parameters for an integrated "re-fit" of the background """)
dtype=bool, default=False, doc="Include parameters to fit for negative values (flux, gradient) separately from pos.")
# Config params for classification of detected diaSources as dipole or not dtype=float, default=np.sqrt(2) * 5.0, doc="Minimum quadrature sum of positive+negative lobe S/N to be considered a dipole")
dtype=float, default=0.65, doc="Maximum flux ratio in either lobe to be considered a dipole")
dtype=float, default=0.05, doc="""Maximum Chi2/DoF significance of fit to be considered a dipole. Default value means \"Choose a chi2DoF corresponding to a significance level of at most 0.05\" (note this is actually a significance, not a chi2 value).""")
"""!Measurement of detected diaSources as dipoles
Currently we keep the "old" DipoleMeasurement algorithms turned on. """
measBase.SingleFrameMeasurementConfig.setDefaults(self)
self.plugins.names = ["base_CircularApertureFlux", "base_PixelFlags", "base_SkyCoord", "base_PsfFlux", "base_SdssCentroid", "base_SdssShape", "base_SdssCentroid", "base_GaussianFlux", "base_PeakLikelihoodFlux", "base_PeakCentroid", "base_NaiveCentroid", "ip_diffim_NaiveDipoleCentroid", "ip_diffim_NaiveDipoleFlux", "ip_diffim_PsfDipoleFlux", "ip_diffim_ClassificationDipole", ]
self.slots.calibFlux = None self.slots.modelFlux = None self.slots.instFlux = None self.slots.shape = "base_SdssShape" self.slots.centroid = "ip_diffim_NaiveDipoleCentroid" self.doReplaceWithNoise = False
"""!Subclass of SingleFrameMeasurementTask which accepts up to three input images in its run() method.
Because it subclasses SingleFrameMeasurementTask, and calls SingleFrameMeasurementTask.run() from its run() method, it still can be used identically to a standard SingleFrameMeasurementTask. """
schema=schema, metadata=algMetadata)
"""!Run dipole measurement and classification
@param sources diaSources that will be measured using dipole measurement @param exposure Difference exposure on which the diaSources were detected; exposure = posExp - negExp @param posExp "Positive" exposure, typically a science exposure, or None if unavailable @param negExp "Negative" exposure, typically a template exposure, or None if unavailable @param **kwds Sent to SingleFrameMeasurementTask
@note When `posExp` is `None`, will compute `posImage = exposure + negExp`. Likewise, when `negExp` is `None`, will compute `negImage = posExp - exposure`. If both `posExp` and `negExp` are `None`, will attempt to fit the dipole to just the `exposure` with no constraint. """
return
"""!Lightweight class containing methods for generating a dipole model for fitting to sources in diffims, used by DipoleFitAlgorithm.
This code is documented in DMTN-007 (http://dmtn-007.lsst.io). """
"""!Generate gradient model (2-d array) with up to 2nd-order polynomial
@param in_x (2, w, h)-dimensional `numpy.array`, containing the input x,y meshgrid providing the coordinates upon which to compute the gradient. This will typically be generated via `_generateXYGrid()`. `w` and `h` correspond to the width and height of the desired grid. @param pars Up to 6 floats for up to 6 2nd-order 2-d polynomial gradient parameters, in the following order: (intercept, x, y, xy, x**2, y**2). If `pars` is emtpy or `None`, do nothing and return `None` (for speed).
@return None, or 2-d numpy.array of width/height matching input bbox, containing computed gradient values. """
# Don't fit for other gradient parameters if the intercept is not included. return
gradient += pars[3] * (x * y) gradient += pars[4] * (x * x) gradient += pars[5] * (y * y)
"""!Generate a meshgrid covering the x,y coordinates bounded by bbox
@param bbox input BoundingBox defining the coordinate limits @return in_x (2, w, h)-dimensional numpy array containing the grid indexing over x- and y- coordinates
@see makeBackgroundModel """
"""!Extract the image from a `HeavyFootprint` as an `afwImage.ImageF`.
@param fp HeavyFootprint to use to generate the subimage @param badfill Value to fill in pixels in extracted image that are outside the footprint @param grow Optionally grow the footprint by this amount before extraction
@return an `afwImage.ImageF` containing the subimage """
"""!Fit a linear (polynomial) model of given order (max 2) to the background of a footprint.
Only fit the pixels OUTSIDE of the footprint, but within its bounding box.
@param source SourceRecord, the footprint of which is to be fit @param posImage The exposure from which to extract the footprint subimage @param order Polynomial order of background gradient to fit.
@return pars `tuple` of length (1 if order==0; 3 if order==1; 6 if order == 2), containing the resulting fit parameters
@todo look into whether to use afwMath background methods -- see http://lsst-web.ncsa.illinois.edu/doxygen/x_masterDoxyDoc/_background_example.html """
# This code constructs the footprint image so that we can identify the pixels that are # outside the footprint (but within the bounding box). These are the pixels used for # fitting the background.
elif order == 2: M = np.vstack([b, x, y, x**2., y**2., x*y]).T
"""!Generate model (2-d Image) of a 'star' (single PSF) centered at given coordinates
@param bbox Bounding box marking pixel coordinates for generated model @param psf Psf model used to generate the 'star' @param xcen Desired x-centroid of the 'star' @param ycen Desired y-centroid of the 'star' @param flux Desired flux of the 'star'
@return 2-d stellar `afwImage.Image` of width/height matching input `bbox`, containing PSF with given centroid and flux """
# Generate the psf image, normalize to flux
# Clip the PSF image bounding box to fall within the footprint bounding box
# Then actually crop the psf image. # Usually not necessary, but if the dipole is near the edge of the image... # Would be nice if we could compare original pos_box with clipped pos_box and # see if it actually was clipped.
b=None, x1=None, y1=None, xy=None, x2=None, y2=None, bNeg=None, x1Neg=None, y1Neg=None, xyNeg=None, x2Neg=None, y2Neg=None, **kwargs): """!Generate dipole model with given parameters.
This is the function whose sum-of-squared difference from data is minimized by `lmfit`.
@param x Input independent variable. Used here as the grid on which to compute the background gradient model. @param flux Desired flux of the positive lobe of the dipole @param xcenPos Desired x-centroid of the positive lobe of the dipole @param ycenPos Desired y-centroid of the positive lobe of the dipole @param xcenNeg Desired x-centroid of the negative lobe of the dipole @param ycenNeg Desired y-centroid of the negative lobe of the dipole @param fluxNeg Desired flux of the negative lobe of the dipole, set to 'flux' if None @param b, x1, y1, xy, x2, y2 Gradient parameters for positive lobe. @param bNeg, x1Neg, y1Neg, xyNeg, x2Neg, y2Neg Gradient parameters for negative lobe. They are set to the corresponding positive values if None.
@param **kwargs Keyword arguments passed through `lmfit` and used by this function. These must include: - `psf` Psf model used to generate the 'star' - `rel_weight` Used to signify least-squares weighting of posImage/negImage relative to diffim. If `rel_weight == 0` then posImage/negImage are ignored. - `bbox` Bounding box containing region to be modelled
@see `makeBackgroundModel` for further parameter descriptions.
@return `numpy.array` of width/height matching input bbox, containing dipole model with given centroids and flux(es). If `rel_weight` = 0, this is a 2-d array with dimensions matching those of bbox; otherwise a stack of three such arrays, representing the dipole (diffim), positive and negative images respectively. """
self.log.debug('%.2f %.2f %.2f %.2f %.2f %.2f', flux, fluxNeg, xcenPos, ycenPos, xcenNeg, ycenNeg) if x1 is not None: self.log.debug(' %.2f %.2f %.2f', b, x1, y1) if xy is not None: self.log.debug(' %.2f %.2f %.2f', xy, x2, y2)
y, x = np.mgrid[bbox.getBeginY():bbox.getEndY(), bbox.getBeginX():bbox.getEndX()] in_x = np.array([x, y]) * 1. in_x[0, :] -= in_x[0, :].mean() # center it! in_x[1, :] -= in_x[1, :].mean()
gradient = self.makeBackgroundModel(in_x, (b, x1, y1, xy, x2, y2))
# If bNeg is None, then don't fit the negative background separately if bNeg is not None: gradientNeg = self.makeBackgroundModel(in_x, (bNeg, x1Neg, y1Neg, xyNeg, x2Neg, y2Neg)) else: gradientNeg = gradient
posIm.getArray()[:, :] += gradient negIm.getArray()[:, :] += gradientNeg
# Generate the diffIm model
"""!Lightweight class containing methods for fitting a dipole model in a diffim, used by DipoleFitPlugin.
This code is documented in DMTN-007 (http://dmtn-007.lsst.io).
Below is a (somewhat incomplete) list of improvements that would be worth investigating, given the time:
@todo 1. evaluate necessity for separate parameters for pos- and neg- images @todo 2. only fit background OUTSIDE footprint (DONE) and dipole params INSIDE footprint (NOT DONE)? @todo 3. correct normalization of least-squares weights based on variance planes @todo 4. account for PSFs that vary across the exposures (should be happening by default?) @todo 5. correctly account for NA/masks (i.e., ignore!) @todo 6. better exception handling in the plugin @todo 7. better classification of dipoles (e.g. by comparing chi2 fit vs. monopole?) @todo 8. (DONE) Initial fast estimate of background gradient(s) params -- perhaps using numpy.lstsq @todo 9. (NOT NEEDED - see (2)) Initial fast test whether a background gradient needs to be fit @todo 10. (DONE) better initial estimate for flux when there's a strong gradient @todo 11. (DONE) requires a new package `lmfit` -- investiate others? (astropy/scipy/iminuit?) """
# This is just a private version number to sync with the ipython notebooks that I have been # using for algorithm development.
"""!Algorithm to run dipole measurement on a diaSource
@param diffim Exposure on which the diaSources were detected @param posImage "Positive" exposure from which the template was subtracted @param negImage "Negative" exposure which was subtracted from the posImage """
fitBackground=1, bgGradientOrder=1, maxSepInSigma=5., separateNegParams=True, verbose=False): """!Fit a dipole model to an input difference image.
Actually, fits the subimage bounded by the input source's footprint) and optionally constrain the fit using the pre-subtraction images posImage and negImage.
@return `lmfit.MinimizerResult` object containing the fit parameters and other information.
@see `fitDipole()` """
# Only import lmfit if someone wants to use the new DipoleFitAlgorithm.
negSubim.getArrays()[0]], axis=0) # Weight the pos/neg images by rel_weight relative to the diffim 1. / negSubim.getArrays()[2] * rel_weight], axis=0) else:
# It seems that `lmfit` requires a static functor as its optimized method, which eliminates # the ability to pass a bound method or other class method. Here we write a wrapper which # makes this possible. b=None, x1=None, y1=None, xy=None, x2=None, y2=None, bNeg=None, x1Neg=None, y1Neg=None, xyNeg=None, x2Neg=None, y2Neg=None, **kwargs): """!Generate dipole model with given parameters.
It simply defers to `modelObj.makeModel()`, where `modelObj` comes out of `kwargs['modelObj']`.
@see DipoleModel.makeModel """ b=b, x1=x1, y1=y1, xy=xy, x2=x2, y2=y2, bNeg=bNeg, x1Neg=x1Neg, y1Neg=y1Neg, xyNeg=xyNeg, x2Neg=x2Neg, y2Neg=y2Neg, **kwargs)
# Create the lmfit model (lmfit uses scipy 'leastsq' option by default - Levenberg-Marquardt) # Note we can also tell it to drop missing values from the data. # independent_vars=independent_vars) #, param_names=param_names)
# Add the constraints for centroids, fluxes. # starting constraint - near centroid of footprint
# For close/faint dipoles the starting locs (min/max) might be way off, let's help them a bit. # First assume dipole is not separated by more than 5*psfSigma.
# As an initial guess -- assume the dipole is close to the center of the footprint. cenPos = fpCentroid cenPos = fpCentroid
# parameter hints/constraints: https://lmfit.github.io/lmfit-py/model.html#model-param-hints-section # might make sense to not use bounds -- see http://lmfit.github.io/lmfit-py/bounds.html # also see this discussion -- https://github.com/scipy/scipy/issues/3129 min=cenPos[0]-maxSep, max=cenPos[0]+maxSep) min=cenPos[1]-maxSep, max=cenPos[1]+maxSep) min=cenNeg[0]-maxSep, max=cenNeg[0]+maxSep) min=cenNeg[1]-maxSep, max=cenNeg[1]+maxSep)
# Use the (flux under the dipole)*5 for an estimate. # Lots of testing showed that having startingFlux be too high was better than too low.
# TBD: set max. flux limit?
# TBD: set max negative lobe flux limit? gmod.set_param_hint('fluxNeg', value=np.abs(negFlux), min=0.1)
# Fixed parameters (don't fit for them if there are no pre-sub images or no gradient fit requested): # Right now (fitBackground == 1), we fit a linear model to the background and then subtract # it from the data and then don't fit the background again (this is faster). # A slower alternative (fitBackground == 2) is to use the estimated background parameters as # starting points in the integrated model fit. That is currently not performed by default, # but might be desirable in some cases. # Fit the gradient to the background (linear model) order=bgGradientOrder)
# Generate the gradient and subtract it from the pre-subtraction image data
bgParsNeg = dipoleModel.fitFootprintBackground(source, self.negImage, order=bgGradientOrder) pbg = dipoleModel.makeBackgroundModel(in_x, tuple(bgParsNeg)) negFlux = np.nansum(z[2, :]) gmod.set_param_hint('fluxNeg', value=negFlux*1.5, min=0.1)
# Do not subtract the background from the images but include the background parameters in the fit if bgGradientOrder >= 0: gmod.set_param_hint('b', value=bgParsPos[0]) if separateNegParams: gmod.set_param_hint('bNeg', value=bgParsNeg[0]) if bgGradientOrder >= 1: gmod.set_param_hint('x1', value=bgParsPos[1]) gmod.set_param_hint('y1', value=bgParsPos[2]) if separateNegParams: gmod.set_param_hint('x1Neg', value=bgParsNeg[1]) gmod.set_param_hint('y1Neg', value=bgParsNeg[2]) if bgGradientOrder >= 2: gmod.set_param_hint('xy', value=bgParsPos[3]) gmod.set_param_hint('x2', value=bgParsPos[4]) gmod.set_param_hint('y2', value=bgParsPos[5]) if separateNegParams: gmod.set_param_hint('xyNeg', value=bgParsNeg[3]) gmod.set_param_hint('x2Neg', value=bgParsNeg[4]) gmod.set_param_hint('y2Neg', value=bgParsNeg[5])
# Instead of explicitly using a mask to ignore flagged pixels, just set the ignored pixels' # weights to 0 in the fit. TBD: need to inspect mask planes to set this mask.
# I'm not sure about the variance planes in the diffim (or convolved pre-sub. images # for that matter) so for now, let's just do an un-weighted least-squares fit # (override weights computed above). np.ones_like(diArr)*rel_weight])
# Set the weights to zero if mask is False weights[~mask] = 0.
# Note that although we can, we're not required to set initial values for params here, # since we set their param_hint's above. # Can add "method" param to not use 'leastsq' (==levenberg-marquardt), e.g. "method='nelder'" verbose=verbose, fit_kws={'ftol': tol, 'xtol': tol, 'gtol': tol, 'maxfev': 250}, # see scipy docs psf=self.diffim.getPsf(), # hereon: kwargs that get passed to genDipoleModel() rel_weight=rel_weight, footprint=fp, modelObj=dipoleModel)
# Never wanted in production - this takes a long time (longer than the fit!) # This is how to get confidence intervals out: # https://lmfit.github.io/lmfit-py/confidence.html and # http://cars9.uchicago.edu/software/python/lmfit/model.html print(result.fit_report(show_correl=False)) if separateNegParams: print(result.ci_report())
fitBackground=1, maxSepInSigma=5., separateNegParams=True, bgGradientOrder=1, verbose=False, display=False): """!Wrapper around `fitDipoleImpl()` which performs the fit of a dipole model to an input diaSource.
Actually, fits the subimage bounded by the input source's footprint) and optionally constrain the fit using the pre-subtraction images self.posImage (science) and self.negImage (template). Wraps the output into a `pipeBase.Struct` named tuple after computing additional statistics such as orientation and SNR.
@param source Record containing the (merged) dipole source footprint detected on the diffim @param tol Tolerance parameter for scipy.leastsq() optimization @param rel_weight Weighting of posImage/negImage relative to the diffim in the fit @param fitBackground How to fit linear background gradient in posImage/negImage (see notes) @param bgGradientOrder Desired polynomial order of background gradient (allowed are [0,1,2]) @param maxSepInSigma Allowed window of centroid parameters relative to peak in input source footprint @param separateNegParams Fit separate parameters to the flux and background gradient in the negative images? If true, this adds a separate parameter for the negative flux, and [1, 3, or 6] additional parameters to fit for the background gradient in the negImage. Otherwise, the flux and gradient parameters are constrained to be exactly equal in the fit. @param verbose Be verbose @param display Display input data, best fit model(s) and residuals in a matplotlib window.
@return `pipeBase.Struct` object containing the fit parameters and other information. @return `lmfit.MinimizerResult` object for debugging and error estimation, etc.
@note Parameter `fitBackground` has three options, thus it is an integer: - 0: do not fit background at all - 1 (default): pre-fit the background using linear least squares and then do not fit it as part of the dipole fitting optimization - 2: pre-fit the background using linear least squares (as in 1), and use the parameter estimates from that fit as starting parameters for an integrated "re-fit" of the background as part of the overall dipole fitting optimization. """
source, tol=tol, rel_weight=rel_weight, fitBackground=fitBackground, maxSepInSigma=maxSepInSigma, separateNegParams=separateNegParams, bgGradientOrder=bgGradientOrder, verbose=verbose)
# Display images, model fits and residuals (currently uses matplotlib display functions) fp = source.getFootprint() self.displayFitResults(fp, fitResult)
out = Struct(posCentroidX=np.nan, posCentroidY=np.nan, negCentroidX=np.nan, negCentroidY=np.nan, posFlux=np.nan, negFlux=np.nan, posFluxSigma=np.nan, negFluxSigma=np.nan, centroidX=np.nan, centroidY=np.nan, orientation=np.nan, signalToNoise=np.nan, chi2=np.nan, redChi2=np.nan) return out, fitResult
(fitParams['ycenPos'] + fitParams['ycenNeg']) / 2.)
# Exctract flux value, compute signalToNoise from flux/variance_within_footprint # Also extract the stderr of flux estimate.
else:
fluxValNeg = fitParams['fluxNeg'] fluxErrNeg = fitResult.params['fluxNeg'].stderr
except ZeroDivisionError: # catch divide by zero - should never happen. signalToNoise = np.nan
negCentroidX=fitParams['xcenNeg'], negCentroidY=fitParams['ycenNeg'], posFlux=fluxVal, negFlux=-fluxValNeg, posFluxSigma=fluxErr, negFluxSigma=fluxErrNeg, centroidX=centroid[0], centroidY=centroid[1], orientation=angle, signalToNoise=signalToNoise, chi2=fitResult.chisqr, redChi2=fitResult.redchi)
# fitResult may be returned for debugging
"""!Display data, model fits and residuals (currently uses matplotlib display functions).
@param footprint Footprint containing the dipole that was fit @param result `lmfit.MinimizerResult` object returned by `lmfit` optimizer """ try: import matplotlib.pyplot as plt except ImportError as err: self.log.warn('Unable to import matplotlib: %s', err) raise err
def display2dArray(arr, title='Data', extent=None): """!Use `matplotlib.pyplot.imshow` to display a 2-D array with a given coordinate range. """ fig = plt.imshow(arr, origin='lower', interpolation='none', cmap='gray', extent=extent) plt.title(title) plt.colorbar(fig, cmap='gray') return fig
z = result.data fit = result.best_fit bbox = footprint.getBBox() extent = (bbox.getBeginX(), bbox.getEndX(), bbox.getBeginY(), bbox.getEndY()) if z.shape[0] == 3: fig = plt.figure(figsize=(8, 8)) for i in range(3): plt.subplot(3, 3, i*3+1) display2dArray(z[i, :], 'Data', extent=extent) plt.subplot(3, 3, i*3+2) display2dArray(fit[i, :], 'Model', extent=extent) plt.subplot(3, 3, i*3+3) display2dArray(z[i, :] - fit[i, :], 'Residual', extent=extent) return fig else: fig = plt.figure(figsize=(8, 2.5)) plt.subplot(1, 3, 1) display2dArray(z, 'Data', extent=extent) plt.subplot(1, 3, 2) display2dArray(fit, 'Model', extent=extent) plt.subplot(1, 3, 3) display2dArray(z - fit, 'Residual', extent=extent) return fig
plt.show()
"""!Subclass of SingleFramePlugin which fits dipoles to all merged (two-peak) diaSources
Accepts up to three input images in its `measure` method. If these are provided, it includes data from the pre-subtraction posImage (science image) and optionally negImage (template image) to constrain the fit. The meat of the fitting routines are in the class DipoleFitAlgorithm.
The motivation behind this plugin and the necessity for including more than one exposure are documented in DMTN-007 (http://dmtn-007.lsst.io).
This class is named `ip_diffim_DipoleFit` so that it may be used alongside the existing `ip_diffim_DipoleMeasurement` classes until such a time as those are deemed to be replaceable by this. """
def getExecutionOrder(cls): """!Set execution order to `FLUX_ORDER`.
This includes algorithms that require both `getShape()` and `getCentroid()`, in addition to a Footprint and its Peaks. """ return cls.FLUX_ORDER
# Get a FunctorKey that can quickly look up the "blessed" centroid value.
# Add some fields for our outputs, and save their Keys. # Use setattr() to programmatically set the pos/neg named attributes to values, e.g. # self.posCentroidKeyX = 'ip_diffim_DipoleFit_pos_centroid_x'
schema.join(name, pos_neg, "flux"), type=float, units="count", doc="Dipole {0} lobe flux".format(pos_neg))
schema.join(name, pos_neg, "fluxSigma"), type=float, units="pixel", doc="1-sigma uncertainty for {0} dipole flux".format(pos_neg))
schema.join(name, pos_neg, "centroid", x_y), type=float, units="pixel", doc="Dipole {0} lobe centroid".format(pos_neg))
schema.join(name, "centroid", x_y), type=float, units="pixel", doc="Dipole centroid")
schema.join(name, "flux"), type=float, units="count", doc="Dipole overall flux")
schema.join(name, "orientation"), type=float, units="deg", doc="Dipole orientation")
schema.join(name, "separation"), type=float, units="pixel", doc="Pixel separation between positive and negative lobes of dipole")
schema.join(name, "chi2dof"), type=float, doc="Chi2 per degree of freedom of dipole fit")
schema.join(name, "signalToNoise"), type=float, doc="Estimated signal-to-noise of dipole fit")
schema.join(name, "flag", "classification"), type="Flag", doc="Flag indicating diaSource is classified as a dipole")
schema.join(name, "flag", "classificationAttempted"), type="Flag", doc="Flag indicating diaSource was attempted to be classified as a dipole")
schema.join(name, "flag"), type="Flag", doc="General failure flag for dipole fit")
schema.join(name, "flag", "edge"), type="Flag", doc="Flag set when dipole is too close to edge of image")
"""!Perform the non-linear least squares minimization on the putative dipole source.
@param measRecord diaSources that will be measured using dipole measurement @param exposure Difference exposure on which the diaSources were detected; `exposure = posExp-negExp` @param posExp "Positive" exposure, typically a science exposure, or None if unavailable @param negExp "Negative" exposure, typically a template exposure, or None if unavailable
@note When `posExp` is `None`, will compute `posImage = exposure + negExp`. Likewise, when `negExp` is `None`, will compute `negImage = posExp - exposure`. If both `posExp` and `negExp` are `None`, will attempt to fit the dipole to just the `exposure` with no constraint.
The main functionality of this routine was placed outside of this plugin (into `DipoleFitAlgorithm.fitDipole()`) so that `DipoleFitAlgorithm.fitDipole()` can be called separately for testing (@see `tests/testDipoleFitter.py`) """
# Check if the footprint consists of a putative dipole - else don't fit it. (len(pks) <= 1) or # one peak in the footprint - not a dipole (len(pks) > 1 and (np.sign(pks[0].getPeakValue()) == np.sign(pks[-1].getPeakValue()))) # peaks are same sign - not a dipole ):
measRecord, rel_weight=self.config.relWeight, tol=self.config.tolerance, maxSepInSigma=self.config.maxSeparation, fitBackground=self.config.fitBackground, separateNegParams=self.config.fitSeparateNegParams, verbose=False, display=False) except pexExcept.LengthError: self.fail(measRecord, measBase.MeasurementError('edge failure', self.FAILURE_EDGE)) except Exception: self.fail(measRecord, measBase.MeasurementError('dipole fit failure', self.FAILURE_FIT))
measRecord.set(self.classificationFlagKey, False) measRecord.set(self.classificationAttemptedFlagKey, False) return result
self.fail(measRecord, measBase.MeasurementError('dipole fit failure', self.FAILURE_FIT))
# add chi2, coord/flux uncertainties (TBD), dipole classification # Add the relevant values to the measRecord
# Dia source flux: average of pos+neg (result.posCentroidY - result.negCentroidY)**2.)
"""!Determine if source is classified as dipole via three criteria: - does the total signal-to-noise surpass the minSn? - are the pos/neg fluxes greater than 1.0 and no more than 0.65 (param `maxFluxRatio`) of the total flux? By default this will never happen since `posFlux == negFlux`. - is it a good fit (`chi2dof` < 1)? (Currently not used.) """
# First, does the total signal-to-noise surpass the minSn?
# Second, are the pos/neg fluxes greater than 1.0 and no more than 0.65 (param maxFluxRatio) # of the total flux? By default this will never happen since posFlux = negFlux. (measRecord[self.fluxKey]*2.)) < self.config.maxFluxRatio (measRecord[self.fluxKey]*2.)) < self.config.maxFluxRatio
# Third, is it a good fit (chi2dof < 1)? # Use scipy's chi2 cumulative distrib to estimate significance # This doesn't really work since I don't trust the values in the variance plane (which # affects the least-sq weights, which affects the resulting chi2). # But I'm going to keep this here for future use. if False: from scipy.stats import chi2 ndof = chi2val / measRecord[self.chi2dofKey] significance = chi2.cdf(chi2val, ndof) passesChi2 = significance < self.config.maxChi2DoF allPass = allPass and passesChi2
else: measRecord.set(self.classificationFlagKey, False)
"""!Catch failures and set the correct flags. """
self.log.warn('DipoleFitPlugin not run on record %d: %s', measRecord.getId(), str(error)) measRecord.set(self.edgeFlagKey, True) self.log.warn('DipoleFitPlugin failed on record %d: %s', measRecord.getId(), str(error)) measRecord.set(self.flagKey, True) measRecord.getId(), str(error)) else: self.log.warn('DipoleFitPlugin failed on record %d', measRecord.getId()) |