Ejemplo n.º 1
0
    def __init__(self, model_path=None, scalar_path=None, save_num=1):
        """Initialise the data by loading the pickled models and scalar.

        Parameters
        ----------
        model_path : str, optional
            The path to the model to be used to predict the flux. By default, this path points to ``models/kri`` in ``breidablik``.
        scalar_path : str, optional
            The path to the scalar corresponding to the model. By default, this path points to ``models/kri/scalar.npy`` in ``breidablik``.
        save_num : int
            The number of cubic spline models to save. This makes the code run faster, but takes memory. Only worth increasing if you are repeatedly analysing different observations of multiple stars.
        """

        # set default paths
        model_path = model_path or _base_path.parent / 'models/kri'
        scalar_path = scalar_path or _base_path.parent / 'models/kri/scalar.npy'
        # load models
        self.scalar = Scalar()
        self.scalar.load(scalar_path)
        self.models = joblib.load(os.path.join(model_path, 'kri.pkl'))
        self.relative_error = np.load(os.path.join(model_path,
                                                   'relative_err.npy'),
                                      allow_pickle=False)
        self.save_num = save_num
        self.cut_models = None
        self.saved_cspline = {}
        self.saved_cut_cspline = {}
Ejemplo n.º 2
0
    def __init__(self,
                 model_path=None,
                 scalar_path=None,
                 model_path_610=None,
                 scalar_path_610=None,
                 model_path_810=None,
                 scalar_path_810=None):
        """Initialise the data by reading the pickled models and scalar.

        Parameters
        ----------
        model_path : str, optional
            The path to the rew model to be used to predict the lithium abundance. By default, this path points to ``models/rew.pkl`` in ``breidablik``.
        scalar_path : str, optional
            The path to the scalar corresponding to the rew model. By default, this path points to ``models/rew_scalar.pkl`` in ``breidablik``.
        """

        # set default paths
        model_path = model_path or _base_path.parent / 'models/rew'
        scalar_path = scalar_path or _base_path.parent / 'models/rew/scalar.npy'
        model_path_610 = model_path_610 or _base_path.parent / 'models/rew_610'
        scalar_path_610 = scalar_path_610 or _base_path.parent / 'models/rew_610/scalar.npy'
        model_path_810 = model_path_810 or _base_path.parent / 'models/rew_810'
        scalar_path_810 = scalar_path_810 or _base_path.parent / 'models/rew_810/scalar.npy'
        # load models
        scalar = Scalar()
        scalar.load(scalar_path)
        scalar_610 = Scalar()
        scalar_610.load(scalar_path_610)
        scalar_810 = Scalar()
        scalar_810.load(scalar_path_810)
        self.models = [
            load.load(model_path_610),
            load.load(model_path),
            load.load(model_path_810)
        ]
        self.scalars = [scalar_610, scalar, scalar_810]
Ejemplo n.º 3
0
 def setup_class(cls):
     cls.models = Scalar()
