545 fitBackground=1, bgGradientOrder=1, maxSepInSigma=5.,
546 separateNegParams=True, verbose=False):
547 """Fit a dipole model to an input difference image.
549 Actually, fits the subimage bounded by the input source's
550 footprint) and optionally constrain the fit using the
551 pre-subtraction images posImage and negImage.
555 source : TODO: DM-17458
557 tol : float, optional
559 rel_weight : `float`, optional
561 fitBackground : `int`, optional
563 bgGradientOrder : `int`, optional
565 maxSepInSigma : `float`, optional
567 separateNegParams : `bool`, optional
569 verbose : `bool`, optional
574 result : `lmfit.MinimizerResult`
575 return `lmfit.MinimizerResult` object containing the fit
576 parameters and other information.
582 fp = source.getFootprint()
584 subim = afwImage.MaskedImageF(self.
diffim.getMaskedImage(), bbox=bbox, origin=afwImage.PARENT)
586 z = diArr = subim.image.array
589 weights = 1. / subim.variance.array
591 if rel_weight > 0.
and ((self.
posImage is not None)
or (self.
negImage is not None)):
593 negSubim = afwImage.MaskedImageF(self.
negImage.getMaskedImage(), bbox, origin=afwImage.PARENT)
595 posSubim = afwImage.MaskedImageF(self.
posImage.getMaskedImage(), bbox, origin=afwImage.PARENT)
597 posSubim = subim.clone()
600 negSubim = posSubim.clone()
603 z = np.append([z], [posSubim.image.array,
604 negSubim.image.array], axis=0)
606 weights = np.append([weights], [1. / posSubim.variance.array * rel_weight,
607 1. / negSubim.variance.array * rel_weight], axis=0)
614 def dipoleModelFunctor(x, flux, xcenPos, ycenPos, xcenNeg, ycenNeg, fluxNeg=None,
615 b=None, x1=None, y1=None, xy=None, x2=None, y2=None,
616 bNeg=None, x1Neg=None, y1Neg=None, xyNeg=None, x2Neg=None, y2Neg=None,
618 """Generate dipole model with given parameters.
620 It simply defers to `modelObj.makeModel()`, where `modelObj` comes
621 out of `kwargs['modelObj']`.
623 modelObj = kwargs.pop(
'modelObj')
624 return modelObj.makeModel(x, flux, xcenPos, ycenPos, xcenNeg, ycenNeg, fluxNeg=fluxNeg,
625 b=b, x1=x1, y1=y1, xy=xy, x2=x2, y2=y2,
626 bNeg=bNeg, x1Neg=x1Neg, y1Neg=y1Neg, xyNeg=xyNeg,
627 x2Neg=x2Neg, y2Neg=y2Neg, **kwargs)
631 modelFunctor = dipoleModelFunctor
634 gmod = lmfit.Model(modelFunctor, verbose=verbose)
638 fpCentroid = np.array([fp.getCentroid().getX(), fp.getCentroid().getY()])
639 cenNeg = cenPos = fpCentroid
644 cenPos = pks[0].getF()
646 cenNeg = pks[-1].getF()
650 maxSep = self.
psfSigma * maxSepInSigma
653 if np.sum(np.sqrt((np.array(cenPos) - fpCentroid)**2.)) > maxSep:
655 if np.sum(np.sqrt((np.array(cenNeg) - fpCentroid)**2.)) > maxSep:
661 gmod.set_param_hint(
'xcenPos', value=cenPos[0],
662 min=cenPos[0]-maxSep, max=cenPos[0]+maxSep)
663 gmod.set_param_hint(
'ycenPos', value=cenPos[1],
664 min=cenPos[1]-maxSep, max=cenPos[1]+maxSep)
665 gmod.set_param_hint(
'xcenNeg', value=cenNeg[0],
666 min=cenNeg[0]-maxSep, max=cenNeg[0]+maxSep)
667 gmod.set_param_hint(
'ycenNeg', value=cenNeg[1],
668 min=cenNeg[1]-maxSep, max=cenNeg[1]+maxSep)
672 startingFlux = np.nansum(np.abs(diArr) - np.nanmedian(np.abs(diArr))) * 5.
673 posFlux = negFlux = startingFlux
676 gmod.set_param_hint(
'flux', value=posFlux, min=0.1)
678 if separateNegParams:
680 gmod.set_param_hint(
'fluxNeg', value=np.abs(negFlux), min=0.1)
688 bgParsPos = bgParsNeg = (0., 0., 0.)
689 if ((rel_weight > 0.)
and (fitBackground != 0)
and (bgGradientOrder >= 0)):
693 bgParsPos = bgParsNeg = dipoleModel.fitFootprintBackground(source, bgFitImage,
694 order=bgGradientOrder)
697 if fitBackground == 1:
698 in_x = dipoleModel._generateXYGrid(bbox)
699 pbg = dipoleModel.makeBackgroundModel(in_x, tuple(bgParsPos))
701 z[1, :] -= np.nanmedian(z[1, :])
702 posFlux = np.nansum(z[1, :])
703 gmod.set_param_hint(
'flux', value=posFlux*1.5, min=0.1)
705 if separateNegParams
and self.
negImage is not None:
706 bgParsNeg = dipoleModel.fitFootprintBackground(source, self.
negImage,
707 order=bgGradientOrder)
708 pbg = dipoleModel.makeBackgroundModel(in_x, tuple(bgParsNeg))
710 z[2, :] -= np.nanmedian(z[2, :])
711 if separateNegParams:
712 negFlux = np.nansum(z[2, :])
713 gmod.set_param_hint(
'fluxNeg', value=negFlux*1.5, min=0.1)
716 if fitBackground == 2:
717 if bgGradientOrder >= 0:
718 gmod.set_param_hint(
'b', value=bgParsPos[0])
719 if separateNegParams:
720 gmod.set_param_hint(
'bNeg', value=bgParsNeg[0])
721 if bgGradientOrder >= 1:
722 gmod.set_param_hint(
'x1', value=bgParsPos[1])
723 gmod.set_param_hint(
'y1', value=bgParsPos[2])
724 if separateNegParams:
725 gmod.set_param_hint(
'x1Neg', value=bgParsNeg[1])
726 gmod.set_param_hint(
'y1Neg', value=bgParsNeg[2])
727 if bgGradientOrder >= 2:
728 gmod.set_param_hint(
'xy', value=bgParsPos[3])
729 gmod.set_param_hint(
'x2', value=bgParsPos[4])
730 gmod.set_param_hint(
'y2', value=bgParsPos[5])
731 if separateNegParams:
732 gmod.set_param_hint(
'xyNeg', value=bgParsNeg[3])
733 gmod.set_param_hint(
'x2Neg', value=bgParsNeg[4])
734 gmod.set_param_hint(
'y2Neg', value=bgParsNeg[5])
736 y, x = np.mgrid[bbox.getBeginY():bbox.getEndY(), bbox.getBeginX():bbox.getEndX()]
737 in_x = np.array([x, y]).astype(np.float64)
738 in_x[0, :] -= in_x[0, :].mean()
739 in_x[1, :] -= in_x[1, :].mean()
743 mask = np.ones_like(z, dtype=bool)
748 weights = mask.astype(np.float64)
749 if self.
posImage is not None and rel_weight > 0.:
750 weights = np.array([np.ones_like(diArr), np.ones_like(diArr)*rel_weight,
751 np.ones_like(diArr)*rel_weight])
758 nans = (np.isnan(z) | np.isnan(weights))
762 z[nans] = np.nanmedian(z)
770 with warnings.catch_warnings():
773 warnings.filterwarnings(
"ignore",
"The keyword argument .* does not match", UserWarning)
774 result = gmod.fit(z, weights=weights, x=in_x, max_nfev=250,
778 fit_kws={
'ftol': tol,
'xtol': tol,
'gtol': tol,
782 rel_weight=rel_weight,
784 modelObj=dipoleModel)
791 print(result.fit_report(show_correl=
False))
792 if separateNegParams:
793 print(result.ci_report())
798 fitBackground=1, maxSepInSigma=5., separateNegParams=True,
799 bgGradientOrder=1, verbose=False, display=False):
800 """Fit a dipole model to an input ``diaSource`` (wraps `fitDipoleImpl`).
802 Actually, fits the subimage bounded by the input source's
803 footprint) and optionally constrain the fit using the
804 pre-subtraction images self.posImage (science) and
805 self.negImage (template). Wraps the output into a
806 `pipeBase.Struct` named tuple after computing additional
807 statistics such as orientation and SNR.
811 source : `lsst.afw.table.SourceRecord`
812 Record containing the (merged) dipole source footprint detected on the diffim
813 tol : `float`, optional
814 Tolerance parameter for scipy.leastsq() optimization
815 rel_weight : `float`, optional
816 Weighting of posImage/negImage relative to the diffim in the fit
817 fitBackground : `int`, {0, 1, 2}, optional
818 How to fit linear background gradient in posImage/negImage
820 - 0: do not fit background at all
821 - 1 (default): pre-fit the background using linear least squares and then do not fit it
822 as part of the dipole fitting optimization
823 - 2: pre-fit the background using linear least squares (as in 1), and use the parameter
824 estimates from that fit as starting parameters for an integrated "re-fit" of the
825 background as part of the overall dipole fitting optimization.
826 maxSepInSigma : `float`, optional
827 Allowed window of centroid parameters relative to peak in input source footprint
828 separateNegParams : `bool`, optional
829 Fit separate parameters to the flux and background gradient in
830 bgGradientOrder : `int`, {0, 1, 2}, optional
831 Desired polynomial order of background gradient
832 verbose: `bool`, optional
835 Display input data, best fit model(s) and residuals in a matplotlib window.
840 `pipeBase.Struct` object containing the fit parameters and other information.
843 `lmfit.MinimizerResult` object for debugging and error estimation, etc.
847 Parameter `fitBackground` has three options, thus it is an integer:
852 source, tol=tol, rel_weight=rel_weight, fitBackground=fitBackground,
853 maxSepInSigma=maxSepInSigma, separateNegParams=separateNegParams,
854 bgGradientOrder=bgGradientOrder, verbose=verbose)
858 fp = source.getFootprint()
861 fitParams = fitResult.best_values
862 if fitParams[
'flux'] <= 1.:
863 return None, fitResult
865 centroid = ((fitParams[
'xcenPos'] + fitParams[
'xcenNeg']) / 2.,
866 (fitParams[
'ycenPos'] + fitParams[
'ycenNeg']) / 2.)
867 dx, dy = fitParams[
'xcenPos'] - fitParams[
'xcenNeg'], fitParams[
'ycenPos'] - fitParams[
'ycenNeg']
868 angle = np.arctan2(dy, dx) / np.pi * 180.
872 def computeSumVariance(exposure, footprint):
873 return np.sqrt(np.nansum(exposure[footprint.getBBox(), afwImage.PARENT].variance.array))
875 fluxVal = fluxVar = fitParams[
'flux']
876 fluxErr = fluxErrNeg = fitResult.params[
'flux'].stderr
878 fluxVar = computeSumVariance(self.
posImage, source.getFootprint())
880 fluxVar = computeSumVariance(self.
diffim, source.getFootprint())
882 fluxValNeg, fluxVarNeg = fluxVal, fluxVar
883 if separateNegParams:
884 fluxValNeg = fitParams[
'fluxNeg']
885 fluxErrNeg = fitResult.params[
'fluxNeg'].stderr
887 fluxVarNeg = computeSumVariance(self.
negImage, source.getFootprint())
890 signalToNoise = np.sqrt((fluxVal/fluxVar)**2 + (fluxValNeg/fluxVarNeg)**2)
891 except ZeroDivisionError:
892 signalToNoise = np.nan
894 out = Struct(posCentroidX=fitParams[
'xcenPos'], posCentroidY=fitParams[
'ycenPos'],
895 negCentroidX=fitParams[
'xcenNeg'], negCentroidY=fitParams[
'ycenNeg'],
896 posFlux=fluxVal, negFlux=-fluxValNeg, posFluxErr=fluxErr, negFluxErr=fluxErrNeg,
897 centroidX=centroid[0], centroidY=centroid[1], orientation=angle,
898 signalToNoise=signalToNoise, chi2=fitResult.chisqr, redChi2=fitResult.redchi,
899 nData=fitResult.ndata)
902 return out, fitResult