LSST Applications g00d0e8bbd7+edbf708997,g199a45376c+5137f08352,g1fd858c14a+48cd4dd530,g228ff663f5+2051e4e242,g262e1987ae+9c6f24d2e3,g29ae962dfc+03663621e0,g2cef7863aa+73c82f25e4,g35bb328faa+edbf708997,g3fd5ace14f+8c4d25a1ce,g47891489e3+27ba970c8a,g53246c7159+edbf708997,g5b326b94bb+db962c32ee,g64539dfbff+d237af7fd9,g67b6fd64d1+27ba970c8a,g74acd417e5+8234f56c0c,g786e29fd12+af89c03590,g87389fa792+a4172ec7da,g88cb488625+6878ed1c5e,g89139ef638+27ba970c8a,g8d7436a09f+f76ea57dde,g8ea07a8fe4+79658f16ab,g90f42f885a+6577634e1f,g97be763408+494f77a6c4,g98df359435+1750ea0126,g9b50b81019+d8f85438e7,ga2180abaac+edbf708997,ga9e74d7ce9+128cc68277,gad4c79568f+321c5e11c3,gbf99507273+edbf708997,gc2a301910b+d237af7fd9,gca7fc764a6+27ba970c8a,gcedae5159b+afaec0eb3d,gd7ef33dd92+27ba970c8a,gdab6d2f7ff+8234f56c0c,gdbb4c4dda9+d237af7fd9,ge410e46f29+27ba970c8a,ge41e95a9f2+d237af7fd9,geaed405ab2+062dfc8cdc,w.2025.45
LSST Data Management Base Package
Loading...
Searching...
No Matches
lsst.ip.diffim.dipoleFitTask.DipoleFitAlgorithm Class Reference

Public Member Functions

 __init__ (self, diffim, posImage=None, negImage=None)
 
 fitDipoleImpl (self, source, tol=1e-7, rel_weight=0.5, fitBackground=1, bgGradientOrder=1, maxSepInSigma=5., separateNegParams=True, verbose=False)
 
 fitDipole (self, source, tol=1e-7, rel_weight=0.1, fitBackground=1, maxSepInSigma=5., separateNegParams=True, bgGradientOrder=1, verbose=False, display=False)
 
 displayFitResults (self, footprint, result)
 

Public Attributes

 diffim = diffim
 
 posImage = posImage
 
 negImage = negImage
 
 psfSigma = None
 
 log = logging.getLogger(__name__)
 
 debug = lsstDebug.Info(__name__).debug
 

Static Protected Attributes

str _private_version_ = '0.0.5'
 

Detailed Description

Fit a dipole model using an image difference.

See also:
`DMTN-007: Dipole characterization for image differencing  <https://dmtn-007.lsst.io>`_.

Definition at line 490 of file dipoleFitTask.py.

Constructor & Destructor Documentation

◆ __init__()

lsst.ip.diffim.dipoleFitTask.DipoleFitAlgorithm.__init__ ( self,
diffim,
posImage = None,
negImage = None )
Algorithm to run dipole measurement on a diaSource

Parameters
----------
diffim : `lsst.afw.image.Exposure`
    Exposure on which the diaSources were detected
posImage : `lsst.afw.image.Exposure`
    "Positive" exposure from which the template was subtracted
negImage : `lsst.afw.image.Exposure`
    "Negative" exposure which was subtracted from the posImage

Definition at line 516 of file dipoleFitTask.py.

516 def __init__(self, diffim, posImage=None, negImage=None):
517 """Algorithm to run dipole measurement on a diaSource
518
519 Parameters
520 ----------
521 diffim : `lsst.afw.image.Exposure`
522 Exposure on which the diaSources were detected
523 posImage : `lsst.afw.image.Exposure`
524 "Positive" exposure from which the template was subtracted
525 negImage : `lsst.afw.image.Exposure`
526 "Negative" exposure which was subtracted from the posImage
527 """
528
529 self.diffim = diffim
530 self.posImage = posImage
531 self.negImage = negImage
532 self.psfSigma = None
533 if diffim is not None:
534 diffimPsf = diffim.getPsf()
535 diffimAvgPos = diffimPsf.getAveragePosition()
536 self.psfSigma = diffimPsf.computeShape(diffimAvgPos).getDeterminantRadius()
537
538 self.log = logging.getLogger(__name__)
539
540 import lsstDebug
541 self.debug = lsstDebug.Info(__name__).debug
542

Member Function Documentation

◆ displayFitResults()