Ejemplo n.º 4
0
class Spectra:
    """Interpolation class for spectra. Used to interpolate between the stellar parameters. Can find the abundance of an input flux given the stellar parameters. Can also predict a flux from the stellar parameters and abundance.
    """
    def __init__(self, model_path=None, scalar_path=None, save_num=1):
        """Initialise the data by loading the pickled models and scalar.

        Parameters
        ----------
        model_path : str, optional
            The path to the model to be used to predict the flux. By default, this path points to ``models/kri`` in ``breidablik``.
        scalar_path : str, optional
            The path to the scalar corresponding to the model. By default, this path points to ``models/kri/scalar.npy`` in ``breidablik``.
        save_num : int
            The number of cubic spline models to save. This makes the code run faster, but takes memory. Only worth increasing if you are repeatedly analysing different observations of multiple stars.
        """

        # set default paths
        model_path = model_path or _base_path.parent / 'models/kri'
        scalar_path = scalar_path or _base_path.parent / 'models/kri/scalar.npy'
        # load models
        self.scalar = Scalar()
        self.scalar.load(scalar_path)
        self.models = joblib.load(os.path.join(model_path, 'kri.pkl'))
        self.relative_error = np.load(os.path.join(model_path,
                                                   'relative_err.npy'),
                                      allow_pickle=False)
        self.save_num = save_num
        self.cut_models = None
        self.saved_cspline = {}
        self.saved_cut_cspline = {}

    def find_abund(self,
                   wavelength,
                   flux,
                   flux_err,
                   eff_t,
                   surf_g,
                   met,
                   accuracy=1e-4,
                   method='bayes',
                   min_abund=-0.5,
                   max_abund=4,
                   initial_accuracy=1e-1,
                   abunds=None,
                   prior=None):
        """Finds the abundance of the spectrum.

        Parameters
        ----------
        wavelength : List[Real] or 1darray
            The wavelengths that correspond to the flux. Needs to be monotonically increasing.
        flux : List[Real] or 1darray
            The flux that the abundance will be found for. Needs to be the same length as wavelength.
        flux_err : List[Real] or 1darray
            The error in each flux point. Needs to be the same length as wavelength.
        eff_t : Real
            The effective temperature of the star.
        surf_g : Real
            The log surface gravity of the star.
        met : Real
            The metallicity of the star.
        accuracy : Real, optional
            The decimal place you want the result to be accurate to.
        method : str, optional
            The method of finding the abundance. Accepted methods are: 'bayes' and 'chisq'.
        min_abund : Real, optional
            The minimum abundance that the algorithm should search to.
        max_abund : Real, optional
            The maximum abundance that the algorithm should search to.
        initial_accuracy : Real, optional
            The initial accuracy that the algorithm starts searching through. If 'bayes' is returning warnings try decreasing the initial accuracy. Note that this does make the algorithm run slower.
        abunds : List[Real], 1darray, optional
            Determine the abundances you want the probability caculated over. Overrides the min_abund and max_abund parameters. This parameter is ignored if prior is not set. Only used if method is 'bayes'.
        prior : List[Real], 1darray, optional
            Set the prior to the abundances specified via abunds. This parameter is ignored if abunds is not set. Only used if method is 'bayes'. If method is 'bayes' but no prior is set, uses uniform prior. Needs to be the same length as abunds.

        Returns
        -------
        (abundance, error) : (float, list)
            The abundance that matches best with the input flux and the error value associated with the abundance. The error list has 2 values, left is the negative error, right is the positive error. The error can be None if the method is 'chisq' or if abunds and prior were given but error cannot be calculated.
        """

        # change it all to numpy arrays
        wavelength = np.array(wavelength)
        flux = np.array(flux)
        flux_err = np.array(flux_err)

        # make sure wavelength is monotonically increasing
        if not np.all(wavelength[1:] > wavelength[:-1]):
            raise ValueError(
                'Wavelength needs to be monotonically increasing.')
        # make sure wavelength, flux, flux_err all are 1D arrays with the same length
        if not (wavelength.shape == flux.shape) and (
                flux.shape == flux_err.shape) and (len(wavelength.shape) == 1):
            raise ValueError(
                'Wavelength, flux, and flux_err needs to 1D array and have the same shape. Detected shapes: wavelength {}, flux {}, flux_err {}'
                .format(wavelength.shape, flux.shape, flux_err.shape))
        # make sure method is an accepted method
        if not ((method == 'bayes') or (method == 'chisq')):
            raise ValueError(
                'Invalid method, detected input: {}'.format(method))
        # make sure min abund < max abund
        if min_abund > max_abund:
            raise ValueError(
                'minimum abundance is bigger than maximum abundance, detected input: min_abund = {}, max_abund = {}'
                .format(min_abund, max_abund))

        # warn if prior/abunds is set but method is not bayes
        if (method == 'chisq') and ((abunds is not None) or
                                    (prior is not None)):
            warnings.warn(
                'method is set to chisq but abunds or prior is not None, ignoring the abunds and prior inputs.'
            )
        # warn if prior is defined but abunds is not or vice versa.
        if (abunds is not None) and (prior is None):
            warnings.warn(
                'abunds is defined but prior is not. Both needs to be defined or else abunds is ignored.'
            )
        if (prior is not None) and (abunds is None):
            warnings.warn(
                'prior is defined but abunds is not. Both needs to be defined or else prior is ignored.'
            )
        # warn if stellar parameters are too far outside the edge of the grid
        _grid_check(eff_t, surf_g, met)

        # makes things go vroom vroom. Predictions take a long time
        lower_wl = min(wavelength)
        upper_wl = max(wavelength)
        balder_wl = read.get_wavelengths()
        mask = (lower_wl <= balder_wl) & (balder_wl <= upper_wl)
        if (mask == False).all(
        ):  # check if the input wavelength is encompassed by our data
            raise ValueError(
                'Input wavelength does not overlap with the model data. Minimum input wavelength : {}, maximum input wavelength, minimum model data wavelength, maximum model data wavelength: {}'
                .format(wavelength[0], wavelength[-1], balder_wl[0],
                        balder_wl[-1]))
        self.cut_wl = balder_wl[mask]
        inds = np.where(mask == True)[0]
        ind_l = inds[0]
        ind_u = inds[-1] + 1
        self.cut_models = {}
        for abund in list(self.models):
            self.cut_models[abund] = self.models[abund][ind_l:ind_u]

        err = None
        if method == 'bayes':
            # if there is prior and abunds
            if (prior is not None) and (abunds is not None):
                prior = np.array(prior)
                abunds = np.array(abunds)
                if (abunds.shape != prior.shape) or (len(
                        abunds.shape) != 1) or (len(
                            prior.shape) != 1):  # each abundance needs a prior
                    raise ValueError(
                        'The length of abundance and prior should be the same, they should also be 1D arrays, detected shape: abunds: {}, prior: {}'
                        .format(abunds.shape, prior.shape))
                probs = self._bayesian_inference(wavelength,
                                                 flux,
                                                 flux_err,
                                                 eff_t,
                                                 surf_g,
                                                 met,
                                                 abunds,
                                                 prior=prior)
                # if we have the whole pdf
                tolerance = 1e-5
                if (probs > tolerance).any() and (probs[0] < tolerance) and (
                        probs[-1] < tolerance):
                    abundance, err = self._calc_bayes_err(abunds, probs)
                # if we only have part of the pdf
                else:
                    warnings.warn(
                        'Only the abundance is returned. The probability distribution function is not fully covered by the input abunds and prior, therefore, the error in abundance cannot be calculated. Increase the range of the input abunds to cover the rest of the probability distribution function to get an error estimate. Minimum input abunds = {}, corresponding probability = {}; maximum input abunds = {}, corresponding probability = {}.'
                        .format(abunds[0], probs[0], abunds[-1], probs[-1]))
                    abundance = abunds[np.argmax(probs)]
            # if there is no prior
            else:
                abundance, err = self._coarse_search(wavelength,
                                                     flux,
                                                     flux_err,
                                                     eff_t,
                                                     surf_g,
                                                     met,
                                                     min_abund=min_abund,
                                                     max_abund=max_abund,
                                                     accuracy=accuracy)
        else:
            abundance = self._window_search(wavelength,
                                            flux,
                                            flux_err,
                                            eff_t,
                                            surf_g,
                                            met,
                                            accuracy=accuracy,
                                            min_abund=min_abund,
                                            max_abund=max_abund,
                                            initial_accuracy=initial_accuracy)

        # warn if predicted Li is outside of grid
        if (abundance < -0.5) or (abundance > 4):
            warnings.warn(
                'Predicted lithium abundance is outside of the grid, results are extrapolated and may not be reliable.'
            )

        return (abundance, err)

    def _window_search(self,
                       wavelength,
                       flux,
                       flux_err,
                       eff_t,
                       surf_g,
                       met,
                       accuracy=1e-4,
                       min_abund=-0.5,
                       max_abund=4,
                       initial_accuracy=1e-1):
        """An algorithm that finds the minimum - variation of ternary search. Used to make finding the abundances faster. Only for chisq method.
        """

        current_accuracy = initial_accuracy
        once = False
        while abs(current_accuracy - accuracy) > accuracy:
            abunds = np.arange(min_abund, max_abund + current_accuracy / 2,
                               current_accuracy)
            # find the index of the best abundance
            res_sq = self._chisq(wavelength, flux, flux_err, abunds, eff_t,
                                 surf_g, met)
            best_abund_index = np.argmin(res_sq)

            # update search window
            if best_abund_index == 0:  # leftmost value
                max_abund = abunds[1]
            elif best_abund_index == (len(abunds) - 1):  # rightmost value
                min_abund = abunds[-2]
            else:
                min_abund = abunds[best_abund_index - 1]
                max_abund = abunds[best_abund_index + 1]
            current_accuracy /= 10
        abundance = abunds[best_abund_index]
        return abundance

    def _coarse_search(self,
                       wavelength,
                       flux,
                       flux_err,
                       eff_t,
                       surf_g,
                       met,
                       min_abund,
                       max_abund,
                       accuracy=1e-4,
                       initial_accuracy=1e-1):

        # get initial probabilities
        abunds = np.arange(min_abund, max_abund + initial_accuracy / 2,
                           initial_accuracy)
        probs = self._bayesian_inference(wavelength, flux, flux_err, eff_t,
                                         surf_g, met, abunds)

        tolerance = 1e-5
        # not a flat line
        if (probs > tolerance).any():
            # if we have found the pdf
            if (probs[0] < tolerance) and (probs[-1] < tolerance):
                # find smallest range of abundances we can feed into bayesian inference
                max_ind = np.argmax(probs)
                left = probs[:max_ind]
                l_ind = len(left[left <= tolerance]) - 1
                right = probs[max_ind + 1:]
                r_ind = len(probs) - len(right[right <= tolerance])
                # find pdf
                fine_abunds = np.arange(abunds[l_ind],
                                        abunds[r_ind] + accuracy / 2, accuracy)
                fine_probs = self._bayesian_inference(wavelength, flux,
                                                      flux_err, eff_t, surf_g,
                                                      met, fine_abunds)
                return self._calc_bayes_err(fine_abunds, fine_probs)
            # if we haven't found the pdf
            half = (max_abund - min_abund) / 2
            # if we have found left half the pdf
            if (probs[0] < tolerance) and (probs[-1] > tolerance):
                l_half = 0
                r_half = half
            # if we have found right half of the pdf
            elif (probs[0] > tolerance) and (probs[-1] < tolerance):
                l_half = half
                r_half = 0
            # if we have found the peak but not the edges
            else:
                l_half = half
                r_half = half
            return self._coarse_search(wavelength,
                                       flux,
                                       flux_err,
                                       eff_t,
                                       surf_g,
                                       met,
                                       min_abund - l_half,
                                       max_abund + r_half,
                                       accuracy=accuracy,
                                       initial_accuracy=initial_accuracy)
        # if we have not found the pdf
        else:
            raise ValueError(
                'Either the actual abundance of the spectra is not between the min_abund and max_abund, or the initial_accuracy value is too large. Try finding the actual abundance through the chisq method, if this abundance is between the min_abund and max_abund, then make the initial_accuracy smaller. Detected inputs: min_abund = {}, max_abund = {}, initial_accuracy = {}'
                .format(min_abund, max_abund, initial_accuracy))

    def _calc_bayes_err(self, abunds, probs):
        """Find the best abundance through bayesian inference and also calculate the error associated with this measurement.
        """

        # find best abundance
        best_abund = abunds[np.argmax(probs)]
        # find errors
        cumsum = np.cumsum(probs)
        cdf = cumsum / cumsum[-1]
        l_sig = 0.5 - 0.34
        r_sig = 0.5 + 0.34
        l_abund = abunds[np.argmin(np.abs(cdf - l_sig))]
        r_abund = abunds[np.argmin(np.abs(cdf - r_sig))]
        l_err = l_abund - best_abund
        r_err = r_abund - best_abund
        return (best_abund, [l_err, r_err])

    def _bayesian_inference(self,
                            wavelength,
                            flux,
                            flux_err,
                            eff_t,
                            surf_g,
                            met,
                            abunds,
                            prior=None):
        """Use Bayesian optimisation to find the probability of the model (abundance) given data.
        """

        probs = []
        guess_fluxes = self._predict_flux(eff_t, surf_g, met, abunds)
        if prior is None:
            prior = np.full(len(guess_fluxes), 1)

        for g_flux, p, a in zip(guess_fluxes, prior, abunds):
            model = CubicSpline(self.cut_wl, g_flux)
            probability = self._p_model_given_data(wavelength, flux, flux_err,
                                                   p, model)
            probs.append(probability)
        probs = np.array(probs)
        exp_probs = np.exp(probs - max(probs))
        width = abunds[1] - abunds[0]
        rescaled_probs = exp_probs / (
            np.sum(exp_probs) * width
        )  # the probabilities were logged in _p_model_given_data, normalise before taking the exponential to prevent overflow
        return rescaled_probs

    def _p_model_given_data(self, xdata, ydata, sigma, prior, model):
        """Bayesian inference - probability of data given model. The prior is for the input model.
        """

        xdata = np.array(xdata)
        ydata = np.array(ydata)
        ln_p_data_given_model = np.sum(-(ydata - model(xdata))**2 /
                                       (2 * sigma**2))
        posterior = ln_p_data_given_model + np.log(prior)
        return posterior

    def _chisq(self, wavelength, flux, flux_err, abunds, eff_t, surf_g, met):
        """Calculates the least squares of the input abundances.
        """

        guess_flux = self._predict_flux(eff_t, surf_g, met, abunds)
        int_flux = [
            CubicSpline(self.cut_wl, g_flux)(wavelength)
            for g_flux in guess_flux
        ]
        diff = np.array(int_flux) - np.array(flux)
        chi_square_ind = np.square(diff) / np.square(flux_err)
        chi_sq = np.sum(chi_square_ind, axis=1)
        return chi_sq

    def predict_flux(self, eff_t, surf_g, met, abundance):
        """Predicts the flux for the input stellar parameters and abundances.

        Parameters
        ----------
        eff_t : Real
            The effective temperature of the star.
        surf_g : Real
            The log surface gravity of the star.
        met : Real
            The metallicity of the star.
        abundance : Real
            The lithium abundance of the star.

        Returns
        -------
        predicted : 1darray
            The predicted flux given the input stellar parameters and lithium abundance.
        """

        # check the input stellar parameters and abundance
        if not ((np.array(eff_t).shape == ()) and
                (np.array(surf_g).shape == ()) and
                (np.array(met).shape == ()) and
                (np.array(abundance).shape == ())):
            raise ValueError(
                'The input effective temperature, surface gravity, metallicity, or abundance is not in the right format, they all need to be scalar numbers, detected inputs: eff_t = {}, surf_g = {}, met = {}, and abund = {}'
                .format(eff_t, surf_g, met, abundance))
        # warn if stellar parameters are too far outside the edge of the grid
        _grid_check(eff_t, surf_g, met)
        # warn if abundance is outside the grid range
        if (abundance < -0.5) or (abundance > 4):
            warnings.warn(
                'Input abundance is outside of the grid, results are extrapolated and may not be reliable.'
            )

        return self._predict_flux(eff_t,
                                  surf_g,
                                  met, [abundance],
                                  user_call=True)[0]

    def _predict_flux(self, eff_t, surf_g, met, abundance, user_call=False):
        """Same as predict_flux. This is the hidden version without asserts for improved performance. The flux is only predicted in the region of the cut_models (where cut_models are determined by the input observed spectrum).

        Also, you can call this version with a list of abundances. It's faster if you call this function with a list of abundances vs calling the user visible one with a for-loop over abundances. Vroom vroom.
        """

        if (self.cut_models is not None) and (
                not user_call):  # only predict a range of models
            saved_cspline = self.saved_cut_cspline
            cut = True
        else:  # predict all models
            saved_cspline = self.saved_cspline
            cut = False

        if (eff_t, surf_g, met
            ) in list(saved_cspline):  # if the cubic spline is already saved
            splines = saved_cspline[eff_t, surf_g, met]
        else:  # if new stellar parameters
            splines = self._create_cspline(eff_t, surf_g, met, cut=cut)

        # evaluate the splines at the desired abundances
        predicted = np.array([s(abundance) for s in splines]).T
        predicted = 1 + self.relative_error - 10**predicted

        return predicted

    def _create_cspline(self, eff_t, surf_g, met, cut=True):
        """Creates a cubic spline for each abundance for the input stellar parameters. The pixels for which these cubic splines are created over is determined by cut.
        """

        # get correct models and cubic splines
        if cut:
            models = self.cut_models
            cspline = self.saved_cut_cspline
        else:
            models = self.models
            cspline = self.saved_cspline

        # transform input
        transformed_input = self.scalar.transform([[eff_t, surf_g, met]])[0]

        # evaluating the models for the input stellar parameters
        abunds = list(models)
        grid_spec = []
        for a in abunds:
            spec = []
            for m in models[a]:
                try:  # can't train on outputs that are all the same
                    point = m.execute('points',
                                      *transformed_input,
                                      backend='loop')[0][0]
                except AttributeError:  # append the value in that case
                    point = m
                spec.append(point)
            grid_spec.append(spec)
        grid_spec = np.array(grid_spec).T

        # create splines over abundances
        splines = []
        for spec in grid_spec:
            splines.append(CubicSpline(abunds, spec))

        # save in dictionary and remove exceeded number
        if len(list(cspline)) + 1 > self.save_num:
            del cspline[list(cspline)[0]]
        cspline[eff_t, surf_g, met] = splines

        return splines