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
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
587 z = z.copy()
588 weights = 1. / subim.variance.array
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:
596 posSubim = subim.clone()
597 posSubim += negSubim
598 if self.negImage is None:
599 negSubim = posSubim.clone()
600 negSubim -= subim
601
602 z = np.append([z], [posSubim.image.array,
603 negSubim.image.array], axis=0)
604
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.
609
610
611
612
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
631
632
633
634
635
636 gmod = lmfit.Model(modelFunctor, independent_vars=["x"], verbose=verbose)
637
638
639
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()
647 if len(pks) >= 2:
648 cenNeg = pks[-1].getF()
649
650
651
652 maxSep = self.psfSigma * maxSepInSigma
653
654
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
661
662
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
673
674 startingFlux = np.nansum(np.abs(diArr) - np.nanmedian(np.abs(diArr))) * 5.
675 posFlux = negFlux = startingFlux
676
677
678 gmod.set_param_hint('flux', value=posFlux, min=0.1)
679
680 if separateNegParams:
681
682 gmod.set_param_hint('fluxNeg', value=np.abs(negFlux), min=0.1)
683
684
685
686
687
688
689
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
695 bgParsPos = bgParsNeg = dipoleModel.fitFootprintBackground(source, bgFitImage,
696 order=bgGradientOrder)
697
698
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
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()
741 in_x[1, :] -= in_x[1, :].mean()
742
743
744
745 mask = np.ones_like(z, dtype=bool)
746
747
748
749
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
756 if np.any(~mask):
757 weights[~mask] = 0.
758
759
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
770
771
772 with warnings.catch_warnings():
773
774
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",
778 verbose=verbose,
779
780 fit_kws={'ftol': tol, 'xtol': tol, 'gtol': tol,
781
782 'epsfcn': 1e-8},
783 psf=self.diffim.getPsf(),
784 rel_weight=rel_weight,
785 footprint=fp,
786 modelObj=dipoleModel)
787
788 if verbose:
789
790
791
792
793 print(result.fit_report(show_correl=False))
794 if separateNegParams:
795 print(result.ci_report())
796
797 return result
798