lsst.ip.diffim.dipoleFitTask.DipoleFitAlgorithm.displayFitResults ( self,
footprint,
result )
Display data, model fits and residuals (currently uses matplotlib display functions).

Parameters
----------
footprint : `lsst.afw.detection.Footprint`
    Footprint containing the dipole that was fit
result : `lmfit.MinimizerResult`
    `lmfit.MinimizerResult` object returned by `lmfit` optimizer

Definition at line 933 of file dipoleFitTask.py.

933 def displayFitResults(self, footprint, result):
934 """Display data, model fits and residuals (currently uses matplotlib display functions).
935
936 Parameters
937 ----------
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
942 """
943 try:
944 import matplotlib.pyplot as plt
945 except ImportError as err:
946 self.log.warning('Unable to import matplotlib: %s', err)
947 raise err
948
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.
951 """
952 fig = ax.imshow(arr, origin='lower', interpolation='none', cmap='gray', extent=extent)
953 ax.set_title(title)
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")
957 return fig
958
959 z = result.data
960 fit = result.best_fit
961 bbox = footprint.getBBox()
962 extent = (bbox.getBeginX(), bbox.getEndX(), bbox.getBeginY(), bbox.getEndY())
963
964 if z.shape[0] == 3:
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)
975
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)
984
985 plt.setp(axes[i][1].get_yticklabels(), visible=False)
986 plt.setp(axes[i][2].get_yticklabels(), visible=False)
987 if i != 2: # remove top two row x-axis labels
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)
991 else:
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)
996
997 fig.tight_layout(pad=0, w_pad=0, h_pad=0)
998 plt.show()
999
1000
1001@measBase.register("ip_diffim_DipoleFit")

◆ fitDipole()

lsst.ip.diffim.dipoleFitTask.DipoleFitAlgorithm.fitDipole ( self,
source,
tol = 1e-7,
rel_weight = 0.1,
fitBackground = 1,
maxSepInSigma = 5.,
separateNegParams = True,
bgGradientOrder = 1,
verbose = False,
display = False )
Fit a dipole model to an input ``diaSource`` (wraps `fitDipoleImpl`).

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.

Parameters
----------
source : `lsst.afw.table.SourceRecord`
    Record containing the (merged) dipole source footprint detected on the diffim
tol : `float`, optional
    Tolerance parameter for scipy.leastsq() optimization
rel_weight : `float`, optional
    Weighting of posImage/negImage relative to the diffim in the fit
fitBackground : `int`, {0, 1, 2}, optional
    How to fit linear background gradient in posImage/negImage

        - 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.
maxSepInSigma : `float`, optional
    Allowed window of centroid parameters relative to peak in input source footprint
separateNegParams : `bool`, optional
    Fit separate parameters to the flux and background gradient in
bgGradientOrder : `int`, {0, 1, 2}, optional
    Desired polynomial order of background gradient
verbose: `bool`, optional
    Be verbose
display
    Display input data, best fit model(s) and residuals in a matplotlib window.

Returns
-------
result : `struct`
    `pipeBase.Struct` object containing the fit parameters and other information.

result : `callable`
    `lmfit.MinimizerResult` object for debugging and error estimation, etc.

Notes
-----
Parameter `fitBackground` has three options, thus it is an integer:

Definition at line 799 of file dipoleFitTask.py.

801 bgGradientOrder=1, verbose=False, display=False):
802 """Fit a dipole model to an input ``diaSource`` (wraps `fitDipoleImpl`).
803
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.
810
811 Parameters
812 ----------
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
821
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
835 Be verbose
836 display
837 Display input data, best fit model(s) and residuals in a matplotlib window.
838
839 Returns
840 -------
841 result : `struct`
842 `pipeBase.Struct` object containing the fit parameters and other information.
843
844 result : `callable`
845 `lmfit.MinimizerResult` object for debugging and error estimation, etc.
846
847 Notes
848 -----
849 Parameter `fitBackground` has three options, thus it is an integer:
850
851 """
852
853 fitResult = self.fitDipoleImpl(
854 source, tol=tol, rel_weight=rel_weight, fitBackground=fitBackground,
855 maxSepInSigma=maxSepInSigma, separateNegParams=separateNegParams,
856 bgGradientOrder=bgGradientOrder, verbose=verbose)
857
858 # Display images, model fits and residuals (currently uses matplotlib display functions)
859 if display:
860 fp = source.getFootprint()
861 self.displayFitResults(fp, fitResult)
862
863 # usually around 0.1 -- the minimum flux allowed -- i.e. bad fit.
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
872
873 # TODO: We could include covariances, which could be derived from
874 # `fitResult.params[name].correl`, but those are correlations.
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)
896
897 # Extract flux value, compute signalToNoise from flux/variance_within_footprint
898 # Also extract the stderr of flux estimate.
899 # TODO: should this instead use the lmfit-computed uncertainty from
900 # `lmfitResult.result.uvars['flux'].std_dev`?
901 def computeSumVariance(exposure, footprint):
902 return math.sqrt(np.nansum(exposure[footprint.getBBox(), afwImage.PARENT].variance.array))
903
904 # NOTE: These will all be the same unless separateNegParams=True!
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)
908 if self.posImage is not None:
909 fluxVar = computeSumVariance(self.posImage, source.getFootprint())
910 else:
911 fluxVar = computeSumVariance(self.diffim, source.getFootprint())
912 fluxVarNeg = fluxVar
913
914 if separateNegParams:
915 negFlux.instFlux = fitResult.params['fluxNeg'].value
916 negFlux.instFluxErr = fitResult.params['fluxNeg'].stderr
917 if self.negImage is not None:
918 fluxVarNeg = computeSumVariance(self.negImage, source.getFootprint())
919
920 try:
921 signalToNoise = math.sqrt((posFlux.instFlux/fluxVar)**2 + (negFlux.instFlux/fluxVarNeg)**2)
922 except ZeroDivisionError: # catch divide by zero - should never happen.
923 signalToNoise = np.nan
924
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)
929
930 # fitResult may be returned for debugging
931 return out, fitResult
932

◆ fitDipoleImpl()

lsst.ip.diffim.dipoleFitTask.DipoleFitAlgorithm.fitDipoleImpl ( self,
source,
tol = 1e-7,
rel_weight = 0.5,
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.

Parameters
----------
source : TODO: DM-17458
    TODO: DM-17458
tol : float, optional
    TODO: DM-17458
rel_weight : `float`, optional
    TODO: DM-17458
fitBackground : `int`, optional
    TODO: DM-17458
bgGradientOrder : `int`, optional
    TODO: DM-17458
maxSepInSigma : `float`, optional
    TODO: DM-17458
separateNegParams : `bool`, optional
    TODO: DM-17458
verbose : `bool`, optional
    TODO: DM-17458

Returns
-------
result : `lmfit.MinimizerResult`
    return `lmfit.MinimizerResult` object containing the fit
    parameters and other information.

Definition at line 543 of file dipoleFitTask.py.

545 separateNegParams=True, verbose=False):
546 """Fit a dipole model to an input difference image.
547
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.
551
552 Parameters
553 ----------
554 source : TODO: DM-17458
555 TODO: DM-17458
556 tol : float, optional
557 TODO: DM-17458
558 rel_weight : `float`, optional
559 TODO: DM-17458
560 fitBackground : `int`, optional
561 TODO: DM-17458
562 bgGradientOrder : `int`, optional
563 TODO: DM-17458
564 maxSepInSigma : `float`, optional
565 TODO: DM-17458
566 separateNegParams : `bool`, optional
567 TODO: DM-17458
568 verbose : `bool`, optional
569 TODO: DM-17458
570
571 Returns
572 -------
573 result : `lmfit.MinimizerResult`
574 return `lmfit.MinimizerResult` object containing the fit
575 parameters and other information.
576 """
577
578 # Only import lmfit if someone wants to use the new DipoleFitAlgorithm.
579 import lmfit
580
581 fp = source.getFootprint()
582 bbox = fp.getBBox()
583 subim = afwImage.MaskedImageF(self.diffim.getMaskedImage(), bbox=bbox, origin=afwImage.PARENT)
584
585 z = diArr = subim.image.array
586 # Make sure we don't overwrite buffers.
587 z = z.copy()
588 weights = 1. / subim.variance.array # get the weights (=1/variance)
589
590 if rel_weight > 0. and ((self.posImage is not None) or (self.negImage is not None)):
591 if self.negImage is not None:
592 negSubim = afwImage.MaskedImageF(self.negImage.getMaskedImage(), bbox, origin=afwImage.PARENT)
593 if self.posImage is not None:
594 posSubim = afwImage.MaskedImageF(self.posImage.getMaskedImage(), bbox, origin=afwImage.PARENT)
595 if self.posImage is None: # no science image provided; generate it from diffim + negImage
596 posSubim = subim.clone()
597 posSubim += negSubim
598 if self.negImage is None: # no template provided; generate it from the posImage - diffim
599 negSubim = posSubim.clone()
600 negSubim -= subim
601
602 z = np.append([z], [posSubim.image.array,
603 negSubim.image.array], axis=0)
604 # Weight the pos/neg images by rel_weight relative to the diffim
605 weights = np.append([weights], [1. / posSubim.variance.array * rel_weight,
606 1. / negSubim.variance.array * rel_weight], axis=0)
607 else:
608 rel_weight = 0. # a short-cut for "don't include the pre-subtraction data"
609
610 # It seems that `lmfit` requires a static functor as its optimized method, which eliminates
611 # the ability to pass a bound method or other class method. Here we write a wrapper which
612 # makes this possible.
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,
616 **kwargs):
617 """Generate dipole model with given parameters.
618
619 It simply defers to `modelObj.makeModel()`, where `modelObj` comes
620 out of `kwargs['modelObj']`.
621 """
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)
627
628 dipoleModel = DipoleModel()
629
630 modelFunctor = dipoleModelFunctor # dipoleModel.makeModel does not work for now.
631 # Create the lmfit model (lmfit uses scipy 'leastsq' option by default - Levenberg-Marquardt)
632 # We have to (later) filter out the nans by hand in our input to gmod.fit().
633 # The only independent variable in the model is "x"; lmfit tries to
634 # introspect variables and parameters from the function signature, but
635 # gets it wrong for the model signature above.
636 gmod = lmfit.Model(modelFunctor, independent_vars=["x"], verbose=verbose)
637
638 # Add the constraints for centroids, fluxes.
639 # starting constraint - near centroid of footprint
640 fpCentroid = np.array([fp.getCentroid().getX(), fp.getCentroid().getY()])
641 cenNeg = cenPos = fpCentroid
642
643 pks = fp.getPeaks()
644
645 if len(pks) >= 1:
646 cenPos = pks[0].getF() # if individual (merged) peaks were detected, use those
647 if len(pks) >= 2: # peaks are already sorted by centroid flux so take the most negative one
648 cenNeg = pks[-1].getF()
649
650 # For close/faint dipoles the starting locs (min/max) might be way off, let's help them a bit.
651 # First assume dipole is not separated by more than 5*psfSigma.
652 maxSep = self.psfSigma * maxSepInSigma
653
654 # As an initial guess -- assume the dipole is close to the center of the footprint.
655 if np.sum(np.sqrt((np.array(cenPos) - fpCentroid)**2.)) > maxSep:
656 cenPos = fpCentroid
657 if np.sum(np.sqrt((np.array(cenNeg) - fpCentroid)**2.)) > maxSep:
658 cenPos = fpCentroid
659
660 # parameter hints/constraints: https://lmfit.github.io/lmfit-py/model.html#model-param-hints-section
661 # might make sense to not use bounds -- see http://lmfit.github.io/lmfit-py/bounds.html
662 # also see this discussion -- https://github.com/scipy/scipy/issues/3129
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)
671
672 # Use the (flux under the dipole)*5 for an estimate.
673 # Lots of testing showed that having startingFlux be too high was better than too low.
674 startingFlux = np.nansum(np.abs(diArr) - np.nanmedian(np.abs(diArr))) * 5.
675 posFlux = negFlux = startingFlux
676
677 # TBD: set max. flux limit?
678 gmod.set_param_hint('flux', value=posFlux, min=0.1)
679
680 if separateNegParams:
681 # TBD: set max negative lobe flux limit?
682 gmod.set_param_hint('fluxNeg', value=np.abs(negFlux), min=0.1)
683
684 # Fixed parameters (don't fit for them if there are no pre-sub images or no gradient fit requested):
685 # Right now (fitBackground == 1), we fit a linear model to the background and then subtract
686 # it from the data and then don't fit the background again (this is faster).
687 # A slower alternative (fitBackground == 2) is to use the estimated background parameters as
688 # starting points in the integrated model fit. That is currently not performed by default,
689 # but might be desirable in some cases.
690 bgParsPos = bgParsNeg = (0., 0., 0.)
691 if ((rel_weight > 0.) and (fitBackground != 0) and (bgGradientOrder >= 0)):
692 pbg = 0.
693 bgFitImage = self.posImage if self.posImage is not None else self.negImage
694 # Fit the gradient to the background (linear model)
695 bgParsPos = bgParsNeg = dipoleModel.fitFootprintBackground(source, bgFitImage,
696 order=bgGradientOrder)
697
698 # Generate the gradient and subtract it from the pre-subtraction image data
699 if fitBackground == 1:
700 in_x = dipoleModel._generateXYGrid(bbox)
701 pbg = dipoleModel.makeBackgroundModel(in_x, tuple(bgParsPos))
702 z[1, :] -= pbg
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)
706
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))
711 z[2, :] -= pbg
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)
716
717 # Do not subtract the background from the images but include the background parameters in the fit
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])
737
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() # center it!
741 in_x[1, :] -= in_x[1, :].mean()
742
743 # Instead of explicitly using a mask to ignore flagged pixels, just set the ignored pixels'
744 # weights to 0 in the fit. TBD: need to inspect mask planes to set this mask.
745 mask = np.ones_like(z, dtype=bool) # TBD: set mask values to False if the pixels are to be ignored
746
747 # I'm not sure about the variance planes in the diffim (or convolved pre-sub. images
748 # for that matter) so for now, let's just do an un-weighted least-squares fit
749 # (override weights computed above).
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])
754
755 # Set the weights to zero if mask is False
756 if np.any(~mask):
757 weights[~mask] = 0.
758
759 # Filter out any nans, and make the weights 0.
760 nans = (np.isnan(z) | np.isnan(weights))
761 nNans = nans.sum()
762 if nNans > 0:
763 if nNans < len(z):
764 z[nans] = np.nanmedian(z)
765 else:
766 z[nans] = 0
767 weights[nans] = 0
768
769 # Note that although we can, we're not required to set initial values for params here,
770 # since we set their param_hint's above.
771 # Can add "method" param to not use 'leastsq' (==levenberg-marquardt), e.g. "method='nelder'"
772 with warnings.catch_warnings():
773 # Ignore lmfit unknown argument warnings:
774 # "psf, rel_weight, footprint, modelObj" all become pass-through kwargs for makeModel.
775 warnings.filterwarnings("ignore", "The keyword argument .* does not match", UserWarning)
776 result = gmod.fit(z, weights=weights, x=in_x, max_nfev=250,
777 method="leastsq", # TODO: try using `least_squares` here for speed/robustness
778 verbose=verbose,
779 # see scipy docs for the meaning of these keywords
780 fit_kws={'ftol': tol, 'xtol': tol, 'gtol': tol,
781 # Our model is float32 internally, so we need a larger epsfcn.
782 'epsfcn': 1e-8},
783 psf=self.diffim.getPsf(), # hereon: kwargs that get passed to makeModel()
784 rel_weight=rel_weight,
785 footprint=fp,
786 modelObj=dipoleModel)
787
788 if verbose: # the ci_report() seems to fail if neg params are constrained -- TBD why.
789 # Never wanted in production - this takes a long time (longer than the fit!)
790 # This is how to get confidence intervals out:
791 # https://lmfit.github.io/lmfit-py/confidence.html and
792 # http://cars9.uchicago.edu/software/python/lmfit/model.html
793 print(result.fit_report(show_correl=False))
794 if separateNegParams:
795 print(result.ci_report())
796
797 return result
798

Member Data Documentation

◆ _private_version_

str lsst.ip.diffim.dipoleFitTask.DipoleFitAlgorithm._private_version_ = '0.0.5'
staticprotected

Definition at line 499 of file dipoleFitTask.py.

◆ debug

lsst.ip.diffim.dipoleFitTask.DipoleFitAlgorithm.debug = lsstDebug.Info(__name__).debug

Definition at line 541 of file dipoleFitTask.py.

◆ diffim

lsst.ip.diffim.dipoleFitTask.DipoleFitAlgorithm.diffim = diffim

Definition at line 529 of file dipoleFitTask.py.

◆ log

lsst.ip.diffim.dipoleFitTask.DipoleFitAlgorithm.log = logging.getLogger(__name__)

Definition at line 538 of file dipoleFitTask.py.

◆ negImage

lsst.ip.diffim.dipoleFitTask.DipoleFitAlgorithm.negImage = negImage

Definition at line 531 of file dipoleFitTask.py.

◆ posImage

lsst.ip.diffim.dipoleFitTask.DipoleFitAlgorithm.posImage = posImage

Definition at line 530 of file dipoleFitTask.py.

◆ psfSigma

lsst.ip.diffim.dipoleFitTask.DipoleFitAlgorithm.psfSigma = None

Definition at line 532 of file dipoleFitTask.py.


The documentation for this class was generated from the following file: