Repository: mattpitkin/psrqpy

Issue:
Add parsing of the Galactic MSP table
Similarly to #9, we could add a function to get and parse [Duncan Lorimer's](https://astro.wvu.edu/people/facultystaff/dlorimer) [Galactic MSP](http://astro.phys.wvu.edu/GalacticMSPs/) [table](http://astro.phys.wvu.edu/GalacticMSPs/GalacticMSPs.txt).

Discussion:

Patch:
diff --git a/docs/source/index.rst b/docs/source/index.rst
index b25f64c..2d89dee 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -273,6 +273,9 @@ download and parse:
 * Paolo Freire's `table of pulsars in globular clusters <http://www.naic.edu/~pfreire/GCpsr.txt>`_
   (note that the downloaded table does not include accompanying notes or references, and binary
   parameters that are listed as upper/lower limits within the table are just returned as equalities.)
+* Duncan Lorimer & Elizabeth Ferrara's `table of galactic millisecond pulsars
+  <http://astro.phys.wvu.edu/GalacticMSPs/>`_ (accessed via the `JSON-ified version
+  <https://github.com/astrogewgaw/galmsps>`_ by `Ujjwal Panda <https://github.com/astrogewgaw>`_)
 
 Examples
 --------
@@ -287,7 +290,7 @@ The glitches for a single pulsar can be returned with, e.g.:
 
     >>> crabglitches = get_glitch_catalogue(psr="J0534+2200")
 
-Accessing the globular cluster tables can be achieved with the
+Accessing the globular cluster table can be achieved with the
 :func:`~psrqpy.utils.get_gc_catalogue` function using:
 
     >>> from psrqpy.utils import get_gc_catalogue
@@ -297,6 +300,12 @@ The pulsars with a single globular cluster, e.g., 47 Tuc, can the be returned wi
 
     >>> tucpulsars = gctable.cluster_pulsars("47 Tuc")
 
+Accessing the MSP table can be achieved with the :func:`~psrqpy.utils.get_msp_catalogue` function
+using:
+
+    >>> from psrqpy.utils import get_msp_catalogue
+    >>> msps = get_msp_catalogue()
+
 Differences with the ATNF Pulsar Catalogue
 ==========================================
 
diff --git a/psrqpy/config.py b/psrqpy/config.py
index 1ab5547..2225084 100644
--- a/psrqpy/config.py
+++ b/psrqpy/config.py
@@ -19,6 +19,9 @@ GLITCH_URL = r"http://www.jb.man.ac.uk/pulsar/glitches/gTable.html"
 #: Paolo Freire's globular cluster pulsar table URL
 GC_URL = r"http://www.naic.edu/~pfreire/GCpsr.txt"
 
+#: JSON-ified version of Dunc Lorimer's MSP table URL
+MSP_URL = r"https://raw.githubusercontent.com/astrogewgaw/galmsps/main/galmsps.json"
+
 # Pulsar parameters (http://www.atnf.csiro.au/research/pulsar/psrcat/psrcat_help.html) that can be
 # queried. For each parameter there is a dictionary giving:
 #  - 'ref': True if the parameter can have an associated reference in the ATNF catalogue
