Skip to content

Commit

Permalink
Add PSP Radio Frequency Spectrometner client
Browse files Browse the repository at this point in the history
* Add new generic client
* Add tests
* Add to API docs
  • Loading branch information
Shane Maloney committed Oct 22, 2020
1 parent 722bd97 commit 81047ef
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 1 deletion.
1 change: 1 addition & 0 deletions changelog/34.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add Parker Solar Probe (PSP) Radio Frequency Receiver (RFS) Fido client `radiospectra.net.sources.psp.RFSClient`.
2 changes: 1 addition & 1 deletion docs/code_ref/sources.rst
Original file line number Diff line number Diff line change
@@ -1 +1 @@
.. automodapi:: radiospectra.sources
.. automodapi:: radiospectra.net
4 changes: 4 additions & 0 deletions radiospectra/net/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Import to register with Fido but keep out of namespace
from radiospectra.net.sources.psp import RFSClient

__all__ = ['RFSClient']
Empty file.
137 changes: 137 additions & 0 deletions radiospectra/net/sources/psp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import astropy.units as u
from sunpy.net import attrs as a
from sunpy.net.dataretriever.client import GenericClient, QueryResponse
from sunpy.util.scraper import Scraper

__all__ = ['RFSClient']

RECEIVER_FREQUENCIES = {
'rfs_lfr': a.Wavelength(10*u.kHz, 1.7*u.MHz),
'rfs_hfr': a.Wavelength(1.3*u.MHz, 19.2*u.MHz)
}


class RFSClient(GenericClient):
"""
Provides access to Parker Solar Probe FIELDS Radio Frequency Spectrometer data
`archive <https://spdf.gsfc.nasa.gov/pub/data/psp/fields/>`__ at `NASA Goddard Space Physics
Data Facility (SPDF) <https://spdf.gsfc.nasa.gov>`__.
Examples
--------
>>> import radiospectra.net
>>> from sunpy.net import Fido, attrs as a
>>> results = Fido.search(a.Time("2019/10/02", "2019/10/05"),
... a.Instrument('rfs')) #doctest: +REMOTE_DATA
>>> print(results) #doctest: +REMOTE_DATA
Results from 1 Provider:
<BLANKLINE>
8 Results from the RFSClient:
Start Time End Time ... Provider Wavelength [2]
------------------- ------------------- ... -------- -----------------
2019-10-02 00:00:00 2019-10-02 23:59:59 ... SPDF 10.0 .. 1700.0
2019-10-03 00:00:00 2019-10-03 23:59:59 ... SPDF 10.0 .. 1700.0
2019-10-04 00:00:00 2019-10-04 23:59:59 ... SPDF 10.0 .. 1700.0
2019-10-05 00:00:00 2019-10-05 23:59:59 ... SPDF 10.0 .. 1700.0
2019-10-02 00:00:00 2019-10-02 23:59:59 ... SPDF 1300.0 .. 19200.0
2019-10-03 00:00:00 2019-10-03 23:59:59 ... SPDF 1300.0 .. 19200.0
2019-10-04 00:00:00 2019-10-04 23:59:59 ... SPDF 1300.0 .. 19200.0
2019-10-05 00:00:00 2019-10-05 23:59:59 ... SPDF 1300.0 .. 19200.0
<BLANKLINE>
<BLANKLINE>
"""

baseurl = (r'https://spdf.gsfc.nasa.gov/pub/data/psp/fields/l2/{Wavelength}/'
r'{year}/psp_fld_l2_(\w){{7}}_(\d){{8}}_v(\d){{2}}.cdf')
pattern = r'{}/{Wavelength}/{year:4d}/psp_fld_l2_{Wavelength}_{year:4d}{month:2d}{day:2d}_v{:2d}.cdf'

@classmethod
def _check_wavelengths(cls, wavelength):
"""
Check for overlap between given wavelength and receiver frequency coverage defined in `RECEIVER_FREQUENCIES`.
Parameters
----------
wavelength : `sunpy.net.attrs.Wavelength`
Input wavelength range to check
Returns
-------
`list`
List of receivers names or empty list if no overlap
"""
# Input wavelength range is completely contained in one receiver range
receivers = [k for k, v in RECEIVER_FREQUENCIES.items() if wavelength in v]
# If not defined need to continue
if not receivers:
# Overlaps but not contained in, either max in lfr or min hfr
if wavelength.min in RECEIVER_FREQUENCIES['rfs_hfr'] or wavelength.max in RECEIVER_FREQUENCIES['rfs_hfr']:
receivers.append('rfs_hfr')
if wavelength.min in RECEIVER_FREQUENCIES['rfs_lfr'] or wavelength.max in RECEIVER_FREQUENCIES['rfs_lfr']:
receivers.append('rfs_lfr')
# min in lfr and max in hfr
# min and max of combined lft and hfr contained in give wavelength range
if a.Wavelength(RECEIVER_FREQUENCIES['rfs_lfr'].min,
RECEIVER_FREQUENCIES['rfs_hfr'].max) in wavelength:
receivers = ['rfs_lfr', 'rfs_hfr']
# If we get here the is no overlap so set to empty list
return receivers

def search(self, *args, **kwargs):
"""
Query this client for a list of results.
Parameters
----------
*args: `tuple`
`sunpy.net.attrs` objects representing the query.
**kwargs: `dict`
Any extra keywords to refine the search.
Returns
-------
A `QueryResponse` instance containing the query result.
"""
matchdict = self._get_match_dict(*args, **kwargs)
req_wave = matchdict.get('Wavelength', None)
receivers = RECEIVER_FREQUENCIES.keys()
if req_wave is not None:
receivers = self._check_wavelengths(req_wave)

