Ejemplo n.º 1
0
def get_table_path(filename, datadir="data"):
    """

    Parameters
    ----------
    filename:

    datadir:

    Returns
    -------
    path: string
        The path to the data file
    """

    if filename in available_methods():
        filename += ".hdf5"

    if datadir == "data":
        path = utils.get_path_to_file_from_here(filename, subdirs=["data"])

    return path
Ejemplo n.º 2
0
def create(method,
           output_dir='data',
           filename=None,
           zmin=0,
           zmax=20,
           num_samples=10000,
           **kwargs):
    """
    Creates an interpolated 1D redshift lookup table which can be read
    in  using :func:`~fruitbat.table.load`.

    Parameters
    ----------
    method : str
        The DM-redshift relation to assume when creating the table.

    output_dir : str, optional
        The path of the output directory. If ``output_dir = 'data'``,
        then created table will created in the same directory with
        the builtin tables and will be found when using functions
        such as :meth:`~fruitbat._frb.calc_redshift()`.
        Default: 'data'

    filename : str, optional
        The output filename. If ``name=None`` then the filename will
        become custom_method. Default: *None*

    zmin : int or float, optional
        The minimum redshift in the table. Default: 0

    zmax : int or float, optional
        The maximum redshift in the table. Default: 20

    num_samples : int, optional
        The number of sample dispersion measure and redshifts.
        Default: 10000

    Keyword Arguments
    -----------------
    cosmo : An instance of :obj:`astropy.cosmology`, optional
        The cosmology to assume when calculating the outputs of the
        table. Required when creating new tables of ``'Ioka2003'``,
        ``'Inoue2004'``, ``'Zhang2018'``.

    free_elec : float or int, optional
        The amount of free electrons per proton mass in the Universe.
        This applies when using ``'Zhang2018'``. Must be between 0
        and 1. Default: 0.875.

    f_igm : float or int, optional
        The fraction of baryons in the intergalactic medium. This
        applies when using ``'Zhang2018'``. Must be between 0 and 1.
        Default: 0.83

    Generates
    ---------
    A compressed file in .npz format containing the arrays ``'dm'``
    and ``'z'``.

    Example
    -------
    >>> def simple_dm(z):
        dm = 1200 * z
        return dm
    >>> fruitbat.add_method("simple_dm", simple_dm)
    >>> fruitbat.table.create("simple_dm")
    >>> frb = fruitbat.Frb(1200)
    >>> frb.calc_redshift(method="simple_dm")
    <Quantity 1.>

    """

    if method not in available_methods():
        err_msg = ("{} is not a valid method."
                   "The currently defined methods "
                   "are: {}".format(method, available_methods()))
        raise ValueError(err_msg)

    else:
        dm_function = method_functions()[method]

        z_vals = np.linspace(zmin, zmax, num_samples)
        dm_vals = np.array([dm_function(z, **kwargs) for z in z_vals])

        if filename is not None:
            output_name = filename
        else:
            output_name = "custom_{}".format(method)

        if output_dir == 'data':
            output_file = os.path.join(os.path.dirname(__file__), 'data',
                                       output_name)
        else:
            output_file = os.path.join(output_dir, output_name)

        np.savez(output_file, dm=dm_vals, z=z_vals)
Ejemplo n.º 3
0
import os
import pandas as pd

from e13tools import docstring_substitute

from fruitbat import Frb, methods, cosmologies

__all__ = ["create_analysis_catalogue", "create_methods_catalogue",
           "read_frb_row"]


@docstring_substitute(meth=methods.available_methods(),
                      cosmo=cosmologies.available_cosmologies())
