544 fitBackground=1, bgGradientOrder=1, maxSepInSigma=5.,
545 separateNegParams=True, verbose=False):
546 """Fit a dipole model to an input difference image.
548 Actually, fits the subimage bounded by the input source's
549 footprint) and optionally constrain the fit using the
550 pre-subtraction images posImage and negImage.
554 source : TODO: DM-17458
556 tol : float, optional
558 rel_weight : `float`, optional
560 fitBackground : `int`, optional
562 bgGradientOrder : `int`, optional
564 maxSepInSigma : `float`, optional
566 separateNegParams : `bool`, optional
568 verbose : `bool`, optional
573 result : `lmfit.MinimizerResult`
574 return `lmfit.MinimizerResult` object containing the fit
575 parameters and other information.
581 fp = source.getFootprint()
583 subim = afwImage.MaskedImageF(self.
diffim.getMaskedImage(), bbox=bbox, origin=afwImage.PARENT)
585 z = diArr = subim.image.array
588 weights = 1. / subim.variance.array
590 if rel_weight > 0.
and ((self.
posImage is not None)
or (self.
negImage is not None)):
592 negSubim = afwImage.MaskedImageF(self.
negImage.getMaskedImage(), bbox, origin=afwImage.PARENT)
594 posSubim = afwImage.MaskedImageF(self.
posImage.getMaskedImage(), bbox, origin=afwImage.PARENT)
596 posSubim = subim.clone()
599 negSubim = posSubim.clone()
602 z = np.append([z], [posSubim.image.array,
603 negSubim.image.array], axis=0)
605 weights = np.append([weights], [1. / posSubim.variance.array * rel_weight,
606 1. / negSubim.variance.array * rel_weight], axis=0)
613 def dipoleModelFunctor(x, flux, xcenPos, ycenPos, xcenNeg, ycenNeg, fluxNeg=None,
614 b=None, x1=None, y1=None, xy=None, x2=None, y2=None,
615 bNeg=None, x1Neg=None, y1Neg=None, xyNeg=None, x2Neg=None, y2Neg=None,
617 """Generate dipole model with given parameters.
619 It simply defers to `modelObj.makeModel()`, where `modelObj` comes
620 out of `kwargs['modelObj']`.
622 modelObj = kwargs.pop(
'modelObj')
623 return modelObj.makeModel(x, flux, xcenPos, ycenPos, xcenNeg, ycenNeg, fluxNeg=fluxNeg,
624 b=b, x1=x1, y1=y1, xy=xy, x2=x2, y2=y2,
625 bNeg=bNeg, x1Neg=x1Neg, y1Neg=y1Neg, xyNeg=xyNeg,
626 x2Neg=x2Neg, y2Neg=y2Neg, **kwargs)
630 modelFunctor = dipoleModelFunctor
636 gmod = lmfit.Model(modelFunctor, independent_vars=[
"x"], verbose=verbose)
640 fpCentroid = np.array([fp.getCentroid().getX(), fp.getCentroid().getY()])
641 cenNeg = cenPos = fpCentroid
646 cenPos = pks[0].getF()
648 cenNeg = pks[-1].getF()
652 maxSep = self.
psfSigma * maxSepInSigma
655 if np.sum(np.sqrt((np.array(cenPos) - fpCentroid)**2.)) > maxSep:
657 if np.sum(np.sqrt((np.array(cenNeg) - fpCentroid)**2.)) > maxSep:
663 gmod.set_param_hint(
'xcenPos', value=cenPos[0],
664 min=cenPos[0]-maxSep, max=cenPos[0]+maxSep)
665 gmod.set_param_hint(
'ycenPos', value=cenPos[1],
666 min=cenPos[1]-maxSep, max=cenPos[1]+maxSep)
667 gmod.set_param_hint(
'xcenNeg', value=cenNeg[0],
668 min=cenNeg[0]-maxSep, max=cenNeg[0]+maxSep)
669 gmod.set_param_hint(
'ycenNeg', value=cenNeg[1],
670 min=cenNeg[1]-maxSep, max=cenNeg[1]+maxSep)
674 startingFlux = np.nansum(np.abs(diArr) - np.nanmedian(np.abs(diArr))) * 5.
675 posFlux = negFlux = startingFlux
678 gmod.set_param_hint(
'flux', value=posFlux, min=0.1)
680 if separateNegParams:
682 gmod.set_param_hint(
'fluxNeg', value=np.abs(negFlux), min=0.1)
690 bgParsPos = bgParsNeg = (0., 0., 0.)
691 if ((rel_weight > 0.)
and (fitBackground != 0)
and (bgGradientOrder >= 0)):
695 bgParsPos = bgParsNeg = dipoleModel.fitFootprintBackground(source, bgFitImage,
696 order=bgGradientOrder)
699 if fitBackground == 1:
700 in_x = dipoleModel._generateXYGrid(bbox)
701 pbg = dipoleModel.makeBackgroundModel(in_x, tuple(bgParsPos))
703 z[1, :] -= np.nanmedian(z[1, :])
704 posFlux = np.nansum(z[1, :])
705 gmod.set_param_hint(
'flux', value=posFlux*1.5, min=0.1)
707 if separateNegParams
and self.
negImage is not None:
708 bgParsNeg = dipoleModel.fitFootprintBackground(source, self.
negImage,
709 order=bgGradientOrder)
710 pbg = dipoleModel.makeBackgroundModel(in_x, tuple(bgParsNeg))
712 z[2, :] -= np.nanmedian(z[2, :])
713 if separateNegParams:
714 negFlux = np.nansum(z[2, :])
715 gmod.set_param_hint(
'fluxNeg', value=negFlux*1.5, min=0.1)
718 if fitBackground == 2:
719 if bgGradientOrder >= 0:
720 gmod.set_param_hint(
'b', value=bgParsPos[0])
721 if separateNegParams:
722 gmod.set_param_hint(
'bNeg', value=bgParsNeg[0])
723 if bgGradientOrder >= 1:
724 gmod.set_param_hint(
'x1', value=bgParsPos[1])
725 gmod.set_param_hint(
'y1', value=bgParsPos[2])
726 if separateNegParams:
727 gmod.set_param_hint(
'x1Neg', value=bgParsNeg[1])
728 gmod.set_param_hint(
'y1Neg', value=bgParsNeg[2])
729 if bgGradientOrder >= 2:
730 gmod.set_param_hint(
'xy', value=bgParsPos[3])
731 gmod.set_param_hint(
'x2', value=bgParsPos[4])
732 gmod.set_param_hint(
'y2', value=bgParsPos[5])
733 if separateNegParams:
734 gmod.set_param_hint(
'xyNeg', value=bgParsNeg[3])
735 gmod.set_param_hint(
'x2Neg', value=bgParsNeg[4])
736 gmod.set_param_hint(
'y2Neg', value=bgParsNeg[5])
738 y, x = np.mgrid[bbox.getBeginY():bbox.getEndY(), bbox.getBeginX():bbox.getEndX()]
739 in_x = np.array([x, y]).astype(np.float64)
740 in_x[0, :] -= in_x[0, :].mean()
741 in_x[1, :] -= in_x[1, :].mean()
745 mask = np.ones_like(z, dtype=bool)
750 weights = mask.astype(np.float64)
751 if self.
posImage is not None and rel_weight > 0.:
752 weights = np.array([np.ones_like(diArr), np.ones_like(diArr)*rel_weight,
753 np.ones_like(diArr)*rel_weight])
760 nans = (np.isnan(z) | np.isnan(weights))
764 z[nans] = np.nanmedian(z)
772 with warnings.catch_warnings():
775 warnings.filterwarnings(
"ignore",
"The keyword argument .* does not match", UserWarning)
776 result = gmod.fit(z, weights=weights, x=in_x, max_nfev=250,
780 fit_kws={
'ftol': tol,
'xtol': tol,
'gtol': tol,
784 rel_weight=rel_weight,
786 modelObj=dipoleModel)
793 print(result.fit_report(show_correl=
False))
794 if separateNegParams:
795 print(result.ci_report())
800 fitBackground=1, maxSepInSigma=5., separateNegParams=True,
801 bgGradientOrder=1, verbose=False, display=False):
802 """Fit a dipole model to an input ``diaSource`` (wraps `fitDipoleImpl`).
804 Actually, fits the subimage bounded by the input source's
805 footprint) and optionally constrain the fit using the
806 pre-subtraction images self.posImage (science) and
807 self.negImage (template). Wraps the output into a
808 `pipeBase.Struct` named tuple after computing additional
809 statistics such as orientation and SNR.
813 source : `lsst.afw.table.SourceRecord`
814 Record containing the (merged) dipole source footprint detected on the diffim
815 tol : `float`, optional
816 Tolerance parameter for scipy.leastsq() optimization
817 rel_weight : `float`, optional
818 Weighting of posImage/negImage relative to the diffim in the fit
819 fitBackground : `int`, {0, 1, 2}, optional
820 How to fit linear background gradient in posImage/negImage
822 - 0: do not fit background at all
823 - 1 (default): pre-fit the background using linear least squares and then do not fit it
824 as part of the dipole fitting optimization
825 - 2: pre-fit the background using linear least squares (as in 1), and use the parameter
826 estimates from that fit as starting parameters for an integrated "re-fit" of the
827 background as part of the overall dipole fitting optimization.
828 maxSepInSigma : `float`, optional
829 Allowed window of centroid parameters relative to peak in input source footprint
830 separateNegParams : `bool`, optional
831 Fit separate parameters to the flux and background gradient in
832 bgGradientOrder : `int`, {0, 1, 2}, optional
833 Desired polynomial order of background gradient
834 verbose: `bool`, optional
837 Display input data, best fit model(s) and residuals in a matplotlib window.
842 `pipeBase.Struct` object containing the fit parameters and other information.
845 `lmfit.MinimizerResult` object for debugging and error estimation, etc.
849 Parameter `fitBackground` has three options, thus it is an integer:
854 source, tol=tol, rel_weight=rel_weight, fitBackground=fitBackground,
855 maxSepInSigma=maxSepInSigma, separateNegParams=separateNegParams,
856 bgGradientOrder=bgGradientOrder, verbose=verbose)
860 fp = source.getFootprint()
864 if fitResult.params[
'flux'].value <= 1.:
865 self.
log.
debug(
"Fitted flux too small for id=%d; ModelResult.message='%s'",
866 source[
"id"], fitResult.message)
867 return None, fitResult
868 if not fitResult.result.errorbars:
869 self.
log.
debug(
"Could not estimate error bars for id=%d; ModelResult.message='%s'",
870 source[
"id"], fitResult.message)
871 return None, fitResult
875 posCentroid = measBase.CentroidResult(fitResult.params[
'xcenPos'].value,
876 fitResult.params[
'ycenPos'].value,
877 fitResult.params[
'xcenPos'].stderr,
878 fitResult.params[
'ycenPos'].stderr)
879 negCentroid = measBase.CentroidResult(fitResult.params[
'xcenNeg'].value,
880 fitResult.params[
'ycenNeg'].value,
881 fitResult.params[
'xcenNeg'].stderr,
882 fitResult.params[
'ycenNeg'].stderr)
883 xposIdx = fitResult.var_names.index(
"xcenPos")
884 yposIdx = fitResult.var_names.index(
"ycenPos")
885 xnegIdx = fitResult.var_names.index(
"xcenNeg")
886 ynegIdx = fitResult.var_names.index(
"ycenNeg")
887 centroid = measBase.CentroidResult((fitResult.params[
'xcenPos'] + fitResult.params[
'xcenNeg']) / 2,
888 (fitResult.params[
'ycenPos'] + fitResult.params[
'ycenNeg']) / 2.,
889 math.sqrt(posCentroid.xErr**2 + negCentroid.xErr**2
890 + 2*fitResult.covar[xposIdx, xnegIdx]) / 2.,
891 math.sqrt(posCentroid.yErr**2 + negCentroid.yErr**2
892 + 2*fitResult.covar[yposIdx, ynegIdx]) / 2.)
893 dx = fitResult.params[
'xcenPos'].value - fitResult.params[
'xcenNeg'].value
894 dy = fitResult.params[
'ycenPos'].value - fitResult.params[
'ycenNeg'].value
895 angle = np.arctan2(dy, dx)
901 def computeSumVariance(exposure, footprint):
902 return math.sqrt(np.nansum(exposure[footprint.getBBox(), afwImage.PARENT].variance.array))
905 flux = measBase.FluxResult(fitResult.params[
"flux"].value, fitResult.params[
"flux"].stderr)
906 posFlux = measBase.FluxResult(fitResult.params[
"flux"].value, fitResult.params[
"flux"].stderr)
907 negFlux = measBase.FluxResult(fitResult.params[
"flux"].value, fitResult.params[
"flux"].stderr)
909 fluxVar = computeSumVariance(self.
posImage, source.getFootprint())
911 fluxVar = computeSumVariance(self.
diffim, source.getFootprint())
914 if separateNegParams:
915 negFlux.instFlux = fitResult.params[
'fluxNeg'].value
916 negFlux.instFluxErr = fitResult.params[
'fluxNeg'].stderr
918 fluxVarNeg = computeSumVariance(self.
negImage, source.getFootprint())
921 signalToNoise = math.sqrt((posFlux.instFlux/fluxVar)**2 + (negFlux.instFlux/fluxVarNeg)**2)
922 except ZeroDivisionError:
923 signalToNoise = np.nan
925 out = Struct(posCentroid=posCentroid, negCentroid=negCentroid, centroid=centroid,
926 posFlux=posFlux, negFlux=negFlux, flux=flux, orientation=angle,
927 signalToNoise=signalToNoise, chi2=fitResult.chisqr, redChi2=fitResult.redchi,
928 nData=fitResult.ndata)
931 return out, fitResult
934 """Display data, model fits and residuals (currently uses matplotlib display functions).
938 footprint : `lsst.afw.detection.Footprint`
939 Footprint containing the dipole that was fit
940 result : `lmfit.MinimizerResult`
941 `lmfit.MinimizerResult` object returned by `lmfit` optimizer
944 import matplotlib.pyplot
as plt
945 except ImportError
as err:
946 self.
log.warning(
'Unable to import matplotlib: %s', err)
949 def display2dArray(ax, arr, x, y, xErr, yErr, title, extent=None):
950 """Use `matplotlib.pyplot.imshow` to display a 2-D array with a given coordinate range.
952 fig = ax.imshow(arr, origin=
'lower', interpolation=
'none', cmap=
'gray', extent=extent)
954 ax.errorbar(x[
"total"], y[
"total"], xErr[
"total"], yErr[
"total"], c=
"cyan")
955 ax.errorbar(x[
"Pos"], y[
"Pos"], xErr[
"Pos"], yErr[
"Pos"], c=
"green")
956 ax.errorbar(x[
"Neg"], y[
"Neg"], xErr[
"Neg"], yErr[
"Neg"], c=
"red")
960 fit = result.best_fit
961 bbox = footprint.getBBox()
962 extent = (bbox.getBeginX(), bbox.getEndX(), bbox.getBeginY(), bbox.getEndY())
965 x, y, xErr, yErr = {}, {}, {}, {}
966 for name
in (
"Pos",
"Neg"):
967 x[name] = result.best_values[f
"xcen{name}"]
968 y[name] = result.best_values[f
"ycen{name}"]
969 xErr[name] = result.params[f
"xcen{name}"].stderr
970 yErr[name] = result.params[f
"ycen{name}"].stderr
971 x[
"total"] = (result.best_values[
"xcenPos"] + result.best_values[
"xcenNeg"])/2
972 y[
"total"] = (result.best_values[
"ycenPos"] + result.best_values[
"ycenNeg"])/2
973 xErr[
"total"] = math.sqrt(xErr[
"Pos"]**2 + xErr[
"Neg"]**2)
974 yErr[
"total"] = math.sqrt(yErr[
"Pos"]**2 + yErr[
"Neg"]**2)
976 fig, axes = plt.subplots(nrows=3, ncols=3, sharex=
True, sharey=
True, figsize=(8, 8))
977 for i, label
in enumerate((
"total",
"Pos",
"Neg")):
978 display2dArray(axes[i][0], z[i, :], x, y, xErr, yErr,
979 f
'Data {label}', extent=extent)
980 display2dArray(axes[i][1], fit[i, :], x, y, xErr, yErr,
981 f
'Model {label}', extent=extent)
982 display2dArray(axes[i][2], z[i, :] - fit[i, :], x, y, xErr, yErr,
983 f
'Residual {label}', extent=extent)
985 plt.setp(axes[i][1].get_yticklabels(), visible=
False)
986 plt.setp(axes[i][2].get_yticklabels(), visible=
False)
988 plt.setp(axes[i][0].get_xticklabels(), visible=
False)
989 plt.setp(axes[i][1].get_xticklabels(), visible=
False)
990 plt.setp(axes[i][2].get_xticklabels(), visible=
False)
992 fig, axes = plt.subplots(nrows=3, ncols=3, sharex=
True, sharey=
True, figsize=(10, 2.5))
993 display2dArray(axes[0], z,
'Data', extent=extent)
994 display2dArray(axes[1],
'Model', extent=extent)
995 display2dArray(axes[2], z - fit,
'Residual', extent=extent)
997 fig.tight_layout(pad=0, w_pad=0, h_pad=0)
1001@measBase.register("ip_diffim_DipoleFit")