diff --git a/psrqpy/utils.py b/psrqpy/utils.py
index f94426d..9fa5c95 100644
--- a/psrqpy/utils.py
+++ b/psrqpy/utils.py
@@ -25,6 +25,7 @@ from .config import (
     ATNF_BASE_URL,
     ADS_URL,
     ATNF_TARBALL,
+    MSP_URL,
     PSR_ALL,
     PSR_ALL_PARS,
     GLITCH_URL,
@@ -734,6 +735,75 @@ def get_gc_catalogue():
     return GCTable(gctable)
 
 
+def get_msp_catalogue():
+    """
+    Download and parse Dunc Lorimer's `catalogue
+    <http://astro.phys.wvu.edu/GalacticMSPs/GalacticMSPs.txt>`_ of galactic
+    millisecond pulsars. This makes use of `@astrogewgaw <https://github.com/astrogewgaw>`_'s
+    scraped version of the `table <https://github.com/astrogewgaw/galmsps>`_.
+
+    Returns:
+        :class:`~astropy.table.Table`: a table the MSPs.
+    """
+
+    import json
+
+    # get the webpage
+    try:
+        mspt = requests.get(MSP_URL)
+    except Exception as e:
+        raise RuntimeError("Error downloading the MSP table: {}".format(str(e)))
+
+    if mspt.status_code != 200:
+        warnings.warn("Count not query the MSP table.", UserWarning)
+        return None
+
+    # convert from btyes to utf-8
+    tabledata = json.loads(mspt.content.decode("utf-8"))
+
+    # conversion between column names to be consistent with ATNF
+    colnames = {
+        "NAME": "NAME",
+        "P": "P0",  # period
+        "DM": "DM",  # dispersion measure
+        "GAL_L": "GL",  # galactic longitude
+        "GAL_B": "GB",  # galactic latitude,
+        "PBIN": "PB",  # binary period
+        "PSMA": "A1",  # projected semi-major axis
+        "YEAR": "DISCOVERY YEAR",  # discovery year
+        "NOTES": "NOTES",
+    }
+
+    # units for the columns
+    units = {
+        "NAME": None,
+        "P0": aunits.s * 1e-3,
+        "DM": aunits.cm ** -3 * aunits.pc,
+        "GL": aunits.deg,
+        "GB": aunits.deg,
+        "PB": aunits.d,
+        "A1": aunits.s,
+        "DISCOVERY YEAR": aunits.year,
+        "NOTES": None,
+    }
+
+    # create dictionary in form usable by astropy Table
+    tabledict = {
+        colnames[key]: [item[key] for item in tabledata.values()]
+        for key in list(tabledata.values())[0]
+    }
+
+    # convert into astropy Table
+    msptable = Table(
+        data=tabledict,
+        units=units,
+        dtype=[str, float, float, float, float, float, float, int, str],
+        masked=True,
+    )
+
+    return msptable
+
+
 def check_old_references(func):
     @functools.wraps(func)
     def wrapper_check_old_references(*args, **kwargs):


Test patch:
diff --git a/test/query_test.py b/test/query_test.py
index 2fe5e62..b3d1ee4 100644
--- a/test/query_test.py
+++ b/test/query_test.py
@@ -442,6 +442,31 @@ def test_gc_table():
     assert "J0023-7204C" in table.cluster_pulsars("47Tuc")["Pulsar"]
 
 
+def test_msp_table(query):
+    """
+    Try downloading the MSP table.
+    """
+
+    from psrqpy.utils import get_msp_catalogue
+
+    table = get_msp_catalogue()
+
+    # when added there were 400 pulsars in the table
+    assert len(table) >= 400
+    psrcheck = "J0023+0923"
+    assert psrcheck in table["NAME"]
+
+    # compare values to ATNF values to with 1%
+    psr = query.get_pulsar(psrcheck)
+    msppsr = table[table["NAME"] == psrcheck]
+
+    assert 0.99 < psr["GB"] / msppsr["GB"] < 1.01
+    assert 0.99 < psr["GL"] / msppsr["GL"] < 1.01
+    assert 0.99 < psr["DM"] / msppsr["DM"] < 1.01
+    assert 0.99 < psr["A1"] / msppsr["A1"] < 1.01
+    assert 0.99 < psr["PB"] / msppsr["PB"] < 1.01
+
+
 # TEST DERIVED PARAMETERS #
 def test_derived_p0_p1(query_derived, query_atnf):
     """
@@ -919,9 +944,9 @@ def test_download_glitch_table():
 
 
 @pytest.mark.disable_socket
-def test_download_glitch_table():
+def test_download_gc_table():
     """
-    Try downloading the glitch table with the socket disabled.
+    Try downloading the globular cluster table with the socket disabled.
     """
 
     from psrqpy.utils import get_gc_catalogue
@@ -930,6 +955,18 @@ def test_download_glitch_table():
         _ = get_gc_catalogue()
 
 
+@pytest.mark.disable_socket
+def test_download_msp_table():
+    """
+    Try downloading the MSP table with the socket disabled.
+    """
+
+    from psrqpy.utils import get_msp_catalogue
+
+    with pytest.raises(RuntimeError):
+        _ = get_msp_catalogue()
+
+
 def test_sort_exception(query):
     """
     Test exception in sort method.