def create_analysis_catalogue(filename="fruitbat_analysis_catalogue",
                              dataset='default', method='Inoue2004',
                              cosmology='Planck18'):
    """
    Analyses an FRB dataset and produces a CSV file containing the
    estimated redshift, fluence, energy and luminosity for each FRB in
    additional to its measured quantities.

    Parameters
    ----------
    filename: str, optional
        The output file name. Default: 'fruitbat_analysis_catalogue'

    dataset: str, optional
        The path to the FRBCAT dataset. The dataset is required to have
        the following columns: 'frb_name', 'utc', 'telescope',
        'rop_raj', 'rop_decj', 'rop_gl', 'rop_gb', 'rop_bandwidth',
        'rop_centre_frequency', 'rmp_dm', 'rmp_width', 'rmp_snr',
Ejemplo n.º 4
0
 def test_reset_methods(self):
     methods.reset_methods()
     assert "new_method" not in methods.available_methods()
Ejemplo n.º 5
0
 def test_add_method(self):
     methods.add_method("new_method", self.new_method)
     assert "new_method" in methods.available_methods()
Ejemplo n.º 6
0
    def calc_redshift(self,
                      method='Batten2021',
                      cosmology="Planck18",
                      subtract_host=False,
                      lookup_table=None):
        """
        Calculate the redshift of the FRB from its :attr:`dm`,
        :attr:`dm_excess` or :attr:`dm_excess` - :attr:`dm_host_est`.

        Parameters
        ----------
        method : str, optional
            The dispersion meausre -redshift relation to use when
            calculating the redshift. Avaliable methods:  %(meth)s.
            Default: 'Inoue2004'

        cosmology : str, optional
            The cosmology to assume when calculating the redshift.
            Avaliable cosmologies: %(cosmo)s. Default: 'Planck18'

        subtract_host : bool, optional
            Subtract :attr:`dm_host_est` from the :attr:`dm_excess`
            before calculating the redshift. This is is used to account
            for the dispersion measure that arises from the FRB host
            galaxy. Default: False

        lookup_table : str or None, optional
            The path to the lookup table file. If ``lookup_table=None``
            a table will attempted to be loaded from the data directory
            based on the method name. Default: *None*

        Returns
        -------
        float
            The redshift of the FRB.

        Notes
        -----
        The methods_ section in the documentation has a description for
        each of the builtin methods.

        The cosmology_ section in the documentation has a list of the
        cosmological parameters for each cosmology

        .. _cosmology: https://fruitbat.readthedocs.io/en/latest/user_guide/method_and_cosmology.html#cosmology
        .. _methods: https://fruitbat.readthedocs.io/en/latest/user_guide/method_and_cosmology.html#methods

        """
        utils.check_type("subtract_host", subtract_host, bool)

        if subtract_host:
            input_dm = self.dm_excess - self.dm_host_est
            print(input_dm)
        else:
            input_dm = self.dm_excess

        # If the user provides a table use that table for estimation.
        if lookup_table is not None:
            raise NotImplementedError("Not made yet!")

        if method not in methods.available_methods():
            err_msg = ("Method '{}' is not a valid method. "
                       "Valid methods are: {}".format(
                           method, methods.available_methods()))
            raise ValueError(err_msg)

        if method in methods.methods_hydrodynamic():
            zvals, pdf, dz = self.calc_redshift_pdf(method=method)
            z_median = utils.calc_median_from_pdf(zvals, pdf)
            self.z = z_median
            self.method = method
            if method == "Batten2020":
                self.cosmology = "EAGLE"

        elif method in methods.methods_analytic():
            if cosmology in cosmologies.available_cosmologies():
                #if method in methods.builtin_method_functions().keys():
                table_name = table.get_table_path(method)
                self.z = table.get_z_from_table(input_dm, table_name,
                                                cosmology)
                self.cosmology = cosmology
                self.method = method

            else:
                err_msg = ("Cosmology '{}' is not a valid cosmology. Valid "
                           "cosmologies are: {}".format(
                               cosmology, cosmologies.available_cosmologies()))
                raise ValueError(err_msg)

        else:
            table_name = utils.get_path_to_file_from_here(
                "{}.hdf5".format(method), subdirs=["data"])
            self.z = table.get_z_from_table(input_dm, table_name)
            self.cosmology = cosmology
            self.method = method

        return self.z
Ejemplo n.º 7
0
class Frb(object):
    """
    Create a :class:`~Frb` object using the observered properties of a
    FRB including dispersion measure (DM) and its sky coordinates. This
    class utilises various DM-redshift relations as well as the YMW16
    galactic DM model in the analysis.

    A FRB can be defined simply with a single :attr:`dm` value. This
    should be the observed DM prior to subtracting the Milky Way,
    however if you do not provide any additional information, it is
    asssumed that this value is also the :attr:`dm_excess` (DM -
    Galactic DM). You can get the estimated redshift by using the
    method :meth:`calc_redshift`.

    To get a more accurate distance estimate you can account for the
    contribution due to the Milky Way by supplying :attr:`dm_galaxy` or
    by giving the coordinates of the FRB in ICRS (:attr:`raj`,
    :attr:`decj`) or Galactic (:attr:`gl`, :attr:`gb`) coordinates and
    calling the method :meth:`calc_dm_galaxy` before calling
    :meth:`calc_redshift`.


    Parameters
    ----------
    dm : float
        The observed dispersion measure of the FRB. This is without
        Milky Way or host galaxy subtraction. Units: pc cm**-3

    Keyword Arguments
    -----------------
    name : str or None, optional
        The name of the frb object. Default: *None*

    raj : str or None, optional
        The right ascension in J2000 coordinates of the best estimate
        of the FRB position. By default should be given in the ICRS
        frame. An alternative frame can be specified by also including
        the keyword ``frame``. e.g. ``frame='fk5'``. Default: *None*

    decj : str or None, optional
        The declination in J2000 coordinates of the best estimate of
        the FRB position. By default should be given in the ICRS frame.
        An alternative frame can be specified by also including the
        keyword ``frame``. e.g. ``frame='fk5'``. Default: *None*

    gl : str or None, optional
        The Galactic longitude in degrees of the best estimate of the
        FRB position. Default: *None*

    gb : str or None, optional
        The Galactic latitude in degrees of the best estimate of the
        FRB position. Default: *None*

    Other Parameters
    ----------------
    dm_galaxy : float, optional
        The modelled contribution to the FRB DM by electrons in the
        Milky Way. This value is calculated using
        :meth:`calc_dm_galaxy` Units: pc cm**-3 Default: 0.0

    dm_excess : float or None, optional
        The DM excess of the FRB over the estimated Galactic DM. If
        :attr:`dm_excess` is *None*, then :attr:`dm_excess` is
        calculated automatically with :meth:`calc_dm_excess`.
        Units: pc cm**-3 Default: *None*

    z_host : float or None, optional
        The observed redshift of the localised FRB host galaxy.
        Default: *None*

    dm_host_est : float, optional
        The estimated contribution to the measured FRB DM originating
        from the FRB's host galaxy. This value is the amount of DM the
        host galaxy contributes to the observed DM, *not* the DM of the
        host galaxy. Setting this to a non-zero value and setting
        ``subtract_host=True`` when calling :meth:`calc_redshift`
        accounts for the DM contribution due to the host galaxy.
        Units: pc cm**-3 Default: 0.0

    dm_host_loc : float, optional
        The dispersion measure of a localised FRB host galaxy. This
        value is *not* the contribution to the observed DM, but the DM
        at the host galaxy. The observed DM is :attr:`dm_host_loc` but
        attenuated by a factor of (1 + z). To use this, the redshift of
        the host galaxy must be known. Units: pc cm**-3 Default: 0.0

    dm_index : float or None, optional
        The dispersion measure index of the burst :math:`\\alpha` such
        that :math:`\\rm{DM} \\propto \\nu^{-\\alpha}` Default: *None*

    scatt_index : float or None, optional
        The scattering index (:math:`\\beta`) of the FRB pulse. The
        scattering index describes how the width (:math:`\\rm{W}`) of
        the FRB pulse evolves with frequency :math:`\\nu` such that
        :math:`\\rm{W} \\propto \\nu^{-\\beta}`. Default: *None*

    snr : float or None, optional
        The signal-to-noise ratio of the burst.
        Default: *None*

    width : float or None, optional
        The observed width of the pulse obtained by a pulse fitting
        algorithm. Units: ms Default: *None*

    peak_flux : float or None, optional
        The observed peak flux density of the burst. Units: Jy
        Default: *None*

    fluence : float or None, optional
        The observed fluence of the FRB. If :attr:`fluence` is *None*
        and both :attr:`width` and :attr:`peak_flux` are not *None*
        then :attr:`fluence` is automatically calculated with
        :meth:`calc_fluence`. Units: Jy ms Default: *None*

    obs_bandwidth : float or None, optional
        The observing bandwidth. Units: MHz Default: *None*

    obs_freq_central : float or None, optional
        The central observing frequency, Units: MHz Deault: *None*

    utc : str or None, optional
        The UTC time of the FRB Burst. Format should be of the form
        '1999-01-01T00:00:00.000'. Default: *None*


    Example
    -------
    >>> import fruitbat
    >>> FRB = fruitbat.Frb(879, gl="12:31:40.5", gb="3:41:10.0")
    >>> FRB.calc_dm_galaxy()
    >>> FRB.calc_redshift()

    """
    def __init__(self,
                 dm,
                 name=None,
                 raj=None,
                 decj=None,
                 gl=None,
                 gb=None,
                 dm_galaxy=0.0,
                 dm_excess=None,
                 z_host=None,
                 dm_host_est=0.0,
                 dm_host_loc=0.0,
                 dm_index=None,
                 scatt_index=None,
                 snr=None,
                 width=None,
                 peak_flux=None,
                 fluence=None,
                 obs_bandwidth=None,
                 obs_freq_central=None,
                 utc=None,
                 **kwargs):

        self.dm = dm

        self.name = name
        self.raj = raj
        self.decj = decj
        self.gl = gl
        self.gb = gb

        self.dm_galaxy = dm_galaxy

        # Calculate dm_excess from existing parameters if it is not given.
        if dm_excess is None:
            self.calc_dm_excess()
        else:
            self.dm_excess = dm_excess

        self.z_host = z_host
        self.dm_host_est = dm_host_est
        self.dm_host_loc = dm_host_loc
        self.dm_index = dm_index
        self.scatt_index = scatt_index
        self.snr = snr
        self.width = width
        self.peak_flux = peak_flux
        self.fluence = fluence
        self.obs_bandwidth = obs_bandwidth
        self.obs_freq_central = obs_freq_central
        self.utc = utc

        self.z = None
        self.dm_igm = None
        self.cosmology = None
        self.method = None
        self.dm_galaxy_model = None
        self.z_conf_int_lower = None
        self.z_conf_int_upper = None
        self.tau_sc = None

        # Calculate fluence if peak_flux and width are given
        if ((self.fluence is None)
                and (peak_flux is not None and width is not None)):
            self.calc_fluence()

        # Calculate the skycoords of the FRB from (raj, decj) or (gl, gb)
        if (raj and decj) or (gl and gb):
            if 'frame' in kwargs:
                self.skycoords = self.calc_skycoords(frame=kwargs["frame"])
            else:
                self.skycoords = self.calc_skycoords()

            self.raj = self.skycoords.transform_to('icrs').ra
            self.decj = self.skycoords.transform_to('icrs').dec
            self.gl = self.skycoords.transform_to('galactic').l
            self.gb = self.skycoords.transform_to('galactic').b
        else:
            self.skycoords = None

    def __repr__(self):

        frb_repr = []

        var_dict = vars(self)

        for var in var_dict:
            if var[1:] == "skycoords":
                continue
            var_name = var[1:]

            if isinstance(var_dict[var], u.Quantity):
                var_val = var_dict[var].value
            else:
                var_val = var_dict[var]
            frb_repr.append("{}={}".format(var_name, var_val))

        return "Frb({})".format(", ".join(map(str, frb_repr)))

    @docstring_substitute(meth=methods.available_methods(),
                          cosmo=cosmologies.available_cosmologies())
    def calc_redshift(self,
                      method='Batten2021',
                      cosmology="Planck18",
                      subtract_host=False,
                      lookup_table=None):
        """
        Calculate the redshift of the FRB from its :attr:`dm`,
        :attr:`dm_excess` or :attr:`dm_excess` - :attr:`dm_host_est`.

        Parameters
        ----------
        method : str, optional
            The dispersion meausre -redshift relation to use when
            calculating the redshift. Avaliable methods:  %(meth)s.
            Default: 'Inoue2004'

        cosmology : str, optional
            The cosmology to assume when calculating the redshift.
            Avaliable cosmologies: %(cosmo)s. Default: 'Planck18'

        subtract_host : bool, optional
            Subtract :attr:`dm_host_est` from the :attr:`dm_excess`
            before calculating the redshift. This is is used to account
            for the dispersion measure that arises from the FRB host
            galaxy. Default: False

        lookup_table : str or None, optional
            The path to the lookup table file. If ``lookup_table=None``
            a table will attempted to be loaded from the data directory
            based on the method name. Default: *None*

        Returns
        -------
        float
            The redshift of the FRB.

        Notes
        -----
        The methods_ section in the documentation has a description for
        each of the builtin methods.

        The cosmology_ section in the documentation has a list of the
        cosmological parameters for each cosmology

        .. _cosmology: https://fruitbat.readthedocs.io/en/latest/user_guide/method_and_cosmology.html#cosmology
        .. _methods: https://fruitbat.readthedocs.io/en/latest/user_guide/method_and_cosmology.html#methods

        """
        utils.check_type("subtract_host", subtract_host, bool)

        if subtract_host:
            input_dm = self.dm_excess - self.dm_host_est
            print(input_dm)
        else:
            input_dm = self.dm_excess

        # If the user provides a table use that table for estimation.
        if lookup_table is not None:
            raise NotImplementedError("Not made yet!")

        if method not in methods.available_methods():
            err_msg = ("Method '{}' is not a valid method. "
                       "Valid methods are: {}".format(
                           method, methods.available_methods()))
            raise ValueError(err_msg)

        if method in methods.methods_hydrodynamic():
            zvals, pdf, dz = self.calc_redshift_pdf(method=method)
            z_median = utils.calc_median_from_pdf(zvals, pdf)
            self.z = z_median
            self.method = method
            if method == "Batten2020":
                self.cosmology = "EAGLE"

        elif method in methods.methods_analytic():
            if cosmology in cosmologies.available_cosmologies():
                #if method in methods.builtin_method_functions().keys():
                table_name = table.get_table_path(method)
                self.z = table.get_z_from_table(input_dm, table_name,
                                                cosmology)
                self.cosmology = cosmology
                self.method = method

            else:
                err_msg = ("Cosmology '{}' is not a valid cosmology. Valid "
                           "cosmologies are: {}".format(
                               cosmology, cosmologies.available_cosmologies()))
                raise ValueError(err_msg)

        else:
            table_name = utils.get_path_to_file_from_here(
                "{}.hdf5".format(method), subdirs=["data"])
            self.z = table.get_z_from_table(input_dm, table_name)
            self.cosmology = cosmology
            self.method = method

        return self.z

    #@docstring_substitute(meth=methods.available_methods(),
    #                      cosmo=cosmologies.available_cosmologies())
    def calc_redshift_pdf(self,
                          method="Batten2021",
                          cosmology="Planck18",
                          prior="uniform",
                          subtract_host=False,
                          lookup_table=None):
        """
        Calc
        """
        if method == "Batten2021":
            filename = utils.get_path_to_file_from_here("Batten2021.hdf5",
                                                        subdirs=["data"])

        elif method in methods.methods_analytic():
            filename = "{}.hdf5".format(method)
            filename = utils.get_path_to_file_from_here(filename,
                                                        subdirs=["data"])
            raise NotImplementedError(
                "Getting PDFs from analytic moddels has not been implemented yet!"
            )

        else:
            raise NotImplementedError("This has not been implemented yet!")

        with h5py.File(filename, "r") as data:

            DMzHist = data["DMz_hist"][:]
            redshift_bin_widths = data["Redshift_Bin_Widths"][:]
            redshifts = data["Redshifts_Bin_Edges"][1:]
            DMBins = data["DM_Bin_Edges"][:]

            max_bin_idx = np.where(self.dm_excess.value <= DMBins)[0][0]
            prev_bin_idx = max_bin_idx - 1

            hist_higher_dm = DMzHist[max_bin_idx]
            hist_lower_dm = DMzHist[prev_bin_idx]

            pdf2 = utils.normalise_to_pdf(hist_higher_dm,
                                          bin_widths=redshift_bin_widths)
            pdf1 = utils.normalise_to_pdf(hist_lower_dm,
                                          bin_widths=redshift_bin_widths)

            DMlower, DMhigher = DMBins[prev_bin_idx], DMBins[max_bin_idx]
            lin_interp_pdf = utils.linear_interpolate_pdfs(
                self.dm_excess.value, (DMlower, DMhigher), (pdf1, pdf2))

            prior = utils.redshift_prior(redshifts, prior=prior)

            z_pdf = lin_interp_pdf * prior
            z_bins = redshifts
            dz = redshift_bin_widths

        self.z_bins = z_bins
        self.z_pdf = z_pdf
        self.dz = dz

        return z_bins, z_pdf, dz

    @docstring_copy(plot.redshift_pdf)
    def plot_redshift_pdf(self, *args, **kwargs):
        return plot.redshift_pdf(self, *args, **kwargs)

    @docstring_substitute(meth=methods.available_methods(),
                          cosmo=cosmologies.available_cosmologies())
    def calc_redshift_conf_int(self,
                               method="Batten2021",
                               sigma=1,
                               scatter_percentage=0,
                               **calc_redshift_kwargs):
        #method='Batten2020', cosmology="Planck18", sigma=1,
        #                           scatter_percentage=0, subtract_host=False, lookup_table=None):
        """
        Calculates the mean redshift and the confidence interval of
        an FRB from its :attr:`dm`, :attr:`dm_excess` or
        :attr:`dm_excess` - :attr:`dm_host_est`.

        Parameters
        ----------
        method : str, optional
            The dispersion meausre-redshift relation to use when
            calculating the redshift. Avaliable methods:  %(meth)s.
            Default: 'Batten2020'

        cosmology : str, optional
            The cosmology to assume when calculating the redshift.
            This value is overided if using a hydrodynamic method.
            Avaliable cosmologies: %(cosmo)s. Default: 'Planck18'

        sigma : int (1, 2, 3, 4, 5), optional
            The width of the confidence interval in units of standard
            deviation. `sigma=1` is the 68\% confidence interval.

        scatter_percentage : float, optional
            The amount of line of
            Default: 0

        subtract_host : bool, optional
            Subtract :attr:`dm_host_est` from the :attr:`dm_excess`
            before calculating the redshift. This is is used to account
            for the dispersion measure that arises from the FRB host
            galaxy. Default: False

        lookup_table : str or None, optional
            The path to the lookup table file. If ``lookup_table=None``
            a table will attempted to be loaded from the data directory
            based on the method name. Default: *None*

        Returns
        -------
        float
            The extimated redshift of the FRB.

        float
            The redshift confidence interval the FRB.

        Notes
        -----
        The methods_ section in the documentation has a description for
        each of the builtin methods.

        The cosmology_ section in the documentation has a list of the
        cosmological parameters for each cosmology

        .. _cosmology: https://fruitbat.readthedocs.io/en/latest/user_guide/method_and_cosmology.html#cosmology
        .. _methods: https://fruitbat.readthedocs.io/en/latest/user_guide/method_and_cosmology.html#methods

        """
        redshift = self.calc_redshift(**calc_redshift_kwargs)

        if method in methods.methods_hydrodynamic():
            if method == "Batten2021":
                zvals, pdf, dz = self.calc_redshift_pdf(method="Batten2021")

                conf_lower_lim, conf_upper_lim = utils.sigma_to_pdf_percentiles(
                    sigma=sigma)

                conf_int_lower = utils.calc_z_from_pdf_percentile(
                    zvals, pdf, percentile=conf_lower_lim)
                conf_int_upper = utils.calc_z_from_pdf_percentile(
                    zvals, pdf, percentile=conf_upper_lim)

        self.z_conf_int_lower = conf_int_lower
        self.z_conf_int_upper = conf_int_upper

        return self.z, (self.z_conf_int_lower, self.z_conf_int_upper)

    def calc_skycoords(self, frame=None):
        """
        Calculates the skycoord position on the sky of the FRB from
        (:attr:`raj`, :attr:`decj`) or (:attr:`gl`, :attr:`gb`). If
        both (:attr:`raj`, :attr:`decj`) and (:attr:`gl`, :attr:`gb`)
        are given, preference is given to (:attr:`raj`, :attr:`decj`).

        Parameters
        ----------
        frame: str or None, optional
            The type of coordinate frame. If ``frame = None`` then
            :meth:`calc_skycoords` will use a default frame based on
            the coordinates given. If :attr:`raj` and :attr:`decj` are
            given the default frame is 'icrs'. If :attr:`gl` and
            :attr:`gb` are given the default frame is 'galactic'.
            Default: *None*

        Returns
        -------
        :obj:`astropy.coordinates.SkyCoord`
            The sky coordinates of the FRB.
        """

        if self.raj is not None and self.decj is not None:
            if frame is None:
                frame = "icrs"
            skycoords = SkyCoord(ra=self.raj,
                                 dec=self.decj,
                                 frame=frame,
                                 unit=(u.hourangle, u.deg))

        elif self.gl is not None and self.gb is not None:
            if frame is None:
                frame = "galactic"
            skycoords = SkyCoord(self.gl, self.gb, frame=frame, unit=u.deg)
        else:
            raise ValueError("To calculate skycoords either (raj and decj)"
                             "or (gl, gb) must be provided")

        return skycoords

    def calc_dm_excess(self):
        """
        Calculates the dispersion measure excess of the FRB by
        subtracting the DM contribution from the Milky Way.

        Returns
        -------
        :obj:`astropy.units.Quantity`
            The dispersion measure excess.

        Notes
        -----
        :math:`\\rm{DM_{excess}}` is calculated as follows:

        .. math::

            \\rm{DM_{excess} = DM - DM_{galaxy}}
        """
        dm_excess = self.dm.value - self.dm_galaxy.value
        if dm_excess < 0:
            print("dm_excess < 0: This implies that the DM estimate "
                  "from the Milky Way is higher than the observed DM. "
                  "Setting dm_excess = 0")
            self.dm_excess = 0
        else:
            self.dm_excess = dm_excess
        return dm_excess

    def calc_dm_galaxy(self,
                       model='ymw16',
                       include_halo=False,
                       return_tau_sc=False):
        """
        Calculates the dispersion measure contribution of the Milky Way
        from either (:attr:`raj`, :attr:`decj`) or (:attr:`gl`,
        :attr:`gb`). Uses the YMW16 model of the Milky Way free
        electron column density.

        Parameters
        ----------
        model : 'ymw16' or 'ne2001', optional
            The Milky Way dispersion measure model. To use 'ne2001' you
            will need to install the python port. See
            https://fruitbat.readthedocs.io/en/latest/user_guide/ne2001_installation.html
            Default: 'ymw16'

        include_halo : bool, optional
            Include the DM of the galactic halo which isn't included
            in NE2001 or YMW16. This used the YT2020 halo model. Default: False

        return_tau_sc : bool, optional
            Return the scattering timescale in addition to the DM.
            Default: False

        Returns
        -------
        dm_galaxy: :obj:`astropy.units.Quantity`
            The dispersion measure contribution from the Milky Way of
            the FRB.

        tau_sc: :obj:`astropy.units.Quantity`, optional
            The scattering timescale at 1 GHz (s). Only returns if
            :attr:`return_tau_sc` is `True`.

        """
        YMW16_options = ["ymw16", "YMW16"]
        NE2001_options = ["ne2001", "NE2001"]

        if model in YMW16_options:
            model = 'YMW16'
        elif model in NE2001_options:
            model = 'NE2001'
        else:
            raise ValueError(
                "'{}' is not a valid galactic DM model".format(model))

        # Check to make sure some of the keyword are not None
        coord_list = [self.skycoords, self.raj, self.decj, self.gl, self.gb]
        if all(val is None for val in coord_list):
            raise ValueError("""Can not calculate dm_galaxy since coordinates
                             for FRB burst were not provided. Please provide
                             (raj, decj) or (gl, gb) coordinates.""")

        # Calculate skycoords position if it
        elif (self.skycoords is None and
              (self.raj is not None and self.decj is not None)
              or (self.gl is not None and self.gb is not None)):

            self._skycoords = self.calc_skycoords()

        dm_galaxy, tau_sc = pygedm.dist_to_dm(gl=self._skycoords.galactic.l,
                                              gb=self._skycoords.galactic.b,
                                              dist=25000.0,
                                              method=model)

        if include_halo:
            self.galaxy_halo = pygedm.calculate_halo_dm(
                gl=self._skycoords.galactic.l,
                gb=self._skycoords.galactic.b,
                method='yt2020')

        else:
            self.galaxy_halo = 0 * u.pc * u.cm**(-3)

        self.dm_galaxy_model = model
        self.dm_galaxy = dm_galaxy.value + self.galaxy_halo.value
        self.tau_sc = tau_sc.value
        self.calc_dm_excess()

        if return_tau_sc:
            return self.dm_galaxy, self.tau_sc
        else:
            return self.dm_galaxy

    def calc_dm_igm(self):
        """
        Calculates the dispersion measure of the intergalactic medium
        along the line-of-sight of the FRB. This can only be done if
        the redshift and dispersion measure contribution of the FRB
        host galaxy is known.

        Returns
        -------
        :obj:`astropy.units.Quantity`
            The dispersion measure contribution of the IGM.

        Notes
        -----
        :math:`\\rm{DM_{IGM}}` is calculated as follows:

        .. math::

           \\rm{DM_{IGM}} = \\rm{DM_{excess}} -
           \\frac{\\rm{DM_{host, loc}}}{1 + z}
        """

        if self.z_host is None:
            err_msg = ("z_host is None. Provide a non zero value for the "
                       "FRB host redshift")
            raise ValueError(err_msg)

        if self.dm_host_loc == 0.0:
            err_msg = ("dm_host_loc = 0. The dm_igm will be the same as "
                       "dm_excess. Provide a non-zero value for dm_host_loc")
            raise ValueError(err_msg)

        dm_igm = self.dm_excess - (self.dm_host_loc / (1 + self.z_host))
        self.dm_igm = dm_igm.value
        return self.dm_igm

    def calc_fluence(self):
        """
        Calculates the observed fluence of the FRB. This requires
        :attr:`width` and :attr:`peak_flux` to not be *None*.

        Returns
        -------
        :obj:`astropy.units.Quantity` or None:
            The fluence of the FRB.

        Notes
        -----
        The fluence (:math:`\\rm{F_{obs}}`) is calculated as follows:

        .. math::

            F_{obs} = W_{obs} S_{\\nu, p}

        Where :math:`W_{obs}` is the with of the FRB pulse and
        :math:`S_{\\nu, p}` is the specific peak flux.
        """
        if (self.width is None) or (self.peak_flux is None):
            err_msg = ("calc_fluence requires both width and peak_flux "
                       "to not be None")
            raise ValueError(err_msg)

        fluence = self.width * self.peak_flux
        self.fluence = fluence.value
        return fluence

    def calc_luminosity_distance(self):
        """
        Calculates the luminosity distance to the FRB based on its
        redshift. To calculate a luminosity distance either call
        :meth:`calc_redshift` first to determine a redshift estimate
        or provide a value for :attr:`dm_host`.

        Returns
        -------
        :obj:`astropy.units.Quantity`
            The luminosity distance to the FRB.
        """

        if self.z is None:
            raise ValueError(
                """ Can not calculate luminosity distance without a redshift.
                Use calc_redshift() to calculate the FRB redshift or provide
                a value for z_host.
                """)

        cosmo = cosmologies.cosmology_functions()[self.cosmology]
        return cosmo.luminosity_distance(self.z)

    def calc_comoving_distance(self):
        """
        Calculates the comoving distance to the FRB based on its
        redshift. To calculate a comoving distance either call
        :meth:`calc_redshift` first to determine a redshift estimate
        or provide a value for :attr:`dm_host`.

        Returns
        -------
        :obj:`astropy.units.Quantity`
            The comoving distance to the FRB.
        """
        if self.z is not None:
            z_sample = self.z
        elif self.z_host is not None:
            z_sample = self.z_host
        else:
            raise ValueError(
                """ Can not calculate comoving distance without a redshift.
                Use calc_redshift() to calculate the FRB redshift or provide
                a value for z_host.
                """)

        cosmo = cosmologies.cosmology_functions()[self.cosmology]
        return cosmo.comoving_distance(z_sample)

    def calc_luminosity(self, use_bandwidth=False):
        """
        Calculates the isotropic peak luminosity of the FRB. This is
        the upper limit to the the true peak luminosity since the
        luminosity distance required is also an upper limit to the true
        luminosity distance.

        Parameters
        ----------
        use_bandwidth: bool, optional
            The default method of calculating the luminosity of a FRB
            uses :attr:`obs_freq_central` as described in Zhang 2018.
            However some estimates of the FRB luminosity instead use
            :attr:`obs_bandwidth` (see Law et al. 2017). Set to
            ``True`` to use :attr:`obs_bandwidth` instead of
            :attr:`obs_freq_central`. Default: False

        Returns
        -------
        :obj:`astropy.units.Quantity`
            The estimated isotropic peak luminosity of the FRB in units
            of ergs/s.

        Notes
        -----
        The luminosity of a FRB is calculated following that in
        Zhang 2018.

        .. math::

            L_{FRB} \\simeq 4 \\pi D_{\\rm{L}}^2 S_{\\nu, p} \\nu_c

        Where :math:`S_{\\nu, p}` is the specific peak flux of the FRB,
        :math:`\\nu_c` is the central observing frequency,
        :math:`D_{\\rm{L}}` is the luminosity distance.

        In Law et al. 2017, the bandwidth (:math:`B`) is used instead
        of :math:`\\nu_c` to estimate luminosity.

        """
        D = self.calc_luminosity_distance()

        if self.peak_flux is None:
            err_msg = ("Can not calculate energy without peak_flux. Provide "
                       "peak_flux before calculating luminosity.")
            raise ValueError(err_msg)
        else:
            S = self.peak_flux

        if use_bandwidth:
            if self.obs_bandwidth is None:
                err_msg = ("Can not calculate energy without observing "
                           "bandwidth. Provide obs_bandwidth before "
                           "calculating energy.")
                raise ValueError(err_msg)
            B = self.obs_bandwidth
            lum = 4 * np.pi * D**2 * S * B
        else:
            if self.obs_freq_central is None:
                err_msg = ("Can not calculate energy without observing "
                           "frequency. Provide obs_freq_central before "
                           "calculating energy.")
                raise ValueError(err_msg)
            nu = self.obs_freq_central
            lum = 4 * np.pi * D**2 * S * nu

        return lum.to('erg s**-1')

    def calc_energy(self, use_bandwidth=False):
        """
        Calculates the isotropic energy of the FRB. This is the upper
        limit to the the true energy since the luminosity distance
        required is also an upper limit to the true luminosity
        distance.

        Parameters
        ----------
        use_bandwidth: bool, optional
            The default method of calculating the energy of a FRB uses
            :attr:`obs_freq_central` as described in Zhang 2018.
            However some estimates of the FRB energy instead use
            :attr:`obs_bandwidth` (see Law et al. 2017). Set to
            ``True`` to use :attr:`obs_bandwidth` instead of
            :attr:`obs_freq_central`. Default: False

        Returns
        -------
        :obj:`astropy.units.Quantity`
            The estimated isotropic energy of the FRB, in units of
            ergs.

        Notes
        -----
        The energy of a FRB is calculated following that in Zhang 2018.

        .. math::

            E_{FRB} \\simeq \\frac{4 \\pi}{1 + z} D_{\\rm{L}}^2 F_{obs} \\nu_c

        Where :math:`F_{obs}` is the fluence of the FRB, :math:`\\nu_c`
        is the central observing frequency, :math:`D_{\\rm{L}}` is the
        luminosity distance and :math:`z` is the estimated redshift.

        In Law et al. 2017, the bandwidth (:math:`B`) is used instead
        of :math:`\\nu_c` to estimate energy.
        """

        D = self.calc_luminosity_distance()

        if self.fluence is None:
            err_msg = ("Can not calculate energy without fluence. Provide "
                       "fluence or peak_flux and width before calculating "
                       "energy.")
            raise ValueError(err_msg)
        else:
            fluence = self.fluence

        if use_bandwidth:
            if self.obs_bandwidth is None:
                err_msg = ("Can not calculate energy without observing "
                           "bandwidth. Provide obs_bandwidth before "
                           "calculating energy.")
                raise ValueError(err_msg)
            else:
                bandwidth = self.obs_bandwidth
            energy = fluence * bandwidth * 4 * np.pi * D**2 * (1 + self.z)**-1

        else:
            if self.obs_freq_central is None:
                err_msg = ("Can not calculate energy without observing "
                           "frequency. Provide obs_freq_central before "
                           "calculating energy.")
                raise ValueError(err_msg)
            else:
                nu = self.obs_freq_central
            energy = fluence * nu * 4 * np.pi * D**2 * (1 + self.z)**-1

        return energy.to("erg")

    def _set_value_units(self, value, unit=None, non_negative=False):
        """
        Converts ``value`` into a :obj:`~astropy.unit.Quantity` with
        units. Also checks if ``value`` has existing units and the
        units are convertable with ``unit``. If ``value`` is a
        :obj:`~astropy.unit.Quantity` with convertable units then
        ``value`` is returned unchanged.

        Parameters
        ----------
        value: float, int, or :obj:`~astropy.unit.Quantity`
            The input value.

        unit: :obj:`astropy.unit` or None
            The unit to set for ``value``. If ``unit = None`` then
            ``value`` will become a dimensionless quantity.

        non_negative: bool, optional
            Raise an error if the value is negative. This is used to
            verify values that can only be positive are correctly
            specified. Default: *False*

        Return
        ------
        var : :obj:`~astropy.unit.Quantity` or None
            The output quantity with units. If ``value=None``
            returns *None*
        """
        # Check if the value already has units
        if not isinstance(value, u.Quantity):
            if value is None:
                var = None

            elif non_negative and value < 0.0:
                raise ValueError("Value must be greater than zero.")

            elif unit is None:
                var = value * u.dimensionless_unscaled

            else:
                var = value * unit
        else:
            # If passed units check that they are the correct units by checking
            # that they are equivalent

            if value.unit.is_equivalent(unit):
                var = value

            else:
                err_msg = ("Quantity expected to have units of {} instead was "
                           "passed units {} which is not convertable".format(
                               unit, value.unit))
                raise ValueError(err_msg)

        return var

    @property
    def name(self):
        """
        name: str
            The name of the FRB object.
        """
        return self._name

    @name.setter
    def name(self, value):
        if isinstance(value, str):
            self._name = value
        elif value is None:
            self._name = None
        else:
            self._name = str(value)

    @property
    def dm(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The observed dispersion measure of the FRB. This is without
            Milky Way or host galaxy subtraction.
        """
        return self._dm

    @dm.setter
    def dm(self, value):

        utils.check_type("dm", value, str, desire=False)

        self._dm = self._set_value_units(value,
                                         unit=u.pc * u.cm**-3,
                                         non_negative=True)

    @property
    def dm_galaxy(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The galactic component of the dispersion measure. This is
            quantity is calculated using :meth:`calc_dm_galaxy`.
        """
        return self._dm_galaxy

    @dm_galaxy.setter
    def dm_galaxy(self, value):
        self._dm_galaxy = self._set_value_units(value,
                                                unit=u.pc * u.cm**-3,
                                                non_negative=True)

    @property
    def dm_excess(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The dispersion measure with the Milky Way component
            subtracted. This value approximates the dispersion measure
            of the intergalatic medium by assuming that the host galaxy
            contributes zero to the observed dispersion measure. This
            quantity is calculated using :meth:`calc_dm_excess`.
        """
        return self._dm_excess

    @dm_excess.setter
    def dm_excess(self, value):
        self._dm_excess = self._set_value_units(value,
                                                u.pc * u.cm**-3,
                                                non_negative=True)

    @property
    def dm_host_est(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The estimated dispersion measure from the FRB host galaxy.
        """
        return self._dm_host_est

    @dm_host_est.setter
    def dm_host_est(self, value):
        self._dm_host_est = self._set_value_units(value,
                                                  u.pc * u.cm**-3,
                                                  non_negative=True)

    @property
    def dm_host_loc(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The dispersion measure from a localised FRB host galaxy.
        """
        return u.Quantity(self._dm_host_loc, u.pc * u.cm**-3)

    @dm_host_loc.setter
    def dm_host_loc(self, value):
        self._dm_host_loc = self._set_value_units(value,
                                                  u.pc * u.cm**-3,
                                                  non_negative=True)

    @property
    def dm_index(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The dispersion measure index of the burst.
        """
        return self._dm_index

    @dm_index.setter
    def dm_index(self, value):
        self._dm_index = self._set_value_units(value)

    @property
    def z(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The estimated redshift of the burst. By default this
            assumes that the entire :attr:`dm_excess` arrives from the
            IGM and the host galaxy of the FRB and any surrounding
            material contribute nothing to the total DM. This should be
            taken as an upper limit to the bursts true redshift. To
            provide an estimate of the DM contribution due to he host
            galaxy, set :attr:`dm_host_est` to a non-zero value and use
            ``subract_host=True`` when calling :meth:`calc_redshift()`.
        """
        return self._z

    @z.setter
    def z(self, value):
        self._z = self._set_value_units(value, non_negative=True)

    @property
    def z_conf_int_lower(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The lower bound of the redshift confidence interval.
        """
        return self._z_conf_int_lower

    @z_conf_int_lower.setter
    def z_conf_int_lower(self, value):
        self._z_conf_int_lower = self._set_value_units(value,
                                                       non_negative=True)

    @property
    def z_conf_int_upper(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The upper bound of the redshift confidence interval.
        """
        return self._z_conf_int_upper

    @z_conf_int_upper.setter
    def z_conf_int_upper(self, value):
        self._z_conf_int_upper = self._set_value_units(value,
                                                       non_negative=True)

    @property
    def z_host(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The redshift of the localised FRB host galaxy. Note that
            this an observed quantity, not the estimated redshift
            :attr:`z` calculated with :meth:`calc_redshift()`.
        """
        return self._z_host

    @z_host.setter
    def z_host(self, value):
        self._z_host = self._set_value_units(value)

    @property
    def scatt_index(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The scattering index of the burst.
        """
        return self._scatt_index

    @scatt_index.setter
    def scatt_index(self, value):
        self._scatt_index = self._set_value_units(value)

    @property
    def width(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The observed width of the pulse.
        """
        return self._width

    @width.setter
    def width(self, value):
        self._width = self._set_value_units(value, u.ms, non_negative=True)

    @property
    def peak_flux(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The observed peak flux density of the FRB.
        """
        return self._peak_flux

    @peak_flux.setter
    def peak_flux(self, value):
        self._peak_flux = self._set_value_units(value, u.Jy, non_negative=True)

    @property
    def fluence(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The observed fluence of the FRB. This is calculated from
            :meth:`calc_fluence`.
        """
        return self._fluence

    @fluence.setter
    def fluence(self, value):
        self._fluence = self._set_value_units(value,
                                              u.Jy * u.ms,
                                              non_negative=True)

    @property
    def obs_bandwidth(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The observing bandwidth of the FRB.
        """
        return self._obs_bandwidth

    @obs_bandwidth.setter
    def obs_bandwidth(self, value):
        self._obs_bandwidth = self._set_value_units(value,
                                                    u.MHz,
                                                    non_negative=True)

    @property
    def obs_freq_central(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The central observing frequency of the FRB.
        """
        return self._obs_freq_central

    @obs_freq_central.setter
    def obs_freq_central(self, value):
        self._obs_freq_central = self._set_value_units(value,
                                                       u.MHz,
                                                       non_negative=True)

    @property
    def raj(self):
        """
        :obj:`astropy.coordinates.Longitude` or None:
            The right accension in J2000 coordinates of the best
            estimate of the FRB position. This is given in the IRCS
            frame.
        """
        return self._raj

    @raj.setter
    def raj(self, value):
        if value is None:
            self._raj = None
        else:
            self._raj = coord.angles.Longitude(value, u.hourangle)

    @property
    def decj(self):
        """
        :obj:`astropy.coordinates.Latitude` or None:
            The declination in J2000 coordinates of the best estimate
            of the FRB position. This is given in the IRCS frame.
        """
        return self._decj

    @decj.setter
    def decj(self, value):
        if value is None:
            self._decj = None
        else:
            self._decj = coord.angles.Latitude(value, u.deg)

    @property
    def gl(self):
        """
        :obj:`astropy.coordinates.Longitude` or None:
            The longitude in galactic coordinates of the best estimate
            of the FRB position.
        """
        return self._gl

    @gl.setter
    def gl(self, value):
        if value is None:
            self._gl = None
        else:
            self._gl = coord.angles.Longitude(value, u.deg)

    @property
    def gb(self):
        """
        :obj:`astropy.coordinates.Latitude` or None:
            The latitude in galactic coordinates of the best estimate
            of the FRB position.
        """
        return self._gb

    @gb.setter
    def gb(self, value):
        if value is None:
            self._gb = None
        else:
            self._gb = coord.angles.Latitude(value, u.deg)

    @property
    def skycoords(self):
        """
        :obj:`astropy.coordinates.SkyCoord` or None:
            The skycoords of the FRB. This is calculated from either
            (:attr:`raj`, :attr:`decj`) or (:attr:`gl`, :attr:`gb`)
            using :meth:`calc_skycoords`.
        """
        return self._skycoords

    @skycoords.setter
    def skycoords(self, value):
        if value is None:
            self._skycoords = None
        else:
            self._skycoords = SkyCoord(value)

    @property
    def dm_igm(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The estimated disperison measure from the IGM. This can be
            calculated for a FRB for which the redshift of its host
            galaxy is known. This value is determined using
            :meth:`calc_dm_igm`.
        """
        return self._dm_igm

    @dm_igm.setter
    def dm_igm(self, value):
        self._dm_igm = self._set_value_units(value,
                                             u.pc * u.cm**-3,
                                             non_negative=True)

    @property
    def utc(self):
        """
        :obj:`astropy.time.Time` or None
        The UTC time of the burst.
        """
        return self._utc

    @utc.setter
    def utc(self, value):
        if value is None:
            self._utc = None
        else:
            self._utc = Time(value, format='isot', scale='utc')

    @property
    def snr(self):
        """
        :obj:`astropy.units.Quantity`:
        The signal-to-noise  ratio of the burst.
        """
        return self._snr

    @snr.setter
    def snr(self, value):
        self._snr = self._set_value_units(value, non_negative=True)

    @property
    def cosmology(self):
        """
        str or None:
            The cosmology used to calculate redshift.
        """
        return self._cosmology

    @cosmology.setter
    def cosmology(self, value):
        if isinstance(value, str):
            self._cosmology = value
        else:
            self._cosmology = str(value)

    @property
    def method(self):
        """
        str or None:
            The method used to calculate redshift.
        """
        return self._method

    @method.setter
    def method(self, value):
        if isinstance(value, str):
            self._method = value
        else:
            self._method = str(value)

    @property
    def dm_galaxy_model(self):
        """
        str or None:
            The method used to calculate redshift.
        """
        return self._dm_galaxy_model

    @dm_galaxy_model.setter
    def dm_galaxy_model(self, value):
        if isinstance(value, str):
            self._dm_galaxy_model = value
        else:
            self._dm_galaxy_model = str(value)
Ejemplo n.º 8
0
    def calc_redshift(self,
                      method='Inoue2004',
                      cosmology="Planck18",
                      subtract_host=False,
                      lookup_table=None):
        """
        Calculate the redshift of the FRB from its :attr:`dm`,
        :attr:`dm_excess` or :attr:`dm_excess` - :attr:`dm_host_est`.

        Parameters
        ----------
        method : str, optional
            The dispersion meausre -redshift relation to use when 
            calculating the redshift. Avaliable methods:  %(meth)s. 
            Default: 'Inoue2004'

        cosmology : str, optional
            The cosmology to assume when calculating the redshift. 
            Avaliable cosmologies: %(cosmo)s. Default: 'Planck18'

        subtract_host : bool, optional
            Subtract :attr:`dm_host_est` from the :attr:`dm_excess`
            before calculating the redshift. This is is used to account
            for the dispersion measure that arises from the FRB host
            galaxy. Default: False

        lookup_table : str or None, optional
            The path to the lookup table file. If ``lookup_table=None``
            a table will attempted to be loaded from the data directory 
            based on the method name. Default: *None*       

        Returns
        -------
        float
            The redshift of the FRB.

        Notes
        -----
        The methods_ section in the documentation has a description for
        each of the builtin methods.

        The cosmology_ section in the documentation has a list of the
        cosmological parameters for each cosmology

        .. _cosmology: https://fruitbat.readthedocs.io/en/latest/user_guide/method_and_cosmology.html#cosmology
        .. _methods: https://fruitbat.readthedocs.io/en/latest/user_guide/method_and_cosmology.html#methods
        """
        if not isinstance(subtract_host, bool):
            raise ValueError("subtract_host must be of type bool.")

        if subtract_host:
            input_dm = self.dm_excess - self.dm_host_est
        else:
            input_dm = self.dm_excess

        if method not in methods.available_methods():
            err_msg = ("Method '{}' is not a valid method. "
                       "Valid methods are: {}".format(
                           method, methods.available_methods()))
            raise ValueError(err_msg)

        if cosmology not in cosmologies.available_cosmologies():
            err_msg = ("Cosmology '{}' is not a valid cosmology. Valid "
                       "cosmologies are: {}".format(
                           cosmology, cosmologies.available_cosmologies()))
            raise ValueError(err_msg)

        # If the user provides a table use that table for estimation.
        if lookup_table is not None:
            lookup_table = table.load(lookup_table)

        else:
            if method in methods.builtin_method_functions().keys():
                table_name = "".join(["_".join([method, cosmology]), ".npz"])
            else:
                table_name = "".join(["custom_", method, ".npz"])

            lookup_table = table.load(table_name)

        self.z = table.get_z_from_table(input_dm, lookup_table)
        lookup_table.close()

        self.cosmology = cosmology
        self.method = method
        return self.z
Ejemplo n.º 9
0
class Frb(object):
    """
    Create a :class:`~Frb` object using the observered properties of a
    FRB including dispersion measure (DM) and its sky coordinates. This
    class utilises various DM-redshift relations as well as the YMW16
    galactic DM model in the analysis.

    A FRB can be defined simply with a single :attr:`dm` value. This
    should be the observed DM prior to subtracting the Milky Way,
    however if you do not provide any additional information, it is
    asssumed that this value is also the :attr:`dm_excess` (DM -
    Galactic DM). You can get the estimated redshift by using the
    method :meth:`calc_redshift`.

    To get a more accurate distance estimate you can account for the
    contribution due to the Milky Way by supplying :attr:`dm_galaxy` or
    by giving the coordinates of the FRB in ICRS (:attr:`raj`,
    :attr:`decj`) or Galactic (:attr:`gl`, :attr:`gb`) coordinates and
    calling the method :meth:`calc_dm_galaxy` before calling
    :meth:`calc_redshift`.


    Parameters
    ----------
    dm : float
        The observed dispersion measure of the FRB. This is without
        Milky Way or host galaxy subtraction. Units: pc cm**-3

    Keyword Arguments
    -----------------
    name : str or None, optional
        The name of the frb object. Default: *None*

    raj : str or None, optional
        The right ascension in J2000 coordinates of the best estimate
        of the FRB position. By default should be given in the ICRS
        frame. An alternative frame can be specified by also including
        the keyword ``frame``. e.g. ``frame='fk5'``. Default: *None*

    decj : str or None, optional
        The declination in J2000 coordinates of the best estimate of
        the FRB position. By default should be given in the ICRS frame.
        An alternative frame can be specified by also including the
        keyword ``frame``. e.g. ``frame='fk5'``. Default: *None*

    gl : str or None, optional
        The Galactic longitude in degrees of the best estimate of the
        FRB position. Default: *None*

    gb : str or None, optional
        The Galactic latitude in degrees of the best estimate of the
        FRB position. Default: *None*

    Other Parameters
    ----------------
    dm_galaxy : float, optional
        The modelled contribution to the FRB DM by electrons in the
        Milky Way. This value is calculated using
        :meth:`calc_dm_galaxy` Units: pc cm**-3 Default: 0.0

    dm_excess : float or None, optional
        The DM excess of the FRB over the estimated Galactic DM. If
        :attr:`dm_excess` is *None*, then :attr:`dm_excess` is
        calculated automatically with :meth:`calc_dm_excess`.
        Units: pc cm**-3 Default: *None*

    z_host : float or None, optional
        The observed redshift of the localised FRB host galaxy.
        Default: *None*

    dm_host_est : float, optional
        The estimated contribution to the measured FRB DM originating
        from the FRB's host galaxy. This value is the amount of DM the
        host galaxy contributes to the observed DM, *not* the DM of the
        host galaxy. Setting this to a non-zero value and setting
        ``subtract_host=True`` when calling :meth:`calc_redshift`
        accounts for the DM contribution due to the host galaxy.
        Units: pc cm**-3 Default: 0.0

    dm_host_loc : float, optional
        The dispersion measure of a localised FRB host galaxy. This
        value is *not* the contribution to the observed DM, but the DM
        at the host galaxy. The observed DM is :attr:`dm_host_loc` but
        attenuated by a factor of (1 + z). To use this, the redshift of
        the host galaxy must be known. Units: pc cm**-3 Default: 0.0

    dm_index : float or None, optional
        The dispersion measure index of the burst :math:`\\alpha` such
        that :math:`\\rm{DM} \\propto \\nu^{-\\alpha}` Default: *None*

    scatt_index : float or None, optional
        The scattering index (:math:`\\beta`) of the FRB pulse. The
        scattering index describes how the width (:math:`\\rm{W}`) of
        the FRB pulse evolves with frequency :math:`\\nu` such that
        :math:`\\rm{W} \\propto \\nu^{-\\beta}`. Default: *None*

    snr : float or None, optional
        The signal-to-noise ratio of the burst.
        Default: *None*

    width : float or None, optional
        The observed width of the pulse obtained by a pulse fitting
        algorithm. Units: ms Default: *None*

    peak_flux : float or None, optional
        The observed peak flux density of the burst. Units: Jy
        Default: *None*

    fluence : float or None, optional
        The observed fluence of the FRB. If :attr:`fluence` is *None*
        and both :attr:`width` and :attr:`peak_flux` are not *None*
        then :attr:`fluence` is automatically calculated with
        :meth:`calc_fluence`. Units: Jy ms Default: *None*

    obs_bandwidth : float or None, optional
        The observing bandwidth. Units: MHz Default: *None*

    obs_freq_central : float or None, optional
        The central observing frequency, Units: MHz Deault: *None*

    utc : str or None, optional
        The UTC time of the FRB Burst. Format should be of the form
        '1999-01-01T00:00:00.000'. Default: *None*


    Example
    -------
    >>> import fruitbat
    >>> FRB = fruitbat.Frb(879, gl="12:31:40.5", gb="3:41:10.0")
    >>> FRB.calc_dm_galaxy()
    >>> FRB.calc_redshift()

    """
    def __init__(self,
                 dm,
                 name=None,
                 raj=None,
                 decj=None,
                 gl=None,
                 gb=None,
                 dm_galaxy=0.0,
                 dm_excess=None,
                 z_host=None,
                 dm_host_est=0.0,
                 dm_host_loc=0.0,
                 dm_index=None,
                 scatt_index=None,
                 snr=None,
                 width=None,
                 peak_flux=None,
                 fluence=None,
                 obs_bandwidth=None,
                 obs_freq_central=None,
                 utc=None,
                 **kwargs):

        self.dm = dm

        self.name = name
        self.raj = raj
        self.decj = decj
        self.gl = gl
        self.gb = gb

        self.dm_galaxy = dm_galaxy

        # Calculate dm_excess from existing parameters if it is not given.
        if dm_excess is None:
            self.calc_dm_excess()
        else:
            self.dm_excess = dm_excess

        self.z_host = z_host
        self.dm_host_est = dm_host_est
        self.dm_host_loc = dm_host_loc
        self.dm_index = dm_index
        self.scatt_index = scatt_index
        self.snr = snr
        self.width = width
        self.peak_flux = peak_flux
        self.fluence = fluence
        self.obs_bandwidth = obs_bandwidth
        self.obs_freq_central = obs_freq_central
        self.utc = utc

        self.z = None
        self.dm_igm = None
        self.cosmology = None
        self.method = None

        # Calculate fluence if peak_flux and width are given
        if ((self.fluence is None)
                and (peak_flux is not None and width is not None)):
            self.calc_fluence()

        # Calculate the skycoords of the FRB from (raj, decj) or (gl, gb)
        if (raj and decj) or (gl and gb):
            if 'frame' in kwargs:
                self.skycoords = self.calc_skycoords(frame=kwargs["frame"])
            else:
                self.skycoords = self.calc_skycoords()

            self.raj = self.skycoords.transform_to('icrs').ra
            self.decj = self.skycoords.transform_to('icrs').dec
            self.gl = self.skycoords.transform_to('galactic').l
            self.gb = self.skycoords.transform_to('galactic').b
        else:
            self.skycoords = None

    def __repr__(self):

        frb_repr = []

        var_dict = vars(self)

        for var in var_dict:
            if var[1:] == "skycoords":
                continue
            var_name = var[1:]

            if isinstance(var_dict[var], u.Quantity):
                var_val = var_dict[var].value
            else:
                var_val = var_dict[var]
            frb_repr.append("{}={}".format(var_name, var_val))

        return "Frb({})".format(", ".join(map(str, frb_repr)))

    @docstring_substitute(meth=methods.available_methods(),
                          cosmo=cosmologies.available_cosmologies())
    def calc_redshift(self,
                      method='Inoue2004',
                      cosmology="Planck18",
                      subtract_host=False,
                      lookup_table=None):
        """
        Calculate the redshift of the FRB from its :attr:`dm`,
        :attr:`dm_excess` or :attr:`dm_excess` - :attr:`dm_host_est`.

        Parameters
        ----------
        method : str, optional
            The dispersion meausre -redshift relation to use when 
            calculating the redshift. Avaliable methods:  %(meth)s. 
            Default: 'Inoue2004'

        cosmology : str, optional
            The cosmology to assume when calculating the redshift. 
            Avaliable cosmologies: %(cosmo)s. Default: 'Planck18'

        subtract_host : bool, optional
            Subtract :attr:`dm_host_est` from the :attr:`dm_excess`
            before calculating the redshift. This is is used to account
            for the dispersion measure that arises from the FRB host
            galaxy. Default: False

        lookup_table : str or None, optional
            The path to the lookup table file. If ``lookup_table=None``
            a table will attempted to be loaded from the data directory 
            based on the method name. Default: *None*       

        Returns
        -------
        float
            The redshift of the FRB.

        Notes
        -----
        The methods_ section in the documentation has a description for
        each of the builtin methods.

        The cosmology_ section in the documentation has a list of the
        cosmological parameters for each cosmology

        .. _cosmology: https://fruitbat.readthedocs.io/en/latest/user_guide/method_and_cosmology.html#cosmology
        .. _methods: https://fruitbat.readthedocs.io/en/latest/user_guide/method_and_cosmology.html#methods
        """
        if not isinstance(subtract_host, bool):
            raise ValueError("subtract_host must be of type bool.")

        if subtract_host:
            input_dm = self.dm_excess - self.dm_host_est
        else:
            input_dm = self.dm_excess

        if method not in methods.available_methods():
            err_msg = ("Method '{}' is not a valid method. "
                       "Valid methods are: {}".format(
                           method, methods.available_methods()))
            raise ValueError(err_msg)

        if cosmology not in cosmologies.available_cosmologies():
            err_msg = ("Cosmology '{}' is not a valid cosmology. Valid "
                       "cosmologies are: {}".format(
                           cosmology, cosmologies.available_cosmologies()))
            raise ValueError(err_msg)

        # If the user provides a table use that table for estimation.
        if lookup_table is not None:
            lookup_table = table.load(lookup_table)

        else:
            if method in methods.builtin_method_functions().keys():
                table_name = "".join(["_".join([method, cosmology]), ".npz"])
            else:
                table_name = "".join(["custom_", method, ".npz"])

            lookup_table = table.load(table_name)

        self.z = table.get_z_from_table(input_dm, lookup_table)
        lookup_table.close()

        self.cosmology = cosmology
        self.method = method
        return self.z

    def calc_skycoords(self, frame=None):
        """
        Calculates the skycoord position on the sky of the FRB from
        (:attr:`raj`, :attr:`decj`) or (:attr:`gl`, :attr:`gb`). If
        both (:attr:`raj`, :attr:`decj`) and (:attr:`gl`, :attr:`gb`)
        are given, preference is given to (:attr:`raj`, :attr:`decj`).

        Parameters
        ----------
        frame: str or None, optional
            The type of coordinate frame. If ``frame = None`` then
            :meth:`calc_skycoords` will use a default frame based on
            the coordinates given. If :attr:`raj` and :attr:`decj` are
            given the default frame is 'icrs'. If :attr:`gl` and
            :attr:`gb` are given the default frame is 'galactic'.
            Default: *None*

        Returns
        -------
        :obj:`astropy.coordinates.SkyCoord`
            The sky coordinates of the FRB.
        """

        if self.raj is not None and self.decj is not None:
            if frame is None:
                frame = "icrs"
            skycoords = SkyCoord(ra=self.raj,
                                 dec=self.decj,
                                 frame=frame,
                                 unit=(u.hourangle, u.deg))

        elif self.gl is not None and self.gb is not None:
            if frame is None:
                frame = "galactic"
            skycoords = SkyCoord(self.gl, self.gb, frame=frame, unit=u.deg)
        else:
            raise ValueError("To calculate skycoords either (raj and decj)"
                             "or (gl, gb) must be provided")

        return skycoords

    def calc_dm_excess(self):
        """
        Calculates the dispersion measure excess of the FRB by
        subtracting the DM contribution from the Milky Way.

        Returns
        -------
        :obj:`astropy.units.Quantity`
            The dispersion measure excess.

        Notes
        -----
        :math:`\\rm{DM_{excess}}` is calculated as follows:

        .. math::

            \\rm{DM_{excess} = DM - DM_{galaxy}}
        """
        dm_excess = self.dm.value - self.dm_galaxy.value
        if dm_excess < 0:
            print("dm_excess < 0: This implies that the DM estimate "
                  "from the Milky Way is higher than the observed DM. "
                  "Setting dm_excess = 0")
            self.dm_excess = 0
        else:
            self.dm_excess = dm_excess
        return dm_excess

    def calc_dm_galaxy(self, model='ymw16'):
        """
        Calculates the dispersion measure contribution of the Milky Way
        from either (:attr:`raj`, :attr:`decj`) or (:attr:`gl`,
        :attr:`gb`). Uses the YMW16 model of the Milky Way free
        electron column density.

        Parameters
        ----------
        model : 'ymw16' or 'ne2001', optional
            The Milky Way dispersion measure model. To use 'ne2001' you
            will need to install the python port. See 
            https://fruitbat.readthedocs.io/en/latest/user_guide/ne2001_installation.html
            Default: 'ymw16'

        Returns
        -------
        :obj:`astropy.units.Quantity`
            The dispersion measure contribution from the Milky Way of
            the FRB.
        """
        YMW16_options = ["ymw16", "YMW16"]

        NE2001_options = ["ne2001", "NE2001"]

        if model in YMW16_options:
            # Since the YMW16 model only gives you a dispersion measure out to a
            # distance within the galaxy, to get the entire DM contribution of the
            # galaxy we need to specify the furthest distance in the YMW16 model.
            max_galaxy_dist = 25000  # units: pc

            # Check to make sure some of the keyword are not None
            coord_list = [
                self.skycoords, self.raj, self.decj, self.gl, self.gb
            ]
            if all(val is None for val in coord_list):
                raise ValueError(
                    """Can not calculate dm_galaxy since coordinates
                                 for FRB burst were not provided. Please provide
                                 (raj, decj) or (gl, gb) coordinates.""")

            # Calculate skycoords position if it
            elif (self.skycoords is None and
                  (self.raj is not None and self.decj is not None)
                  or (self.gl is not None and self.gb is not None)):

                self._skycoords = self.calc_skycoords()

            dm_galaxy, tau_sc = ymw16.dist_to_dm(self._skycoords.galactic.l,
                                                 self._skycoords.galactic.b,
                                                 max_galaxy_dist)

        elif model in NE2001_options:
            try:
                from ne2001 import density
            except ModuleNotFoundError:
                msg = ("""
                By default only the YMW16 Milky Way electron density model
                is installed with Fruitbat. However Fruitbat due support using  
                the NE2001 model via a python port from JXP and Ben Baror. 

                To install the ne2001 model compatible with Fruitbat download
                and install it from github:

                >>> git clone https://github.com/FRBs/ne2001
                >>> cd ne2001
                >>> pip install .

                Once you have installed it you should be able to use Fruitbat
                in exactly the same way by passing 'ne2001' instead of 'ymw16'.
                """)
                raise ModuleNotFoundError(msg)

            # This is the same max distanc e that we used for the YMW16 model
            # However the NE2001 model specifies distance in kpc not pc.
            max_galaxy_dist = 25  # units kpc

            # NE2001 models needs gl and gb in floats
            gl = float(self._skycoords.galactic.l.value)
            gb = float(self._skycoords.galactic.b.value)

            ne = density.ElectronDensity()
            dm_galaxy = ne.DM(gl, gb, max_galaxy_dist)

        self.dm_galaxy = dm_galaxy.value
        self.calc_dm_excess()
        return self.dm_galaxy

    def calc_dm_igm(self):
        """
        Calculates the dispersion measure of the intergalactic medium
        along the line-of-sight of the FRB. This can only be done if
        the redshift and dispersion measure contribution of the FRB
        host galaxy is known.

        Returns
        -------
        :obj:`astropy.units.Quantity`
            The dispersion measure contribution of the IGM.

        Notes
        -----
        :math:`\\rm{DM_{IGM}}` is calculated as follows:

        .. math::

           \\rm{DM_{IGM}} = \\rm{DM_{excess}} -
           \\frac{\\rm{DM_{host, loc}}}{1 + z}
        """

        if self.z_host is None:
            err_msg = ("z_host is None. Provide a non zero value for the "
                       "FRB host redshift")
            raise ValueError(err_msg)

        if self.dm_host_loc == 0.0:
            err_msg = ("dm_host_loc = 0. The dm_igm will be the same as "
                       "dm_excess. Provide a non-zero value for dm_host_loc")
            raise ValueError(err_msg)

        dm_igm = self.dm_excess - (self.dm_host_loc / (1 + self.z_host))
        self.dm_igm = dm_igm.value
        return self.dm_igm

    def calc_fluence(self):
        """
        Calculates the observed fluence of the FRB. This requires
        :attr:`width` and :attr:`peak_flux` to not be *None*.

        Returns
        -------
        :obj:`astropy.units.Quantity` or None:
            The fluence of the FRB.

        Notes
        -----
        The fluence (:math:`\\rm{F_{obs}}`) is calculated as follows:

        .. math::

            F_{obs} = W_{obs} S_{\\nu, p}

        Where :math:`W_{obs}` is the with of the FRB pulse and
        :math:`S_{\\nu, p}` is the specific peak flux.
        """
        if (self.width is None) or (self.peak_flux is None):
            err_msg = ("calc_fluence requires both width and peak_flux "
                       "to not be None")
            raise ValueError(err_msg)

        fluence = self.width * self.peak_flux
        self.fluence = fluence.value
        return fluence

    def calc_luminosity_distance(self):
        """
        Calculates the luminosity distance to the FRB based on its
        redshift. To calculate a luminosity distance either call
        :meth:`calc_redshift` first to determine a redshift estimate
        or provide a value for :attr:`dm_host`.

        Returns
        -------
        :obj:`astropy.units.Quantity`
            The luminosity distance to the FRB.
        """

        if self.z is None:
            raise ValueError(
                """ Can not calculate luminosity distance without a redshift.
                Use calc_redshift() to calculate the FRB redshift or provide
                a value for z_host.
                """)

        cosmo = cosmologies.cosmology_functions()[self.cosmology]
        return cosmo.luminosity_distance(self.z)

    def calc_comoving_distance(self):
        """
        Calculates the comoving distance to the FRB based on its
        redshift. To calculate a comoving distance either call
        :meth:`calc_redshift` first to determine a redshift estimate
        or provide a value for :attr:`dm_host`.

        Returns
        -------
        :obj:`astropy.units.Quantity`
            The comoving distance to the FRB.
        """
        if self.z is not None:
            z_sample = self.z
        elif self.z_host is not None:
            z_sample = self.z_host
        else:
            raise ValueError(
                """ Can not calculate comoving distance without a redshift.
                Use calc_redshift() to calculate the FRB redshift or provide
                a value for z_host.
                """)

        cosmo = cosmologies.cosmology_functions()[self.cosmology]
        return cosmo.comoving_distance(z_sample)

    def calc_luminosity(self, use_bandwidth=False):
        """
        Calculates the isotropic peak luminosity of the FRB. This is
        the upper limit to the the true peak luminosity since the
        luminosity distance required is also an upper limit to the true
        luminosity distance.

        Parameters
        ----------
        use_bandwidth: bool, optional
            The default method of calculating the luminosity of a FRB
            uses :attr:`obs_freq_central` as described in Zhang 2018.
            However some estimates of the FRB luminosity instead use
            :attr:`obs_bandwidth` (see Law et al. 2017). Set to
            ``True`` to use :attr:`obs_bandwidth` instead of
            :attr:`obs_freq_central`. Default: False

        Returns
        -------
        :obj:`astropy.units.Quantity`
            The estimated isotropic peak luminosity of the FRB in units
            of ergs/s.

        Notes
        -----
        The luminosity of a FRB is calculated following that in
        Zhang 2018.

        .. math::

            L_{FRB} \\simeq 4 \\pi D_{\\rm{L}}^2 S_{\\nu, p} \\nu_c

        Where :math:`S_{\\nu, p}` is the specific peak flux of the FRB,
        :math:`\\nu_c` is the central observing frequency,
        :math:`D_{\\rm{L}}` is the luminosity distance.

        In Law et al. 2017, the bandwidth (:math:`B`) is used instead
        of :math:`\\nu_c` to estimate luminosity.

        """
        D = self.calc_luminosity_distance()

        if self.peak_flux is None:
            err_msg = ("Can not calculate energy without peak_flux. Provide "
                       "peak_flux before calculating luminosity.")
            raise ValueError(err_msg)
        else:
            S = self.peak_flux

        if use_bandwidth:
            if self.obs_bandwidth is None:
                err_msg = ("Can not calculate energy without observing "
                           "bandwidth. Provide obs_bandwidth before "
                           "calculating energy.")
                raise ValueError(err_msg)
            B = self.obs_bandwidth
            lum = 4 * np.pi * D**2 * S * B
        else:
            if self.obs_freq_central is None:
                err_msg = ("Can not calculate energy without observing "
                           "frequency. Provide obs_freq_central before "
                           "calculating energy.")
                raise ValueError(err_msg)
            nu = self.obs_freq_central
            lum = 4 * np.pi * D**2 * S * nu

        return lum.to('erg s**-1')

    def calc_energy(self, use_bandwidth=False):
        """
        Calculates the isotropic energy of the FRB. This is the upper
        limit to the the true energy since the luminosity distance
        required is also an upper limit to the true luminosity
        distance.

        Parameters
        ----------
        use_bandwidth: bool, optional
            The default method of calculating the energy of a FRB uses
            :attr:`obs_freq_central` as described in Zhang 2018.
            However some estimates of the FRB energy instead use
            :attr:`obs_bandwidth` (see Law et al. 2017). Set to
            ``True`` to use :attr:`obs_bandwidth` instead of
            :attr:`obs_freq_central`. Default: False

        Returns
        -------
        :obj:`astropy.units.Quantity`
            The estimated isotropic energy of the FRB, in units of
            ergs.

        Notes
        -----
        The energy of a FRB is calculated following that in Zhang 2018.

        .. math::

            E_{FRB} \\simeq \\frac{4 \\pi}{1 + z} D_{\\rm{L}}^2 F_{obs} \\nu_c

        Where :math:`F_{obs}` is the fluence of the FRB, :math:`\\nu_c`
        is the central observing frequency, :math:`D_{\\rm{L}}` is the
        luminosity distance and :math:`z` is the estimated redshift.

        In Law et al. 2017, the bandwidth (:math:`B`) is used instead
        of :math:`\\nu_c` to estimate energy.
        """

        D = self.calc_luminosity_distance()

        if self.fluence is None:
            err_msg = ("Can not calculate energy without fluence. Provide "
                       "fluence or peak_flux and width before calculating "
                       "energy.")
            raise ValueError(err_msg)
        else:
            F = self.fluence

        if use_bandwidth:
            if self.obs_bandwidth is None:
                err_msg = ("Can not calculate energy without observing "
                           "bandwidth. Provide obs_bandwidth before "
                           "calculating energy.")
                raise ValueError(err_msg)
            else:
                B = self.obs_bandwidth
            energy = F * B * 4 * np.pi * D**2 * (1 + self.z)**-1

        else:
            if self.obs_freq_central is None:
                err_msg = ("Can not calculate energy without observing "
                           "frequency. Provide obs_freq_central before "
                           "calculating energy.")
                raise ValueError(err_msg)
            else:
                nu = self.obs_freq_central
            energy = F * nu * 4 * np.pi * D**2 * (1 + self.z)**-1

        return energy.to("erg")

    def _set_value_units(self, value, unit=None, non_negative=False):
        """
        Converts ``value`` into a :obj:`~astropy.unit.Quantity` with
        units. Also checks if ``value`` has existing units and the
        units are convertable with ``unit``. If ``value`` is a
        :obj:`~astropy.unit.Quantity` with convertable units then
        ``value`` is returned unchanged.

        Parameters
        ----------
        value: float, int, or :obj:`~astropy.unit.Quantity`
            The input value.

        unit: :obj:`astropy.unit` or None
            The unit to set for ``value``. If ``unit = None`` then
            ``value`` will become a dimensionless quantity.

        non_negative: bool, optional
            Raise an error if the value is negative. This is used to
            verify values that can only be positive are correctly
            specified. Default: *False*

        Return
        ------
        var : :obj:`~astropy.unit.Quantity` or None
            The output quantity with units. If ``value=None``
            returns *None*
        """
        # Check if the value already has units
        if not isinstance(value, u.Quantity):
            if value is None:
                var = None

            elif non_negative and value < 0.0:
                raise ValueError("Value must be greater than zero.")

            elif unit is None:
                var = value * u.dimensionless_unscaled

            else:
                var = value * unit
        else:
            # If passed units check that they are the correct units by checking
            # that they are equivalent

            if value.unit.is_equivalent(unit):
                var = value

            else:
                err_msg = ("Quantity expected to have units of {} instead was "
                           "passed units {} which is not convertable".format(
                               unit, value.unit))
                raise ValueError(err_msg)

        return var

    @property
    def name(self):
        """
        name: str
            The name of the FRB object.
        """
        return self._name

    @name.setter
    def name(self, value):
        if isinstance(value, str):
            self._name = value
        else:
            self._name = str(value)

    @property
    def dm(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The observed dispersion measure of the FRB. This is without
            Milky Way or host galaxy subtraction.
        """
        return self._dm

    @dm.setter
    def dm(self, value):
        self._dm = self._set_value_units(value,
                                         unit=u.pc * u.cm**-3,
                                         non_negative=True)

    @property
    def dm_galaxy(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The galactic component of the dispersion measure. This is
            quantity is calculated using :meth:`calc_dm_galaxy`.
        """
        return self._dm_galaxy

    @dm_galaxy.setter
    def dm_galaxy(self, value):
        self._dm_galaxy = self._set_value_units(value,
                                                unit=u.pc * u.cm**-3,
                                                non_negative=True)

    @property
    def dm_excess(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The dispersion measure with the Milky Way component
            subtracted. This value approximates the dispersion measure
            of the intergalatic medium by assuming that the host galaxy
            contributes zero to the observed dispersion measure. This
            quantity is calculated using :meth:`calc_dm_excess`.
        """
        return self._dm_excess

    @dm_excess.setter
    def dm_excess(self, value):
        self._dm_excess = self._set_value_units(value,
                                                u.pc * u.cm**-3,
                                                non_negative=True)

    @property
    def dm_host_est(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The estimated dispersion measure from the FRB host galaxy.
        """
        return self._dm_host_est

    @dm_host_est.setter
    def dm_host_est(self, value):
        self._dm_host_est = self._set_value_units(value,
                                                  u.pc * u.cm**-3,
                                                  non_negative=True)

    @property
    def dm_host_loc(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The dispersion measure from a localised FRB host galaxy.
        """
        return u.Quantity(self._dm_host_loc, u.pc * u.cm**-3)

    @dm_host_loc.setter
    def dm_host_loc(self, value):
        self._dm_host_loc = self._set_value_units(value,
                                                  u.pc * u.cm**-3,
                                                  non_negative=True)

    @property
    def dm_index(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The dispersion measure index of the burst.
        """
        return self._dm_index

    @dm_index.setter
    def dm_index(self, value):
        self._dm_index = self._set_value_units(value)

    @property
    def z(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The estimated redshift of the burst. By default this
            assumes that the entire :attr:`dm_excess` arrives from the
            IGM and the host galaxy of the FRB and any surrounding
            material contribute nothing to the total DM. This should be
            taken as an upper limit to the bursts true redshift. To
            provide an estimate of the DM contribution due to he host
            galaxy, set :attr:`dm_host_est` to a non-zero value and use
            ``subract_host=True`` when calling :meth:`calc_redshift()`.
        """
        return self._z

    @z.setter
    def z(self, value):
        self._z = self._set_value_units(value)

    @property
    def z_host(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The redshift of the localised FRB host galaxy. Note that
            this an observed quantity, not the estimated redshift
            :attr:`z` calculated with :meth:`calc_redshift()`.
        """
        return self._z_host

    @z_host.setter
    def z_host(self, value):
        self._z_host = self._set_value_units(value)

    @property
    def scatt_index(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The scattering index of the burst.
        """
        return self._scatt_index

    @scatt_index.setter
    def scatt_index(self, value):
        self._scatt_index = self._set_value_units(value)

    @property
    def width(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The observed width of the pulse.
        """
        return self._width

    @width.setter
    def width(self, value):
        self._width = self._set_value_units(value, u.ms, non_negative=True)

    @property
    def peak_flux(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The observed peak flux density of the FRB.
        """
        return self._peak_flux

    @peak_flux.setter
    def peak_flux(self, value):
        self._peak_flux = self._set_value_units(value, u.Jy, non_negative=True)

    @property
    def fluence(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The observed fluence of the FRB. This is calculated from
            :meth:`calc_fluence`.
        """
        return self._fluence

    @fluence.setter
    def fluence(self, value):
        self._fluence = self._set_value_units(value,
                                              u.Jy * u.ms,
                                              non_negative=True)

    @property
    def obs_bandwidth(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The observing bandwidth of the FRB.
        """
        return self._obs_bandwidth

    @obs_bandwidth.setter
    def obs_bandwidth(self, value):
        self._obs_bandwidth = self._set_value_units(value,
                                                    u.MHz,
                                                    non_negative=True)

    @property
    def obs_freq_central(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The central observing frequency of the FRB.
        """
        return self._obs_freq_central

    @obs_freq_central.setter
    def obs_freq_central(self, value):
        self._obs_freq_central = self._set_value_units(value,
                                                       u.MHz,
                                                       non_negative=True)

    @property
    def raj(self):
        """
        :obj:`astropy.coordinates.Longitude` or None:
            The right accension in J2000 coordinates of the best
            estimate of the FRB position. This is given in the IRCS
            frame.
        """
        return self._raj

    @raj.setter
    def raj(self, value):
        if value is None:
            self._raj = None
        else:
            self._raj = coord.angles.Longitude(value, u.hourangle)

    @property
    def decj(self):
        """
        :obj:`astropy.coordinates.Latitude` or None:
            The declination in J2000 coordinates of the best estimate
            of the FRB position. This is given in the IRCS frame.
        """
        return self._decj

    @decj.setter
    def decj(self, value):
        if value is None:
            self._decj = None
        else:
            self._decj = coord.angles.Latitude(value, u.deg)

    @property
    def gl(self):
        """
        :obj:`astropy.coordinates.Longitude` or None:
            The longitude in galactic coordinates of the best estimate
            of the FRB position.
        """
        return self._gl

    @gl.setter
    def gl(self, value):
        if value is None:
            self._gl = None
        else:
            self._gl = coord.angles.Longitude(value, u.deg)

    @property
    def gb(self):
        """
        :obj:`astropy.coordinates.Latitude` or None:
            The latitude in galactic coordinates of the best estimate
            of the FRB position.
        """
        return self._gb

    @gb.setter
    def gb(self, value):
        if value is None:
            self._gb = None
        else:
            self._gb = coord.angles.Latitude(value, u.deg)

    @property
    def skycoords(self):
        """
        :obj:`astropy.coordinates.SkyCoord` or None:
            The skycoords of the FRB. This is calculated from either
            (:attr:`raj`, :attr:`decj`) or (:attr:`gl`, :attr:`gb`)
            using :meth:`calc_skycoords`.
        """
        return self._skycoords

    @skycoords.setter
    def skycoords(self, value):
        if value is None:
            self._skycoords = None
        else:
            self._skycoords = SkyCoord(value)

    @property
    def dm_igm(self):
        """
        :obj:`astropy.units.Quantity` or None:
            The estimated disperison measure from the IGM. This can be
            calculated for a FRB for which the redshift of its host
            galaxy is known. This value is determined using
            :meth:`calc_dm_igm`.
        """
        return self._dm_igm

    @dm_igm.setter
    def dm_igm(self, value):
        self._dm_igm = self._set_value_units(value,
                                             u.pc * u.cm**-3,
                                             non_negative=True)

    @property
    def utc(self):
        """
        :obj:`astropy.time.Time` or None
        The UTC time of the burst.
        """
        return self._utc

    @utc.setter
    def utc(self, value):
        if value is None:
            self._utc = None
        else:
            self._utc = Time(value, format='isot', scale='utc')

    @property
    def snr(self):
        """
        :obj:`astropy.units.Quantity`:
        The signal-to-noise  ratio of the burst.
        """
        return self._snr

    @snr.setter
    def snr(self, value):
        self._snr = self._set_value_units(value, non_negative=True)

    @property
    def cosmology(self):
        """
        str or None:
            The cosmology used to calculate redshift.
        """
        return self._cosmology

    @cosmology.setter
    def cosmology(self, value):
        if isinstance(value, str):
            self._cosmology = value
        else:
            self._cosmology = str(value)

    @property
    def method(self):
        """
        str or None:
            The method used to calculate redshift.
        """
        return self._method

    @method.setter
    def method(self, value):
        if isinstance(value, str):
            self._method = value
        else:
            self._method = str(value)
Ejemplo n.º 10
0
fig, ax = plt.subplots(1, 1, constrained_layout=True)

extents = [0, 3, 5, -1]
im = ax.imshow(data["arr_0"].T,
               aspect=3 / 5,
               cmap="magma",
               interpolation='none',
               extent=extents)
ax.set_ylim(1, 4)
ax.set_xlim(0, 3)
ax.set_xlabel(r"Redshift")
ax.set_ylabel(r"$\rm{log\left(DM\right)}\ \left[pc\ cm^{-3}\right]$")

dm_vals = 10**np.linspace(1, 4, 1000)

method_list = methods.available_methods()

colours = ["#66c2a5", "#e7298a", "#8da0cb"]
label = [r"$\rm{Ioka\ 2003}$", r"$\rm{Inoue\ 2004}$", r"$\rm{Zhang\ 2018}$"]
lstyle = ["-", "--", "-."]

for j, method in enumerate(method_list):
    z_vals = np.zeros(len(dm_vals))
    cosmology = 'Planck18'

    table_name = "".join(["_".join([method, cosmology]), ".npz"])
    lookup_table = table.load(table_name)

    for i, dm in enumerate(dm_vals):
        z_vals[i] = table.get_z_from_table(dm, lookup_table)
    ax.plot(z_vals,
Ejemplo n.º 11
0
def create(method, output_dir='data', filename=None, zmin=0, zmax=20,
           num_samples=10000, **method_kwargs):
    """
    Creates lookup table


     which can be read
    in  using :func:`~fruitbat.table.load`.

    Parameters
    ----------
    method : str
        The DM-redshift relation to assume when creating the table.

    output_dir : str, optional
        The path of the output directory. If ``output_dir = 'data'``,
        then created table will created in the same directory with
        the builtin tables and will be found when using functions
        such as :meth:`~fruitbat._frb.calc_redshift()`.
        Default: 'data'

    filename : str, optional
        The output filename. If ``name=None`` then the filename will
        become custom_method. Default: *None*

    zmin : int or float, optional
        The minimum redshift in the table. Default: 0

    zmax : int or float, optional
        The maximum redshift in the table. Default: 20

    num_samples : int, optional
        The number of sample dispersion measure and redshifts.
        Default: 10000

    Keyword Arguments
    -----------------
    cosmo : An instance of :obj:`astropy.cosmology`, optional
        The cosmology to assume when calculating the outputs of the
        table. Required when creating new tables of ``'Ioka2003'``,
        ``'Inoue2004'``, ``'Zhang2018'``.

    free_elec : float or int, optional
        The amount of free electrons per proton mass in the Universe.
        This applies when using ``'Zhang2018'``. Must be between 0
        and 1. Default: 0.875.

    f_igm : float or int, optional
        The fraction of baryons in the intergalactic medium. This
        applies when using ``'Zhang2018'``. Must be between 0 and 1.
        Default: 0.83


    Returns
    -------
    string
        The path to the generated hdf5 file containing the table data.

    Generates
    ---------
    A hdf5 file containing datasets for `'DM'` and 'z'`.


    Example
    -------
    >>> def simple_dm(z):
        dm = 1200 * z
        return dm
    >>> fruitbat.add_method("simple_dm", simple_dm)
    >>> fruitbat.table.create("simple_dm")
    >>> frb = fruitbat.Frb(1200)
    >>> frb.calc_redshift(method="simple_dm")
    <Quantity 1.>

    """

    if method not in available_methods():
        err_msg = ("{} is not a valid method."
                   "The currently defined methods "
                   "are: {}".format(method, available_methods()))
        raise ValueError(err_msg)

    # The filename that the user provides can not be one of these as it would
    # over write the built in datasets.
    restricted_filenames = set([
        utils.get_path_to_file_from_here("Ioka2003.hdf5", subdirs=["data"]),
        utils.get_path_to_file_from_here("Inoue2004.hdf5", subdirs=["data"]),
        utils.get_path_to_file_from_here("Zhang2018.hdf5", subdirs=["data"]),
        utils.get_path_to_file_from_here("Batten2020.hdf5", subdirs=["data"]),
    ])

    # Generate the output filename.
    if (filename is not None) and (filename not in restricted_filenames):
        filename = "{}.hdf5".format(filename)
    else:
        filename = "{}.hdf5".format(method)

    # Generate the DM and z values for the method
    dm_function = method_functions()[method]
    z_vals = np.linspace(zmin, zmax, num_samples)
    dm_vals = np.array([dm_function(z, **method_kwargs) for z in z_vals])

    if output_dir == "data":
        output_file = utils.get_path_to_file_from_here(filename, subdirs=["data"])

    else:
        output_file = os.path.join(output_dir, filename)


    with h5py.File(output_file, "w") as new_table:
        new_table.create_group("Header")

        if "cosmo" in method_kwargs:
            cosmo = method_kwargs["cosmo"]
            add_cosmo_params_to_dataset_attrs = True

            # Use a default cosmology name if the prodived one doesnt have a name
            if not cosmo.name:
                cosmo.name = "cosmology"

            dataset_group_name = cosmo.name
        else:
            add_cosmo_params_to_dataset_attrs = False
            dataset_group_name = method

        new_table.create_group(dataset_group_name)

        new_table[dataset_group_name].create_dataset("DM", data=dm_vals, dtype=np.float)
        new_table[dataset_group_name]["DM"].attrs["Units"] = "pc cm**-3"
        new_table[dataset_group_name]["DM"].attrs["VarDesc"] = "Dispersion Measure"

        new_table[dataset_group_name].create_dataset("z", data=z_vals, dtype=np.float)
        new_table[dataset_group_name]["z"].attrs["Units"] = "Dimensionless"
        new_table[dataset_group_name]["z"].attrs["VarDesc"] = "Redshift"

        if add_cosmo_params_to_dataset_attrs:
            new_table[dataset_group_name].attrs["Flat"] = True
            new_table[dataset_group_name].attrs["H0"] = cosmo.H0.value
            new_table[dataset_group_name].attrs["OmegaBaryon0"] = cosmo.Ob0
            new_table[dataset_group_name].attrs["OmegaLambda0"] = cosmo.Ode0
            new_table[dataset_group_name].attrs["OmegaMatter0"] = cosmo.Om0
            new_table[dataset_group_name].attrs["t0"] = cosmo.age(0).value

    return output_file
Ejemplo n.º 12
0
def method_comparison(filename=None, extension="png", usetex=False,
                      passed_ax=None, **kwargs):
    """
    Create a plot comparing how estimated redshift changes as a
    function of dispersion measure for each DM-z relation.

    Parameters
    ----------
    filename: string or None, optional
        The filename of the saved figure. Default: *None*

    extension: string, optional
        The format to save the figure. e.g "png", "pdf", "eps", etc...
        Default: "png"

    usetex: bool, optional
        Use LaTeX for for fonts. 

    passed_ax: or None, optional

    Generates
    ---------
    A figure displaying how estimated redshift changes as a function of
    dispersion measure for each of the different cosmologies.
    """
    set_rc_params(usetex)

    if passed_ax:
        ax = passed_ax
    else:
        fig = plt.figure(figsize=(8, 8), constrained_layout=True)
        ax = fig.add_subplot(111)

    method_list = methods.available_methods()
    dm_vals = np.linspace(0, 3000, 1000)

    colours = ["#1b9e77", "#d95f02", "#7570b3"]
    label = [r"$\rm{Ioka 2003}$", r"$\rm{Inoue 2004}$", r"$\rm{Zhang 2018}$"]

    for j, method in enumerate(method_list):
        z_vals = np.zeros(len(dm_vals))
        if 'cosmology' in kwargs:
            cosmology = kwargs['cosmology']
        else:
            cosmology = 'Planck18'

        table_name = "".join(["_".join([method, cosmology]), ".npz"])
        lookup_table = table.load(table_name)

        for i, dm in enumerate(dm_vals):
            z_vals[i] = table.get_z_from_table(dm, lookup_table)

        ax.plot(dm_vals, z_vals, colours[j], label=label[j], **kwargs)

    if not passed_ax:
        ax.set_ylabel(r"$\rm{Redshift}$")


    ax.set_xlabel(r"$\rm{DM\ \left[pc \ cm^{-3}\right]}$")
    ax.legend(loc='lower right', frameon=False)

    if filename is not None:
        plt.savefig(".".join([filename, extension]))

    if passed_ax:
        return ax
    else:
        return fig