metalist = []
start_year = matchdict['Time'].start.datetime.year
end_year = matchdict['Time'].end.datetime.year
for receiver in receivers:
for year in range(start_year, end_year+1):
urlpattern = self.baseurl.format(Wavelength=receiver, year=year)
scraper = Scraper(urlpattern, regex=True)
filesmeta = scraper._extract_files_meta(matchdict['Time'], extractor=self.pattern)
for i in filesmeta:
rowdict = self.post_search_hook(i, matchdict)
metalist.append(rowdict)

return QueryResponse(metalist, client=self)

def post_search_hook(self, exdict, matchdict):
"""
This method converts 'rfs_hfr' and 'rfs_lfr' in the url's metadata
to the frequency ranges of for low and high frequency receivers.
"""
rowdict = super().post_search_hook(exdict, matchdict)
if rowdict['Wavelength'] == 'rfs_hfr':
fr = RECEIVER_FREQUENCIES['rfs_hfr']
rowdict['Wavelength'] = u.Quantity([float(fr.min.value), float(fr.max.value)], unit=fr.unit)
elif rowdict['Wavelength'] == 'rfs_lfr':
fr = RECEIVER_FREQUENCIES['rfs_lfr']
rowdict['Wavelength'] = u.Quantity([float(fr.min.value), float(fr.max.value)], unit=fr.unit)
return rowdict

@classmethod
def register_values(cls):
from sunpy.net import attrs
adict = {attrs.Instrument: [('RFS',
('Radio Frequency Spectrometer'))],
attrs.Source: [('PSP', 'Parker Solar Probe')],
attrs.Provider: [('SPDF', 'NASA Goddard Space Physics Data Facility')],
attrs.Wavelength: [('*')]}
return adict
Empty file.
89 changes: 89 additions & 0 deletions radiospectra/net/sources/tests/test_psp_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import numpy as np
import pytest

import astropy.units as u
from astropy.time import Time
from sunpy.net import Fido
from sunpy.net import attrs as a

from radiospectra.net.sources.psp import RFSClient

client = RFSClient()


@pytest.mark.parametrize("req_wave,receivers", [
# Completely contain the both receiver ranges
(a.Wavelength(1*u.kHz, 25000*u.kHz), ['rfs_lfr', 'rfs_hfr']),
# Min in lower freq and max in high freq receiver
(a.Wavelength(20*u.kHz, 15*u.MHz), ['rfs_lfr', 'rfs_hfr']),
# Min below and max in low freq receiver
(a.Wavelength(1*u.kHz, 100*u.kHz), ['rfs_lfr']),
# Min and max in low freq receiver
(a.Wavelength(20*u.kHz, 100*u.kHz), ['rfs_lfr']),
# Min and max in high freq receiver
(a.Wavelength(1800*u.kHz, 18000*u.kHz), ['rfs_hfr']),
# Min in high freq receiver and max above
(a.Wavelength(1800*u.kHz, 20000*u.kHz), ['rfs_hfr']),
# Min and max in the over lap
(a.Wavelength(1.4*u.MHz, 1.5*u.MHz), ['rfs_lfr', 'rfs_hfr'])
])
def test_check_wavelength(req_wave, receivers):
res = RFSClient._check_wavelengths(req_wave)
assert set(res) == set(receivers)


@pytest.mark.remote_data
def test_fido():
atr = a.Time('2019/10/01', '2019/10/02')
res = Fido.search(atr, a.Instrument('rfs'))
res0 = res.get_response(0)
isinstance(res0.client, RFSClient)
assert len(res0) == 4
tr = res0.time_range()
assert tr.start.datetime == Time('2019-10-01T00:00').datetime
assert tr.end.datetime == Time('2019-10-02T23:59:59.999').datetime


@pytest.mark.remote_data
def test_search_with_wavelength():
tr = a.Time('2019/10/13', '2019/10/15')
wr1 = a.Wavelength(1*u.kHz, 1.1*u.MHz)
res1 = client.search(tr, wr1)
assert np.array_equal(res1.blocks[0]['Wavelength'], [10, 1700] * u.kHz)
assert len(res1) == 3
assert res1.time_range().start == Time('2019-10-13T00:00').datetime
assert res1.time_range().end == Time('2019-10-15T23:59:59.999').datetime
wr2 = a.Wavelength(2*u.MHz, 20*u.MHz)
res2 = client.search(tr, wr2)
assert np.array_equal(res2.blocks[0]['Wavelength'], [1300, 19200] * u.kHz)
assert len(res2) == 3
assert res2.time_range().start == Time('2019-10-13T00:00').datetime
assert res2.time_range().end == Time('2019-10-15T23:59:59.999').datetime


@pytest.mark.remote_data
def test_get_url_for_time_range():
url_start = 'https://spdf.gsfc.nasa.gov/pub/data/psp/fields/l2/rfs_lfr/2019/' \
'psp_fld_l2_rfs_lfr_20191001_v02.cdf'
url_end = 'https://spdf.gsfc.nasa.gov/pub/data/psp/fields/l2/rfs_hfr/2019/' \
'psp_fld_l2_rfs_hfr_20191015_v02.cdf'
tr = a.Time('2019/10/01', '2019/10/15')
res = client.search(tr)
urls = [i['url'] for i in res]
assert urls[0] == url_start
assert urls[-1] == url_end


def test_can_handle_query():
atr = a.Time('2019/10/01', '2019/11/01')
res = client._can_handle_query(atr, a.Instrument('rfs'))
assert res is True
res = client._can_handle_query(atr)
assert res is False


@pytest.mark.remote_data
def test_get():
query = client.search(a.Time('2019/10/05', '2019/10/10'), a.Instrument('rfr'))
download_list = client.fetch(query)
assert len(download_list) == len(query)

0 comments on commit 81047ef

Please sign in to comment.