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
637 gmod = lmfit.Model(modelFunctor, independent_vars=[
"x"], verbose=verbose)
641 fpCentroid = np.array([fp.getCentroid().getX(), fp.getCentroid().getY()])
642 cenNeg = cenPos = fpCentroid
647 cenPos = pks[0].getF()
649 cenNeg = pks[-1].getF()
653 maxSep = self.
psfSigma * maxSepInSigma
656 if np.sum(np.sqrt((np.array(cenPos) - fpCentroid)**2.)) > maxSep:
658 if np.sum(np.sqrt((np.array(cenNeg) - fpCentroid)**2.)) > maxSep:
664 gmod.set_param_hint(
'xcenPos', value=cenPos[0],
665 min=cenPos[0]-maxSep, max=cenPos[0]+maxSep)
666 gmod.set_param_hint(
'ycenPos', value=cenPos[1],
667 min=cenPos[1]-maxSep, max=cenPos[1]+maxSep)
668 gmod.set_param_hint(
'xcenNeg', value=cenNeg[0],
669 min=cenNeg[0]-maxSep, max=cenNeg[0]+maxSep)
670 gmod.set_param_hint(
'ycenNeg', value=cenNeg[1],
671 min=cenNeg[1]-maxSep, max=cenNeg[1]+maxSep)
675 startingFlux = np.nansum(np.abs(diArr) - np.nanmedian(np.abs(diArr))) * 5.
676 posFlux = negFlux = startingFlux
679 gmod.set_param_hint(
'flux', value=posFlux, min=0.1)
681 if separateNegParams:
683 gmod.set_param_hint(
'fluxNeg', value=np.abs(negFlux), min=0.1)
691 bgParsPos = bgParsNeg = (0., 0., 0.)
692 if ((rel_weight > 0.)
and (fitBackground != 0)
and (bgGradientOrder >= 0)):
696 bgParsPos = bgParsNeg = dipoleModel.fitFootprintBackground(source, bgFitImage,
697 order=bgGradientOrder)
700 if fitBackground == 1:
701 in_x = dipoleModel._generateXYGrid(bbox)
702 pbg = dipoleModel.makeBackgroundModel(in_x, tuple(bgParsPos))
704 z[1, :] -= np.nanmedian(z[1, :])
705 posFlux = np.nansum(z[1, :])
706 gmod.set_param_hint(
'flux', value=posFlux*1.5, min=0.1)
708 if separateNegParams
and self.
negImage is not None:
709 bgParsNeg = dipoleModel.fitFootprintBackground(source, self.
negImage,
710 order=bgGradientOrder)
711 pbg = dipoleModel.makeBackgroundModel(in_x, tuple(bgParsNeg))
713 z[2, :] -= np.nanmedian(z[2, :])
714 if separateNegParams:
715 negFlux = np.nansum(z[2, :])
716 gmod.set_param_hint(
'fluxNeg', value=negFlux*1.5, min=0.1)
719 if fitBackground == 2:
720 if bgGradientOrder >= 0:
721 gmod.set_param_hint(
'b', value=bgParsPos[0])
722 if separateNegParams:
723 gmod.set_param_hint(
'bNeg', value=bgParsNeg[0])
724 if bgGradientOrder >= 1:
725 gmod.set_param_hint(
'x1', value=bgParsPos[1])
726 gmod.set_param_hint(
'y1', value=bgParsPos[2])
727 if separateNegParams:
728 gmod.set_param_hint(
'x1Neg', value=bgParsNeg[1])
729 gmod.set_param_hint(
'y1Neg', value=bgParsNeg[2])
730 if bgGradientOrder >= 2:
731 gmod.set_param_hint(
'xy', value=bgParsPos[3])
732 gmod.set_param_hint(
'x2', value=bgParsPos[4])
733 gmod.set_param_hint(
'y2', value=bgParsPos[5])
734 if separateNegParams:
735 gmod.set_param_hint(
'xyNeg', value=bgParsNeg[3])
736 gmod.set_param_hint(
'x2Neg', value=bgParsNeg[4])
737 gmod.set_param_hint(
'y2Neg', value=bgParsNeg[5])
739 y, x = np.mgrid[bbox.getBeginY():bbox.getEndY(), bbox.getBeginX():bbox.getEndX()]
740 in_x = np.array([x, y]).astype(np.float64)
741 in_x[0, :] -= in_x[0, :].mean()
742 in_x[1, :] -= in_x[1, :].mean()
746 mask = np.ones_like(z, dtype=bool)
751 weights = mask.astype(np.float64)
752 if self.
posImage is not None and rel_weight > 0.:
753 weights = np.array([np.ones_like(diArr), np.ones_like(diArr)*rel_weight,
754 np.ones_like(diArr)*rel_weight])
761 nans = (np.isnan(z) | np.isnan(weights))
765 z[nans] = np.nanmedian(z)
773 with warnings.catch_warnings():
776 warnings.filterwarnings(
"ignore",
"The keyword argument .* does not match", UserWarning)
777 result = gmod.fit(z, weights=weights, x=in_x, max_nfev=250,
781 fit_kws={
'ftol': tol,
'xtol': tol,
'gtol': tol,
785 rel_weight=rel_weight,
787 modelObj=dipoleModel)
794 print(result.fit_report(show_correl=
False))
795 if separateNegParams:
796 print(result.ci_report())
801 fitBackground=1, maxSepInSigma=5., separateNegParams=True,
802 bgGradientOrder=1, verbose=False, display=False):
803 """Fit a dipole model to an input ``diaSource`` (wraps `fitDipoleImpl`).
805 Actually, fits the subimage bounded by the input source's
806 footprint) and optionally constrain the fit using the
807 pre-subtraction images self.posImage (science) and
808 self.negImage (template). Wraps the output into a
809 `pipeBase.Struct` named tuple after computing additional
810 statistics such as orientation and SNR.
814 source : `lsst.afw.table.SourceRecord`
815 Record containing the (merged) dipole source footprint detected on the diffim
816 tol : `float`, optional
817 Tolerance parameter for scipy.leastsq() optimization
818 rel_weight : `float`, optional
819 Weighting of posImage/negImage relative to the diffim in the fit
820 fitBackground : `int`, {0, 1, 2}, optional
821 How to fit linear background gradient in posImage/negImage
823 - 0: do not fit background at all
824 - 1 (default): pre-fit the background using linear least squares and then do not fit it
825 as part of the dipole fitting optimization
826 - 2: pre-fit the background using linear least squares (as in 1), and use the parameter
827 estimates from that fit as starting parameters for an integrated "re-fit" of the
828 background as part of the overall dipole fitting optimization.
829 maxSepInSigma : `float`, optional
830 Allowed window of centroid parameters relative to peak in input source footprint
831 separateNegParams : `bool`, optional
832 Fit separate parameters to the flux and background gradient in
833 bgGradientOrder : `int`, {0, 1, 2}, optional
834 Desired polynomial order of background gradient
835 verbose: `bool`, optional
838 Display input data, best fit model(s) and residuals in a matplotlib window.
843 `pipeBase.Struct` object containing the fit parameters and other information.
846 `lmfit.MinimizerResult` object for debugging and error estimation, etc.
850 Parameter `fitBackground` has three options, thus it is an integer:
855 source, tol=tol, rel_weight=rel_weight, fitBackground=fitBackground,
856 maxSepInSigma=maxSepInSigma, separateNegParams=separateNegParams,
857 bgGradientOrder=bgGradientOrder, verbose=verbose)
861 fp = source.getFootprint()
865 if fitResult.params[
'flux'].value <= 1.:
866 self.
log.
debug(
"Fitted flux too small for id=%d; ModelResult.message='%s'",
867 source[
"id"], fitResult.message)
868 return None, fitResult
869 if not fitResult.result.errorbars:
870 self.
log.
debug(
"Could not estimate error bars for id=%d; ModelResult.message='%s'",
871 source[
"id"], fitResult.message)
872 return None, fitResult
876 posCentroid = measBase.CentroidResult(fitResult.params[
'xcenPos'].value,
877 fitResult.params[
'ycenPos'].value,
878 fitResult.params[
'xcenPos'].stderr,
879 fitResult.params[
'ycenPos'].stderr)
880 negCentroid = measBase.CentroidResult(fitResult.params[
'xcenNeg'].value,
881 fitResult.params[
'ycenNeg'].value,
882 fitResult.params[
'xcenNeg'].stderr,
883 fitResult.params[
'ycenNeg'].stderr)
884 xposIdx = fitResult.var_names.index(
"xcenPos")
885 yposIdx = fitResult.var_names.index(
"ycenPos")
886 xnegIdx = fitResult.var_names.index(
"xcenNeg")
887 ynegIdx = fitResult.var_names.index(
"ycenNeg")
888 centroid = measBase.CentroidResult((fitResult.params[
'xcenPos'] + fitResult.params[
'xcenNeg']) / 2,
889 (fitResult.params[
'ycenPos'] + fitResult.params[
'ycenNeg']) / 2.,
890 math.sqrt(posCentroid.xErr**2 + negCentroid.xErr**2
891 + 2*fitResult.covar[xposIdx, xnegIdx]) / 2.,
892 math.sqrt(posCentroid.yErr**2 + negCentroid.yErr**2
893 + 2*fitResult.covar[yposIdx, ynegIdx]) / 2.)
894 dx = fitResult.params[
'xcenPos'].value - fitResult.params[
'xcenNeg'].value
895 dy = fitResult.params[
'ycenPos'].value - fitResult.params[
'ycenNeg'].value
896 angle = np.arctan2(dy, dx)
902 def computeSumVariance(exposure, footprint):
903 return math.sqrt(np.nansum(exposure[footprint.getBBox(), afwImage.PARENT].variance.array))
906 flux = measBase.FluxResult(fitResult.params[
"flux"].value, fitResult.params[
"flux"].stderr)
907 posFlux = measBase.FluxResult(fitResult.params[
"flux"].value, fitResult.params[
"flux"].stderr)
908 negFlux = measBase.FluxResult(fitResult.params[
"flux"].value, fitResult.params[
"flux"].stderr)
910 fluxVar = computeSumVariance(self.
posImage, source.getFootprint())
912 fluxVar = computeSumVariance(self.
diffim, source.getFootprint())
915 if separateNegParams:
916 negFlux.instFlux = fitResult.params[
'fluxNeg'].value
917 negFlux.instFluxErr = fitResult.params[
'fluxNeg'].stderr
919 fluxVarNeg = computeSumVariance(self.
negImage, source.getFootprint())
922 signalToNoise = math.sqrt((posFlux.instFlux/fluxVar)**2 + (negFlux.instFlux/fluxVarNeg)**2)
923 except ZeroDivisionError:
924 signalToNoise = np.nan
926 out = Struct(posCentroid=posCentroid, negCentroid=negCentroid, centroid=centroid,
927 posFlux=posFlux, negFlux=negFlux, flux=flux, orientation=angle,
928 signalToNoise=signalToNoise, chi2=fitResult.chisqr, redChi2=fitResult.redchi,
929 nData=fitResult.ndata)
932 return out, fitResult
935 """Display data, model fits and residuals (currently uses matplotlib display functions).
939 footprint : `lsst.afw.detection.Footprint`
940 Footprint containing the dipole that was fit
941 result : `lmfit.MinimizerResult`
942 `lmfit.MinimizerResult` object returned by `lmfit` optimizer
945 import matplotlib.pyplot
as plt
946 except ImportError
as err:
947 self.
log.warning(
'Unable to import matplotlib: %s', err)
950 def display2dArray(ax, arr, x, y, xErr, yErr, title, extent=None):
951 """Use `matplotlib.pyplot.imshow` to display a 2-D array with a given coordinate range.
953 fig = ax.imshow(arr, origin=
'lower', interpolation=
'none', cmap=
'gray', extent=extent)
955 ax.errorbar(x[
"total"], y[
"total"], xErr[
"total"], yErr[
"total"], c=
"cyan")
956 ax.errorbar(x[
"Pos"], y[
"Pos"], xErr[
"Pos"], yErr[
"Pos"], c=
"green")
957 ax.errorbar(x[
"Neg"], y[
"Neg"], xErr[
"Neg"], yErr[
"Neg"], c=
"red")
961 fit = result.best_fit
962 bbox = footprint.getBBox()
963 extent = (bbox.getBeginX(), bbox.getEndX(), bbox.getBeginY(), bbox.getEndY())
966 x, y, xErr, yErr = {}, {}, {}, {}
967 for name
in (
"Pos",
"Neg"):
968 x[name] = result.best_values[f
"xcen{name}"]
969 y[name] = result.best_values[f
"ycen{name}"]
970 xErr[name] = result.params[f
"xcen{name}"].stderr
971 yErr[name] = result.params[f
"ycen{name}"].stderr
972 x[
"total"] = (result.best_values[
"xcenPos"] + result.best_values[
"xcenNeg"])/2
973 y[
"total"] = (result.best_values[
"ycenPos"] + result.best_values[
"ycenNeg"])/2
974 xErr[
"total"] = math.sqrt(xErr[
"Pos"]**2 + xErr[
"Neg"]**2)
975 yErr[
"total"] = math.sqrt(yErr[
"Pos"]**2 + yErr[
"Neg"]**2)
977 fig, axes = plt.subplots(nrows=3, ncols=3, sharex=
True, sharey=
True, figsize=(8, 8))
978 for i, label
in enumerate((
"total",
"Pos",
"Neg")):
979 display2dArray(axes[i][0], z[i, :], x, y, xErr, yErr,
980 f
'Data {label}', extent=extent)
981 display2dArray(axes[i][1], fit[i, :], x, y, xErr, yErr,
982 f
'Model {label}', extent=extent)
983 display2dArray(axes[i][2], z[i, :] - fit[i, :], x, y, xErr, yErr,
984 f
'Residual {label}', extent=extent)
986 plt.setp(axes[i][1].get_yticklabels(), visible=
False)
987 plt.setp(axes[i][2].get_yticklabels(), visible=
False)
989 plt.setp(axes[i][0].get_xticklabels(), visible=
False)
990 plt.setp(axes[i][1].get_xticklabels(), visible=
False)
991 plt.setp(axes[i][2].get_xticklabels(), visible=
False)
993 fig, axes = plt.subplots(nrows=3, ncols=3, sharex=
True, sharey=
True, figsize=(10, 2.5))
994 display2dArray(axes[0], z,
'Data', extent=extent)
995 display2dArray(axes[1],
'Model', extent=extent)
996 display2dArray(axes[2], z - fit,
'Residual', extent=extent)
998 fig.tight_layout(pad=0, w_pad=0, h_pad=0)
1002@measBase.register("ip_diffim_DipoleFit")