Skip to content

Commit 101c3a5

Browse files
committed
Merge branch 'staging'
2 parents 9aa61fe + 325bcb6 commit 101c3a5

File tree

6 files changed

+226
-52
lines changed

6 files changed

+226
-52
lines changed

docs/release_notes.rst

+6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Release notes
44
Change log
55
----------
66

7+
Changes from 0.9.0 to 0.9.1
8+
^^^^^^^^^^^^^^^^^^^^^^^^^^^
9+
10+
- Added :py:meth:`viresclient.SwarmRequest.get_conjunctions` to fetch Swarm A/B conjunctions
11+
- Fixed compatibility with xarray v0.19 of ``reshape`` kwarg in :py:meth:`viresclient.ReturnedData.as_xarray`
12+
713
Changes from 0.8.0 to 0.9.0
814
^^^^^^^^^^^^^^^^^^^^^^^^^^^
915

viresclient/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,4 @@
3737
from . import _data
3838

3939

40-
__version__ = "0.9.0"
40+
__version__ = "0.9.1"

viresclient/_client.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -247,13 +247,13 @@ class ClientRequest(object):
247247
"""Base class handling the requests to and downloads from the server.
248248
"""
249249

250-
def __init__(self, url=None, username=None, password=None, token=None,
250+
def __init__(self, url=None, token=None,
251251
config=None, logging_level="NO_LOGGING", server_type=None):
252252

253253
# Check and prompt for token if not already set, then store in config
254254
# Try to only do this if running in a notebook
255255
if IN_JUPYTER:
256-
if not ((username and password) or token or config):
256+
if not (token or config):
257257
cc = ClientConfig()
258258
# Use production url if none chosen
259259
url = url or cc.default_url or "https://vires.services/ows"
@@ -275,7 +275,7 @@ def __init__(self, url=None, username=None, password=None, token=None,
275275
set_stream_handler(self._logger, logging_level)
276276

277277
self._wps_service = self._create_service_proxy_(
278-
config, url, username, password, token
278+
config, url, None, None, token
279279
)
280280
# Test if the token is working; re-enter if not
281281
if IN_JUPYTER:
@@ -292,7 +292,7 @@ def __init__(self, url=None, username=None, password=None, token=None,
292292
raise AuthenticationError(AUTH_ERROR_TEXT)
293293
set_token(url)
294294
self._wps_service = self._create_service_proxy_(
295-
config, url, username, password, token
295+
config, url, None, None, token
296296
)
297297

298298
def _create_service_proxy_(self, config, url, username, password, token):

viresclient/_client_swarm.py

+122-25
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@
2323
'async': "vires_fetch_filtered_data_async.xml",
2424
'model_info': "vires_get_model_info.xml",
2525
'times_from_orbits': "vires_times_from_orbits.xml",
26-
'get_observatories': 'vires_get_observatories.xml'
26+
'get_observatories': 'vires_get_observatories.xml',
27+
'get_conjunctions': 'vires_get_conjunctions.xml',
2728
}
2829

2930
REFERENCES = {
@@ -401,8 +402,6 @@ class SwarmRequest(ClientRequest):
401402
402403
Args:
403404
url (str):
404-
username (str):
405-
password (str):
406405
token (str):
407406
config (str or ClientConfig):
408407
logging_level (str):
@@ -415,6 +414,10 @@ class SwarmRequest(ClientRequest):
415414
'CryoSat-2': None,
416415
}
417416

417+
CONJUNCTION_MISISON_SPACECRAFT_PAIRS = {
418+
(('Swarm', 'A'), ('Swarm', 'B')),
419+
}
420+
418421
COLLECTIONS = {
419422
"MAG": ["SW_OPER_MAG{}_LR_1B".format(x) for x in "ABC"],
420423
"MAG_HR": ["SW_OPER_MAG{}_HR_1B".format(x) for x in "ABC"],
@@ -728,7 +731,7 @@ class SwarmRequest(ClientRequest):
728731
"B_FGM1", "B_FGM2", "B_FGM3", "q_NEC_CRF", "q_error",
729732
],
730733
"MAG_GRACE": [
731-
"F", "B_NEC", "B_NEC_raw", "B_FGM", "B_mod_NEC",
734+
"F", "B_NEC", "B_NEC_raw", "B_FGM",
732735
"q_NEC_CRF", "q_error",
733736
],
734737
"MAG_GFO": [
@@ -770,10 +773,10 @@ class SwarmRequest(ClientRequest):
770773
"MCO_SHA_2X", "CHAOS", "CHAOS-MMA", "MMA_SHA_2C", "MMA_SHA_2F", "MIO_SHA_2C", "MIO_SHA_2D", "SwarmCI",
771774
]
772775

773-
def __init__(self, url=None, username=None, password=None, token=None,
776+
def __init__(self, url=None, token=None,
774777
config=None, logging_level="NO_LOGGING"):
775778
super().__init__(
776-
url, username, password, token, config, logging_level,
779+
url, token, config, logging_level,
777780
server_type="Swarm"
778781
)
779782

@@ -1309,16 +1312,38 @@ def get_times_for_orbits(self, start_orbit, end_orbit, mission="Swarm", spacecra
13091312
start_orbit = int(start_orbit)
13101313
end_orbit = int(end_orbit)
13111314

1312-
if mission not in self.MISSION_SPACECRAFTS:
1313-
raise ValueError(
1314-
f"Invalid mission {mission}!"
1315-
f"Allowed options are: {','.join(self.MISSION_SPACECRAFTS)}"
1316-
)
1315+
# Change to spacecraft = "A" etc. for this request
1316+
spacecraft = self._fix_spacecraft(mission, spacecraft)
1317+
self._check_mission_spacecraft(mission, spacecraft)
1318+
1319+
templatefile = TEMPLATE_FILES["times_from_orbits"]
1320+
template = JINJA2_ENVIRONMENT.get_template(templatefile)
1321+
request = template.render(
1322+
mission=mission,
1323+
spacecraft=spacecraft,
1324+
start_orbit=start_orbit,
1325+
end_orbit=end_orbit
1326+
).encode('UTF-8')
1327+
response = self._get(
1328+
request, asynchronous=False, show_progress=False)
1329+
responsedict = json.loads(response.decode('UTF-8'))
1330+
start_time = parse_datetime(responsedict['start_time'])
1331+
end_time = parse_datetime(responsedict['end_time'])
1332+
return start_time, end_time
13171333

1334+
def _fix_spacecraft(self, mission, spacecraft):
13181335
# Change to spacecraft = "A" etc. for this request
13191336
spacecraft = str(spacecraft) if spacecraft is not None else None
13201337
if mission == "Swarm" and spacecraft in ("Alpha", "Bravo", "Charlie"):
13211338
spacecraft = spacecraft[0]
1339+
return spacecraft
1340+
1341+
def _check_mission_spacecraft(self, mission, spacecraft):
1342+
if mission not in self.MISSION_SPACECRAFTS:
1343+
raise ValueError(
1344+
f"Invalid mission {mission}!"
1345+
f"Allowed options are: {','.join(self.MISSION_SPACECRAFTS)}"
1346+
)
13221347

13231348
if self.MISSION_SPACECRAFTS[mission]:
13241349
# missions with required spacecraft id
@@ -1339,20 +1364,6 @@ def get_times_for_orbits(self, start_orbit, end_orbit, mission="Swarm", spacecra
13391364
"Set spacecraft to None."
13401365
)
13411366

1342-
templatefile = TEMPLATE_FILES["times_from_orbits"]
1343-
template = JINJA2_ENVIRONMENT.get_template(templatefile)
1344-
request = template.render(
1345-
mission=mission,
1346-
spacecraft=spacecraft,
1347-
start_orbit=start_orbit,
1348-
end_orbit=end_orbit
1349-
).encode('UTF-8')
1350-
response = self._get(
1351-
request, asynchronous=False, show_progress=False)
1352-
responsedict = json.loads(response.decode('UTF-8'))
1353-
start_time = parse_datetime(responsedict['start_time'])
1354-
end_time = parse_datetime(responsedict['end_time'])
1355-
return start_time, end_time
13561367

13571368
def get_orbit_number(self, spacecraft, input_time, mission="Swarm"):
13581369
"""Translate a time to an orbit number.
@@ -1506,3 +1517,89 @@ def _check_deprecated_models(models):
15061517
print("WARNING: Model {} is deprecated. {}".format(
15071518
deprecated_model, DEPRECATED_MODELS[deprecated_model]
15081519
), file=sys.stdout)
1520+
1521+
1522+
def get_conjunctions(self, start_time=None, end_time=None, threshold=1.0,
1523+
spacecraft1='A', spacecraft2='B',
1524+
mission1='Swarm', mission2='Swarm'):
1525+
""" Get times of the spacecraft conjunctions. Currently available for
1526+
the following spacecraft pairs:
1527+
- Swarm-A/Swarm-B
1528+
1529+
Args:
1530+
start_time (datetime / ISO_8601 string): optional start time
1531+
end_time (datetime / ISO_8601 string): optional end time
1532+
threshold (float): optional maximum allowed angular separation
1533+
in degrees; by default set to 1; allowed values are [0, 180]
1534+
spacecraft1: identifier of the first spacecraft, default to 'A'
1535+
spacecraft2: identifier of the second spacecraft, default to 'B'
1536+
mission1 (str): mission of the first spacecraft, defaults to 'Swarm'
1537+
mission2 (str): mission of the first spacecraft, defaults to 'Swarm'
1538+
1539+
Returns:
1540+
ReturnedData:
1541+
"""
1542+
try:
1543+
start_time = parse_datetime(start_time) if start_time else None
1544+
end_time = parse_datetime(end_time) if end_time else None
1545+
except TypeError:
1546+
raise TypeError(
1547+
"start_time and end_time must be datetime objects or ISO-8601 "
1548+
"date/time strings"
1549+
) from None
1550+
1551+
if not (0 <= threshold <= 180):
1552+
raise ValueError("Invalid threshold value!")
1553+
1554+
spacecraft1 = self._fix_spacecraft(mission1, spacecraft1)
1555+
spacecraft2 = self._fix_spacecraft(mission2, spacecraft2)
1556+
1557+
self._check_mission_spacecraft(mission1, spacecraft1)
1558+
self._check_mission_spacecraft(mission2, spacecraft2)
1559+
1560+
if (mission1, spacecraft1) == (mission2, spacecraft2):
1561+
raise ValueError(
1562+
"The first and second spacecraft must not be the same!"
1563+
)
1564+
1565+
spacecraft_pair = tuple(sorted([
1566+
(mission1, spacecraft1), (mission2, spacecraft2)
1567+
]))
1568+
1569+
if spacecraft_pair not in self.CONJUNCTION_MISISON_SPACECRAFT_PAIRS:
1570+
raise ValueError(
1571+
"Conjunctions not available for the requested "
1572+
"spacecraft pair {spacecraft_pair}!"
1573+
)
1574+
1575+
templatefile = TEMPLATE_FILES["get_conjunctions"]
1576+
template = JINJA2_ENVIRONMENT.get_template(templatefile)
1577+
request = template.render(
1578+
begin_time=start_time,
1579+
end_time=end_time,
1580+
spacecraft1=spacecraft1,
1581+
spacecraft2=spacecraft2,
1582+
mission1=mission1,
1583+
mission2=mission2,
1584+
threshold=threshold,
1585+
).encode('UTF-8')
1586+
1587+
show_progress = False
1588+
leave_progress_bar = False
1589+
response = ReturnedDataFile(filetype="cdf")
1590+
1591+
response_handler = self._response_handler(
1592+
retdatafile=response,
1593+
show_progress=show_progress,
1594+
leave_progress_bar=leave_progress_bar,
1595+
)
1596+
1597+
self._get(
1598+
request=request,
1599+
asynchronous=False,
1600+
show_progress=show_progress,
1601+
leave_progress_bar=leave_progress_bar,
1602+
response_handler=response_handler,
1603+
)
1604+
1605+
return response

viresclient/_data_handling.py

+27-22
Original file line numberDiff line numberDiff line change
@@ -216,11 +216,10 @@ def as_xarray_dataset(self, reshape=False):
216216
coords={"Timestamp":
217217
self._cdftime_to_datetime(self.get_variable("Timestamp"))})
218218
# Add Spacecraft variable as Categorical to save memory
219-
ds["Spacecraft"] = (("Timestamp",), pandas.Categorical(
220-
self.get_variable("Spacecraft"), categories=ALLOWED_SPACECRFTS))
221-
datanames = set(self.variables)
222-
datanames.remove("Timestamp")
223-
datanames.remove("Spacecraft")
219+
if "Spacecraft" in self.variables:
220+
ds["Spacecraft"] = (("Timestamp",), pandas.Categorical(
221+
self.get_variable("Spacecraft"), categories=ALLOWED_SPACECRFTS))
222+
datanames = set(self.variables) - {"Timestamp", "Spacecraft"}
224223
# Loop through each variable available and append them to the Dataset,
225224
# attaching the Timestamp coordinate to each.
226225
# Attach dimension names based on the name of the variable,
@@ -297,40 +296,45 @@ def reshape_dataset(ds):
297296
# Create integer "Site" identifier based on SiteCode / IAGA_code
298297
sites = dict(enumerate(sorted(set(ds[codevar].values))))
299298
sites_inv = {v: k for k, v in sites.items()}
300-
# Identify (V)OBS locations and mapping from integer "Site" identifier
301-
pos_vars = ["Longitude", "Latitude", "Radius", codevar]
302-
_ds_locs = next(iter(ds[pos_vars].groupby("Timestamp")))[1]
303-
if len(sites) > 1:
304-
_ds_locs = _ds_locs.drop(("Timestamp")).rename({"Timestamp": "Site"})
299+
if len(sites) == 0:
300+
_ds_locs = ds
305301
else:
306-
_ds_locs = _ds_locs.drop(("Timestamp")).expand_dims("Site")
307-
_ds_locs["Site"] = [sites_inv.get(code) for code in _ds_locs[codevar].values]
308-
_ds_locs = _ds_locs.sortby("Site")
302+
# Identify (V)OBS locations and mapping from integer "Site" identifier
303+
pos_vars = ["Longitude", "Latitude", "Radius", codevar]
304+
_ds_locs = next(iter(ds[pos_vars].groupby("Timestamp")))[1]
305+
if len(sites) > 1:
306+
_ds_locs = _ds_locs.drop(("Timestamp")).rename({"Timestamp": "Site"})
307+
else:
308+
_ds_locs = _ds_locs.drop(("Timestamp")).expand_dims("Site")
309+
_ds_locs["Site"] = [sites_inv.get(code) for code in _ds_locs[codevar].values]
310+
_ds_locs = _ds_locs.sortby("Site")
309311
# Create dataset initialised with the (V)OBS positional info as coords
310312
# and datavars (empty) reshaped to (Site, Timestamp, ...)
311313
t = numpy.unique(ds["Timestamp"])
312314
ds2 = xarray.Dataset(
313315
coords={
314316
"Timestamp": t,
315-
codevar: (("Site"), _ds_locs[codevar]),
316-
"Latitude": ("Site", _ds_locs["Latitude"]),
317-
"Longitude": ("Site", _ds_locs["Longitude"]),
318-
"Radius": ("Site", _ds_locs["Radius"]),
317+
codevar: (("Site"), _ds_locs[codevar].data),
318+
"Latitude": ("Site", _ds_locs["Latitude"].data),
319+
"Longitude": ("Site", _ds_locs["Longitude"].data),
320+
"Radius": ("Site", _ds_locs["Radius"].data),
319321
"NEC": ["N", "E", "C"]
320322
},
321323
)
322324
# (Dropping unused Spacecraft var)
323325
data_vars = set(ds.data_vars) - {"Latitude", "Longitude", "Radius", codevar, "Spacecraft"}
324326
N_sites = len(_ds_locs[codevar])
327+
# Create empty data variables to be infilled
325328
for var in data_vars:
326329
shape = [N_sites, len(t), *ds[var].shape[1:]]
327330
ds2[var] = ("Site", *ds[var].dims), numpy.empty(shape, dtype=ds[var].dtype)
328331
ds2[var][...] = None
329-
# Loop through each (V)OBS site to insert the datavars into ds2
330-
for k, _ds in dict(ds.groupby(codevar)).items():
331-
site = sites_inv.get(k)
332-
for var in data_vars:
333-
ds2[var][site, ...] = _ds[var].values
332+
# Loop through each (V)OBS site to infill the data
333+
if N_sites != 0:
334+
for k, _ds in dict(ds.groupby(codevar)).items():
335+
site = sites_inv.get(k)
336+
for var in data_vars:
337+
ds2[var][site, ...] = _ds[var].values
334338
# Revert to using only the "SiteCode"/"IAGA_code" identifier
335339
ds2 = ds2.set_index({"Site": codevar})
336340
ds2 = ds2.rename({"Site": codevar})
@@ -521,6 +525,7 @@ def as_xarray(self, group=None, reshape=False):
521525
ds = f.as_xarray_dataset(reshape=reshape)
522526
elif self.filetype == 'nc':
523527
ds = xarray.open_dataset(self._file.name, group=group)
528+
ds.attrs["Sources"] = self.sources
524529
return ds
525530

526531
@property

0 commit comments

Comments
 (0)