Beispiel #1
0
def test_gls_chi2_full_cov():
    model = get_model(
        StringIO("""
            PSRJ J1234+5678
            ELAT 0
            ELONG 0
            DM 10
            F0 1
            PEPOCH 58000
            TNRedAmp -14.227505410948254
            TNRedGam 4.91353
            TNRedC 45
            """))
    model.free_params = ["ELAT", "ELONG"]
    toas = make_fake_toas(57000, 59000, 100, model=model, error=1 * u.us)
    np.random.seed(0)
    toas.adjust_TOAs(TimeDelta(np.random.randn(len(toas)) * u.us))
    r = Residuals(toas, model)
    assert_allclose(r.calc_chi2(full_cov=True), r.calc_chi2(full_cov=False))
Beispiel #2
0
class Fitter(object):
    """ Base class for fitter.

    The fitting function should be defined as the fit_toas() method.

    Note that the Fitter object makes a deepcopy of the model, so changes to the model
    will not be noticed after the Fitter has been instantiated!  Use Fitter.model instead.

    The Fitter also caches a copy of the original model so it can be restored with reset_model()



    Parameters
    ----------
    toas : a pint TOAs instance
        The input toas.
    model : a pint timing model instance
        The initial timing model for fitting.
    """
    def __init__(self, toas, model, residuals=None):
        self.toas = toas
        self.model_init = model
        if residuals is None:
            self.resids_init = Residuals(toas=toas, model=model)
            self.reset_model()
        else:
            # residuals were provided, we're just going to use them
            # probably using GLSFitter to compute a chi-squared
            self.model = copy.deepcopy(self.model_init)
            self.resids = residuals
            self.fitresult = []
        self.method = None

    def reset_model(self):
        """Reset the current model to the initial model."""
        self.model = copy.deepcopy(self.model_init)
        self.update_resids()
        self.fitresult = []

    def update_resids(self):
        """Update the residuals. Run after updating a model parameter."""
        self.resids = Residuals(toas=self.toas, model=self.model)

    def set_fitparams(self, *params):
        """Update the "frozen" attribute of model parameters.

        Ex. fitter.set_fitparams('F0','F1')
        """
        fit_params_name = []
        for pn in params:
            if pn in self.model.params:
                fit_params_name.append(pn)
            else:
                rn = self.model.match_param_aliases(pn)
                if rn != "":
                    fit_params_name.append(rn)

        for p in self.model.params:
            getattr(self.model, p).frozen = p not in fit_params_name

    def get_allparams(self):
        """Return a dict of all param names and values."""
        return collections.OrderedDict((k, getattr(self.model, k).quantity)
                                       for k in self.model.params_ordered)

    def get_fitparams(self):
        """Return a dict of fittable param names and quantity."""
        return collections.OrderedDict((k, getattr(self.model, k))
                                       for k in self.model.params
                                       if not getattr(self.model, k).frozen)

    def get_fitparams_num(self):
        """Return a dict of fittable param names and numeric values."""
        return collections.OrderedDict((k, getattr(self.model, k).value)
                                       for k in self.model.params
                                       if not getattr(self.model, k).frozen)

    def get_fitparams_uncertainty(self):
        return collections.OrderedDict(
            (k, getattr(self.model, k).uncertainty_value)
            for k in self.model.params if not getattr(self.model, k).frozen)

    def set_params(self, fitp):
        """Set the model parameters to the value contained in the input dict.

        Ex. fitter.set_params({'F0':60.1,'F1':-1.3e-15})
        """
        # In Powell fitter this sometimes fails because after some iterations the values change from
        # plain float to Quantities. No idea why.
        if len(fitp.values()) < 1:
            return
        if isinstance(list(fitp.values())[0], u.Quantity):
            for k, v in fitp.items():
                getattr(self.model, k).value = v.value
        else:
            for k, v in fitp.items():
                getattr(self.model, k).value = v

    def set_param_uncertainties(self, fitp):
        for k, v in fitp.items():
            parunit = getattr(self.model, k).units
            getattr(self.model, k).uncertainty = v * parunit

    def get_designmatrix(self):
        return self.model.designmatrix(toas=self.toas,
                                       incfrozen=False,
                                       incoffset=True)

    def minimize_func(self, x, *args):
        """Wrapper function for the residual class, meant to be passed to
        scipy.optimize.minimize. The function must take a single list of input
        values, x, and a second optional tuple of input arguments.  It returns
        a quantity to be minimized (in this case chi^2).
        """
        self.set_params({k: v for k, v in zip(args, x)})
        self.update_resids()
        # Return chi^2
        return self.resids.chi2

    def fit_toas(self, maxiter=None):
        raise NotImplementedError

    def plot(self):
        """Make residuals plot"""
        import matplotlib.pyplot as plt
        from astropy.visualization import quantity_support

        quantity_support()
        fig, ax = plt.subplots(figsize=(16, 9))
        mjds = self.toas.get_mjds()
        ax.errorbar(mjds,
                    self.resids.time_resids,
                    yerr=self.toas.get_errors(),
                    fmt="+")
        ax.set_xlabel("MJD")
        ax.set_ylabel("Residuals")
        try:
            psr = self.model.PSR
        except:
            psr = self.model.PSRJ
        else:
            psr = "Residuals"
        ax.set_title(psr)
        ax.grid(True)
        plt.show()

    def get_summary(self, nodmx=False):
        """Return a human-readable summary of the Fitter results.
        
        Parameters
        ----------
        nodmx : bool
            Set to True to suppress printing DMX parameters in summary
        """

        # Need to check that fit has been done first!
        if not hasattr(self, "covariance_matrix"):
            log.warning(
                "fit_toas() has not been run, so pre-fit and post-fit will be the same!"
            )

        from uncertainties import ufloat
        import uncertainties.umath as um

        # First, print fit quality metrics
        s = "Fitted model using {} method with {} free parameters to {} TOAs\n".format(
            self.method, len(self.get_fitparams()), self.toas.ntoas)
        s += "Prefit residuals Wrms = {}, Postfit residuals Wrms = {}\n".format(
            self.resids_init.rms_weighted(), self.resids.rms_weighted())
        s += "Chisq = {:.3f} for {} d.o.f. for reduced Chisq of {:.3f}\n".format(
            self.resids.chi2, self.resids.dof, self.resids.chi2_reduced)
        s += "\n"

        # Next, print the model parameters
        s += "{:<14s} {:^20s} {:^28s} {}\n".format("PAR", "Prefit", "Postfit",
                                                   "Units")
        s += "{:<14s} {:>20s} {:>28s} {}\n".format("=" * 14, "=" * 20,
                                                   "=" * 28, "=" * 5)
        for pn in list(self.get_allparams().keys()):
            if nodmx and pn.startswith("DMX"):
                continue
            prefitpar = getattr(self.model_init, pn)
            par = getattr(self.model, pn)
            if par.value is not None:
                if isinstance(par, strParameter):
                    s += "{:14s} {:>20s} {:28s} {}\n".format(
                        pn, prefitpar.value, "", par.units)
                elif isinstance(par, AngleParameter):
                    # Add special handling here to put uncertainty into arcsec
                    if par.frozen:
                        s += "{:14s} {:>20s} {:>28s} {} \n".format(
                            pn, str(prefitpar.quantity), "", par.units)
                    else:
                        if par.units == u.hourangle:
                            uncertainty_unit = pint.hourangle_second
                        else:
                            uncertainty_unit = u.arcsec
                        s += "{:14s} {:>20s}  {:>16s} +/- {:.2g} \n".format(
                            pn,
                            str(prefitpar.quantity),
                            str(par.quantity),
                            par.uncertainty.to(uncertainty_unit),
                        )

                else:
                    # Assume a numerical parameter
                    if par.frozen:
                        s += "{:14s} {:20g} {:28s} {} \n".format(
                            pn, prefitpar.value, "", par.units)
                    else:
                        # s += "{:14s} {:20g} {:20g} {:20.2g} {} \n".format(
                        #     pn,
                        #     prefitpar.value,
                        #     par.value,
                        #     par.uncertainty.value,
                        #     par.units,
                        # )
                        s += "{:14s} {:20g} {:28SP} {} \n".format(
                            pn,
                            prefitpar.value,
                            ufloat(par.value, par.uncertainty.value),
                            par.units,
                        )
        # Now print some useful derived parameters
        s += "\nDerived Parameters:\n"
        if hasattr(self.model, "F0"):
            F0 = self.model.F0.quantity
            if not self.model.F0.frozen:
                p, perr = pint.utils.pferrs(F0, self.model.F0.uncertainty)
                s += "Period = {} +/- {}\n".format(p.to(u.s), perr.to(u.s))
            else:
                s += "Period = {}\n".format((1.0 / F0).to(u.s))
        if hasattr(self.model, "F1"):
            F1 = self.model.F1.quantity
            if not any([self.model.F1.frozen, self.model.F0.frozen]):
                p, perr, pd, pderr = pint.utils.pferrs(
                    F0, self.model.F0.uncertainty, F1,
                    self.model.F1.uncertainty)
                s += "Pdot = {} +/- {}\n".format(
                    pd.to(u.dimensionless_unscaled),
                    pderr.to(u.dimensionless_unscaled))
                brakingindex = 3
                s += "Characteristic age = {:.4g} (braking index = {})\n".format(
                    pint.utils.pulsar_age(F0, F1, n=brakingindex),
                    brakingindex)
                s += "Surface magnetic field = {:.3g}\n".format(
                    pint.utils.pulsar_B(F0, F1))
                s += "Magnetic field at light cylinder = {:.4g}\n".format(
                    pint.utils.pulsar_B_lightcyl(F0, F1))
                I_NS = I = 1.0e45 * u.g * u.cm**2
                s += "Spindown Edot = {:.4g} (I={})\n".format(
                    pint.utils.pulsar_edot(F0, F1, I=I_NS), I_NS)

        if hasattr(self.model, "PX"):
            if not self.model.PX.frozen:
                s += "\n"
                px = ufloat(
                    self.model.PX.quantity.to(u.arcsec).value,
                    self.model.PX.uncertainty.to(u.arcsec).value,
                )
                s += "Parallax distance = {:.3uP} pc\n".format(1.0 / px)

        # Now binary system derived parameters
        binary = None
        for x in self.model.components:
            if x.startswith("Binary"):
                binary = x
        if binary is not None:
            s += "\n"
            s += "Binary model {}\n".format(binary)

            if binary.startswith("BinaryELL1"):
                if not any([
                        self.model.EPS1.frozen,
                        self.model.EPS2.frozen,
                        self.model.TASC.frozen,
                        self.model.PB.frozen,
                ]):
                    eps1 = ufloat(
                        self.model.EPS1.quantity.value,
                        self.model.EPS1.uncertainty.value,
                    )
                    eps2 = ufloat(
                        self.model.EPS2.quantity.value,
                        self.model.EPS2.uncertainty.value,
                    )
                    tasc = ufloat(
                        # This is a time in MJD
                        self.model.TASC.quantity.mjd,
                        self.model.TASC.uncertainty.to(u.d).value,
                    )
                    pb = ufloat(
                        self.model.PB.quantity.to(u.d).value,
                        self.model.PB.uncertainty.to(u.d).value,
                    )
                    s += "Conversion from ELL1 parameters:\n"
                    ecc = um.sqrt(eps1**2 + eps2**2)
                    s += "ECC = {:P}\n".format(ecc)
                    om = um.atan2(eps1, eps2) * 180.0 / np.pi
                    if om < 0.0:
                        om += 360.0
                    s += "OM  = {:P}\n".format(om)
                    t0 = tasc + pb * om / 360.0
                    s += "T0  = {:SP}\n".format(t0)

                    s += pint.utils.ELL1_check(
                        self.model.A1.quantity,
                        ecc.nominal_value,
                        self.resids.rms_weighted(),
                        self.toas.ntoas,
                        outstring=True,
                    )
                    s += "\n"

                # Masses and inclination
                if not any([self.model.PB.frozen, self.model.A1.frozen]):
                    pbs = ufloat(
                        self.model.PB.quantity.to(u.s).value,
                        self.model.PB.uncertainty.to(u.s).value,
                    )
                    a1 = ufloat(
                        self.model.A1.quantity.to(pint.ls).value,
                        self.model.A1.uncertainty.to(pint.ls).value,
                    )
                    fm = 4.0 * np.pi**2 * a1**3 / (4.925490947e-6 * pbs**2)
                    s += "Mass function = {:SP} Msun\n".format(fm)
                    mcmed = pint.utils.companion_mass(
                        self.model.PB.quantity,
                        self.model.A1.quantity,
                        inc=60.0 * u.deg,
                        mpsr=1.4 * u.solMass,
                    )
                    mcmin = pint.utils.companion_mass(
                        self.model.PB.quantity,
                        self.model.A1.quantity,
                        inc=90.0 * u.deg,
                        mpsr=1.4 * u.solMass,
                    )
                    s += "Companion mass min, median (assuming Mpsr = 1.4 Msun) = {:.4f}, {:.4f} Msun\n".format(
                        mcmin, mcmed)

                if hasattr(self.model, "SINI"):
                    try:
                        # Put this in a try in case SINI is UNSET or an illegal value
                        if not self.model.SINI.frozen:
                            si = ufloat(
                                self.model.SINI.quantity.value,
                                self.model.SINI.uncertainty.value,
                            )
                            s += "From SINI in model:\n"
                            s += "    cos(i) = {:SP}\n".format(
                                um.sqrt(1 - si**2))
                            s += "    i = {:SP} deg\n".format(
                                um.asin(si) * 180.0 / np.pi)

                        psrmass = pint.utils.pulsar_mass(
                            self.model.PB.quantity,
                            self.model.A1.quantity,
                            self.model.M2.quantity,
                            np.arcsin(self.model.SINI.quantity),
                        )
                        s += "Pulsar mass (Shapiro Delay) = {}".format(psrmass)
                    except:
                        pass

        return s

    def print_summary(self):
        """Write a summary of the TOAs to stdout."""
        print(self.get_summary())

    def get_covariance_matrix(self,
                              with_phase=False,
                              pretty_print=False,
                              prec=3):
        """Show the parameter covariance matrix post-fit.
        If with_phase, then show and return the phase column as well.
        If pretty_print, then also pretty-print on stdout the matrix.
        prec is the precision of the floating point results.
        """
        if hasattr(self, "covariance_matrix"):
            fps = list(self.get_fitparams().keys())
            cm = self.covariance_matrix
            if with_phase:
                fps = ["PHASE"] + fps
            else:
                cm = cm[1:, 1:]
            if pretty_print:
                lens = [max(len(fp) + 2, prec + 8) for fp in fps]
                maxlen = max(lens)
                print("\nParameter covariance matrix:")
                line = "{0:^{width}}".format("", width=maxlen)
                for fp, ln in zip(fps, lens):
                    line += "{0:^{width}}".format(fp, width=ln)
                print(line)
                for ii, fp1 in enumerate(fps):
                    line = "{0:^{width}}".format(fp1, width=maxlen)
                    for jj, (fp2,
                             ln) in enumerate(zip(fps[:ii + 1],
                                                  lens[:ii + 1])):
                        line += "{0: {width}.{prec}e}".format(cm[ii, jj],
                                                              width=ln,
                                                              prec=prec)
                    print(line)
                print("\n")
            return cm
        else:
            log.error(
                "You must run .fit_toas() before accessing the covariance matrix"
            )
            raise AttributeError

    def get_correlation_matrix(self,
                               with_phase=False,
                               pretty_print=False,
                               prec=3):
        """Show the parameter correlation matrix post-fit.
        If with_phase, then show and return the phase column as well.
        If pretty_print, then also pretty-print on stdout the matrix.
        prec is the precision of the floating point results.
        """
        if hasattr(self, "correlation_matrix"):
            fps = list(self.get_fitparams().keys())
            cm = self.correlation_matrix
            if with_phase:
                fps = ["PHASE"] + fps
            else:
                cm = cm[1:, 1:]
            if pretty_print:
                lens = [max(len(fp) + 2, prec + 4) for fp in fps]
                maxlen = max(lens)
                print("\nParameter correlation matrix:")
                line = "{0:^{width}}".format("", width=maxlen)
                for fp, ln in zip(fps, lens):
                    line += "{0:^{width}}".format(fp, width=ln)
                print(line)
                for ii, fp1 in enumerate(fps):
                    line = "{0:^{width}}".format(fp1, width=maxlen)
                    for jj, (fp2, ln) in enumerate(zip(fps, lens)):
                        line += "{0:^{width}.{prec}f}".format(cm[ii, jj],
                                                              width=ln,
                                                              prec=prec)
                    print(line)
                print("\n")
            return cm
        else:
            log.error(
                "You must run .fit_toas() before accessing the correlation matrix"
            )
            raise AttributeError

    def ftest(self, parameter, component, remove=False, full_output=False):
        """Compare the significance of adding/removing parameters to a timing model.

        Parameters
        -----------
        parameter : PINT parameter object
            (may be a list of parameter objects)
        component : String
            Name of component of timing model that the parameter should be added to (may be a list)
            The number of components must equal number of parameters.
        remove : Bool
            If False, will add the listed parameters to the model. If True will remove the input
            parameters from the timing model.
        full_output : Bool
            If False, just returns the result of the F-Test. If True, will also return the new
            model's residual RMS (us), chi-squared, and number of degrees of freedom of
            new model.

        Returns
        --------
        dictionary

            ft : Float
                F-test significance value for the model with the larger number of
                components over the other. Computed with pint.utils.FTest().

            resid_rms_test : Float (Quantity)
                If full_output is True, returns the RMS of the residuals of the tested model
                fit. Will be in units of microseconds as an astropy quantity.

            resid_wrms_test : Float (Quantity)
                If full_output is True, returns the Weighted RMS of the residuals of the tested model
                fit. Will be in units of microseconds as an astropy quantity.

            chi2_test : Float
                If full_output is True, returns the chi-squared of the tested model.

            dof_test : Int
                If full_output is True, returns the degrees of freedom of the tested model.
        """
        # Copy the fitter that we do not change the initial model and fitter
        fitter_copy = copy.deepcopy(self)
        # Number of times to run the fit
        NITS = 1
        # We need the original degrees of freedome and chi-squared value
        # Because this applies to nested models, model 1 must always have fewer parameters
        if remove:
            dof_2 = self.resids.get_dof()
            chi2_2 = self.resids.calc_chi2()
        else:
            dof_1 = self.resids.get_dof()
            chi2_1 = self.resids.calc_chi2()
        # Single inputs are converted to lists to handle arb. number of parameteres
        if type(parameter) is not list:
            parameter = [parameter]
        # also do the components
        if type(component) is not list:
            component = [component]
        # if not the same length, exit with error
        if len(parameter) != len(component):
            raise RuntimeError(
                "Number of input parameters must match number of input components."
            )
        # Now check if we want to remove or add components; start with removing
        if remove:
            # Set values to zero and freeze them
            for p in parameter:
                getattr(fitter_copy.model, "{:}".format(p.name)).value = 0.0
                getattr(fitter_copy.model,
                        "{:}".format(p.name)).uncertainty_value = 0.0
                getattr(fitter_copy.model, "{:}".format(p.name)).frozen = True
            # validate and setup model
            fitter_copy.model.validate()
            fitter_copy.model.setup()
            # Now refit
            fitter_copy.fit_toas(NITS)
            # Now get the new values
            dof_1 = fitter_copy.resids.get_dof()
            chi2_1 = fitter_copy.resids.calc_chi2()
        else:
            # Dictionary of parameters to check to makes sure input value isn't zero
            check_params = {
                "M2": 0.25,
                "SINI": 0.8,
                "PB": 10.0,
                "T0": 54000.0,
                "FB0": 1.1574e-6,
            }
            # Add the parameters
            for ii in range(len(parameter)):
                # Check if parameter already exists in model
                if hasattr(fitter_copy.model,
                           "{:}".format(parameter[ii].name)):
                    # Set frozen to False
                    getattr(fitter_copy.model,
                            "{:}".format(parameter[ii].name)).frozen = False
                    # Check if parameter is one that needs to be checked
                    if parameter[ii].name in check_params.keys():
                        if parameter[ii].value == 0.0:
                            log.warning(
                                "Default value for %s cannot be 0, resetting to %s"
                                % (parameter[ii].name,
                                   check_params[parameter[ii].name]))
                            parameter[ii].value = check_params[
                                parameter[ii].name]
                    getattr(fitter_copy.model, "{:}".format(
                        parameter[ii].name)).value = parameter[ii].value
                # If not, add it to the model
                else:
                    fitter_copy.model.components[component[ii]].add_param(
                        parameter[ii], setup=True)
            # validate and setup model
            fitter_copy.model.validate()
            fitter_copy.model.setup()
            # Now refit
            fitter_copy.fit_toas(NITS)
            # Now get the new values
            dof_2 = fitter_copy.resids.get_dof()
            chi2_2 = fitter_copy.resids.calc_chi2()
        # Now run the actual F-test
        ft = FTest(chi2_1, dof_1, chi2_2, dof_2)

        if full_output:
            if remove:
                dof_test = dof_1
                chi2_test = chi2_1
            else:
                dof_test = dof_2
                chi2_test = chi2_2
            resid_rms_test = fitter_copy.resids.time_resids.std().to(u.us)
            resid_wrms_test = fitter_copy.resids.rms_weighted()  # units: us
            return {
                "ft": ft,
                "resid_rms_test": resid_rms_test,
                "resid_wrms_test": resid_wrms_test,
                "chi2_test": chi2_test,
                "dof_test": dof_test,
            }
        else:
            return {"ft": ft}