DNS (Thermospheric density)#

SERVER_URL = "https://vires.services/ows"
%load_ext watermark
%watermark -i -v -p viresclient,pandas,xarray,matplotlib,cartopy
Python implementation: CPython
Python version       : 3.11.6
IPython version      : 8.18.0

viresclient: 0.12.0
pandas     : 2.1.3
xarray     : 2023.12.0
matplotlib : 3.8.2
cartopy    : 0.22.0
import datetime as dt

import matplotlib.pyplot as plt
import matplotlib as mpl
import pandas as pd
from viresclient import SwarmRequest

Product information#

Thermospheric density products are available through the following collections, both from Swarm, and from other spacecraft (via the TOLEOS project)

We can check the available parameter names with:

request = SwarmRequest(SERVER_URL)
for collection in ("SW_OPER_DNSAACC_2_", "SW_OPER_DNSAPOD_2_", "CH_OPER_DNS_ACC_2_", "GR_OPER_DNS1ACC_2_", "GF_OPER_DNS1ACC_2_"):
    print(f"{collection}:\n{request.available_measurements(collection)}\n")
SW_OPER_DNSAACC_2_:
['Height_GD', 'Latitude_GD', 'Longitude_GD', 'Height_GD', 'density', 'local_solar_time']

SW_OPER_DNSAPOD_2_:
['Height_GD', 'Latitude_GD', 'Longitude_GD', 'Height_GD', 'local_solar_time', 'density', 'density_orbitmean', 'validity_flag']

CH_OPER_DNS_ACC_2_:
['Height_GD', 'Latitude_GD', 'Longitude_GD', 'density', 'density_orbitmean', 'local_solar_time', 'validity_flag', 'validity_flag_orbitmean']

GR_OPER_DNS1ACC_2_:
['Height_GD', 'Latitude_GD', 'Longitude_GD', 'density', 'density_orbitmean', 'local_solar_time', 'validity_flag', 'validity_flag_orbitmean']

GF_OPER_DNS1ACC_2_:
['Height_GD', 'Latitude_GD', 'Longitude_GD', 'density', 'density_orbitmean', 'local_solar_time', 'validity_flag', 'validity_flag_orbitmean']

We can check for available time windows with:

request.available_times("SW_OPER_DNSAPOD_2_")
starttime endtime bbox identifier
0 2013-11-28 00:00:00+00:00 2013-11-28 23:59:30.001000+00:00 (-90,-180,90,180) SW_OPER_DNSAPOD_2__20131128T000000_20131128T23...
1 2013-11-29 00:00:00+00:00 2013-11-29 23:59:30.001000+00:00 (-90,-180,90,180) SW_OPER_DNSAPOD_2__20131129T000000_20131129T23...
2 2013-11-30 00:00:00+00:00 2013-11-30 23:59:30.001000+00:00 (-90,-180,90,180) SW_OPER_DNSAPOD_2__20131130T000000_20131130T23...
3 2013-12-01 00:00:00+00:00 2013-12-01 23:59:30.001000+00:00 (-90,-180,90,180) SW_OPER_DNSAPOD_2__20131201T000000_20131201T23...
4 2013-12-02 00:00:00+00:00 2013-12-02 23:59:30.001000+00:00 (-90,-180,90,180) SW_OPER_DNSAPOD_2__20131202T000000_20131202T23...
... ... ... ... ...
3850 2024-07-27 00:00:00+00:00 2024-07-27 23:59:30.001000+00:00 (-90,-180,90,180) SW_OPER_DNSAPOD_2__20240727T000000_20240727T23...
3851 2024-07-28 00:00:00+00:00 2024-07-28 23:59:30.001000+00:00 (-90,-180,90,180) SW_OPER_DNSAPOD_2__20240728T000000_20240728T23...
3852 2024-07-29 00:00:00+00:00 2024-07-29 23:59:30.001000+00:00 (-90,-180,90,180) SW_OPER_DNSAPOD_2__20240729T000000_20240729T23...
3853 2024-07-30 00:00:00+00:00 2024-07-30 23:59:30.001000+00:00 (-90,-180,90,180) SW_OPER_DNSAPOD_2__20240730T000000_20240730T23...
3854 2024-07-31 00:00:00+00:00 2024-07-31 23:59:30.001000+00:00 (-90,-180,90,180) SW_OPER_DNSAPOD_2__20240731T000000_20240731T23...

3855 rows × 4 columns

request.available_times("GF_OPER_DNS1ACC_2_")
starttime endtime bbox identifier
0 2018-05-29 00:00:00+00:00 2018-05-29 23:59:50.001000+00:00 (-90,-180,90,180) GF_OPER_DNS1ACC_2__20180529T000000_20180529T23...
1 2018-05-30 00:00:00+00:00 2018-05-30 23:59:50.001000+00:00 (-90,-180,90,180) GF_OPER_DNS1ACC_2__20180530T000000_20180530T23...
2 2018-05-31 00:00:00+00:00 2018-05-31 23:59:50.001000+00:00 (-90,-180,90,180) GF_OPER_DNS1ACC_2__20180531T000000_20180531T23...
3 2018-06-01 00:00:00+00:00 2018-06-01 23:59:50.001000+00:00 (-90,-180,90,180) GF_OPER_DNS1ACC_2__20180601T000000_20180601T23...
4 2018-06-02 00:00:00+00:00 2018-06-02 23:59:50.001000+00:00 (-90,-180,90,180) GF_OPER_DNS1ACC_2__20180602T000000_20180602T23...
... ... ... ... ...
2183 2024-05-27 00:00:00+00:00 2024-05-27 23:59:50.001000+00:00 (-90,-180,90,180) GF_OPER_DNS1ACC_2__20240527T000000_20240527T23...
2184 2024-05-28 00:00:00+00:00 2024-05-28 23:59:50.001000+00:00 (-90,-180,90,180) GF_OPER_DNS1ACC_2__20240528T000000_20240528T23...
2185 2024-05-29 00:00:00+00:00 2024-05-29 23:59:50.001000+00:00 (-90,-180,90,180) GF_OPER_DNS1ACC_2__20240529T000000_20240529T23...
2186 2024-05-30 00:00:00+00:00 2024-05-30 23:59:50.001000+00:00 (-90,-180,90,180) GF_OPER_DNS1ACC_2__20240530T000000_20240530T23...
2187 2024-05-31 00:00:00+00:00 2024-05-31 23:59:50.001000+00:00 (-90,-180,90,180) GF_OPER_DNS1ACC_2__20240531T000000_20240531T23...

2188 rows × 4 columns

Fetching neutral density#

request = SwarmRequest(SERVER_URL)
request.set_collection(f"SW_OPER_DNSAACC_2_")
request.set_products(
    measurements=["density"],
)
data = request.get_between(dt.datetime(2015, 1, 1), dt.datetime(2015, 1, 2))
data.as_xarray()
<xarray.Dataset>
Dimensions:     (Timestamp: 8640)
Coordinates:
  * Timestamp   (Timestamp) datetime64[ns] 2015-01-01 ... 2015-01-01T23:59:50
Data variables:
    Spacecraft  (Timestamp) object 'A' 'A' 'A' 'A' 'A' ... 'A' 'A' 'A' 'A' 'A'
    density     (Timestamp) float64 1.165e-12 1.174e-12 ... 9.99e+32 9.99e+32
    Longitude   (Timestamp) float64 -147.2 -147.1 -147.1 ... -136.5 -136.2
    Latitude    (Timestamp) float64 51.72 51.08 50.44 ... -72.17 -72.8 -73.43
    Radius      (Timestamp) float64 6.83e+06 6.83e+06 ... 6.844e+06 6.844e+06
Attributes:
    Sources:         ['SW_OPER_DNSAACC_2__20150101T000000_20150101T235950_0201']
    MagneticModels:  []
    AppliedFilters:  []

Fetching from multiple spacecraft#

We will fetch the data from around the geomagnetic storm event that affected a SpaceX Starlink launch and subsequent loss of spacecraft. See for example:
Starlink Satellite Losses During the February 2022 Geomagnetic Storm Event: Science, Technical and Economic Consequences, Policy, and Mitigation

def fetch_density(mission="SW", spacecraft="A", source="ACC", start_time=None, end_time=None):
    request = SwarmRequest(SERVER_URL)
    request.set_collection(f"{mission}_OPER_DNS{spacecraft}{source}_2_")
    request.set_products(
        measurements=["density"],
        auxiliaries=["QDLat", "OrbitNumber", "OrbitDirection", "QDOrbitDirection"],
    )
    data = request.get_between(start_time, end_time, asynchronous=False, show_progress=False)
    return data.as_xarray()


start_time = dt.datetime(2022, 2, 1)
end_time = dt.datetime(2022, 2, 8)
# ds_SwA = fetch_density(mission="SW", spacecraft="A", source="POD", start_time=start_time, end_time=end_time)
# ds_SwB = fetch_density(mission="SW", spacecraft="B", source="POD", start_time=start_time, end_time=end_time)
ds_SwC = fetch_density(mission="SW", spacecraft="C", source="POD", start_time=start_time, end_time=end_time)
ds_GF1 = fetch_density(mission="GF", spacecraft="1", source="ACC", start_time=start_time, end_time=end_time)

Now we have the data available from several datasets, e.g.:

# Use Swarm-C dataset, extract one day, and plot density
ds_SwC.sel(Timestamp=slice("2022-02-02", "2022-02-02"))["density"].plot.line(figsize=(10, 2));
../_images/1cf201cb91e395b067af1eb8f4fc7c575bc27616eabd66260bfbf2bef2b0f85c.png

You can check which source products have been accessed:

ds_SwC.attrs["Sources"]
['SW_OPER_AUXCORBCNT_20131122T132146_20240921T233031_0001',
 'SW_OPER_DNSCPOD_2__20220201T000000_20220201T235930_0201',
 'SW_OPER_DNSCPOD_2__20220202T000000_20220202T235930_0201',
 'SW_OPER_DNSCPOD_2__20220203T000000_20220203T235930_0201',
 'SW_OPER_DNSCPOD_2__20220204T000000_20220204T235930_0201',
 'SW_OPER_DNSCPOD_2__20220205T000000_20220205T235930_0201',
 'SW_OPER_DNSCPOD_2__20220206T000000_20220206T235930_0201',
 'SW_OPER_DNSCPOD_2__20220207T000000_20220207T235930_0201',
 'SW_OPER_MODC_SC_1B_20220131T000000_20220131T235959_0504',
 'SW_OPER_MODC_SC_1B_20220201T000000_20220201T235959_0504',
 'SW_OPER_MODC_SC_1B_20220202T000000_20220202T235959_0504',
 'SW_OPER_MODC_SC_1B_20220203T000000_20220203T235959_0504',
 'SW_OPER_MODC_SC_1B_20220204T000000_20220204T235959_0504',
 'SW_OPER_MODC_SC_1B_20220205T000000_20220205T235959_0504',
 'SW_OPER_MODC_SC_1B_20220206T000000_20220206T235959_0504',
 'SW_OPER_MODC_SC_1B_20220207T000000_20220207T235959_0504']

Visualisation example#

Let’s try to display the density data as a function of time and latitude…

ds_GF1.plot.scatter(x="Timestamp", y="Latitude", hue="density", s=2, edgecolors="face", figsize=(10, 4));
../_images/c63e256f5691436804bb95eb9c3ce38b20bfcb247215e86c23ae4dd4b1dc9fd6.png

Now let’s explore that a bit more to have more control over the figure

def select_north(ds):
    return ds.where(ds["Latitude"] > 0, drop=True)

def select_south(ds):
    return ds.where(ds["Latitude"] < 0, drop=True)

def select_ascending(ds):
    return ds.where(ds["OrbitDirection"] == 1, drop=True)

def select_descending(ds):
    return ds.where(ds["OrbitDirection"] == -1, drop=True)

def get_times_at_orbits(ds, orbitnos: list):
    da = ds["OrbitNumber"]
    t = []
    for orbitno in orbitnos:
        try:
            _t = da.where(da == orbitno, drop=True).isel({"Timestamp": 0})["Timestamp"].values.astype("datetime64[s]").astype(dt.datetime)
        except IndexError:
            _t = None
        t.append(_t)
    return t

def get_orbits_at_times(ds, times: list):
    da = ds["OrbitNumber"]
    return list(da.sel(Timestamp=times, method="nearest").values.astype(int))
norm_gf =  mpl.colors.Normalize(vmin=0, vmax=7.5e-13)
norm_sw = mpl.colors.Normalize(vmin=0, vmax=20e-13)

def plot_density(ds=ds_GF1, norm=norm_gf, title="GRACE-FO 1"):
    
    cmap = "viridis"
    plot_kwargs = {"norm": norm, "edgecolors": None, "s": 2, "marker": "s", "cmap": cmap}
    
    fig, axes = plt.subplots(2, 1, figsize=(10, 4), sharex=True)
    # Plot the four segments of each orbit in sequence
    # North
    _ds = select_ascending(select_north(ds))
    axes[0].scatter(x=_ds["OrbitNumber"], y=_ds["Latitude"], c=_ds["density"], **plot_kwargs)
    _ds = select_descending(select_north(ds))
    axes[0].scatter(x=_ds["OrbitNumber"]+.5, y=_ds["Latitude"], c=_ds["density"], **plot_kwargs)
    # South
    _ds = select_descending(select_south(ds))
    axes[1].scatter(x=_ds["OrbitNumber"]+0.5, y=_ds["Latitude"], c=_ds["density"], **plot_kwargs)
    _ds = select_ascending(select_south(ds))
    axes[1].scatter(x=_ds["OrbitNumber"]+1.0, y=_ds["Latitude"], c=_ds["density"], **plot_kwargs)
    # Adjust axes
    axes[0].set_ylim(0, 90)
    axes[1].set_ylim(-90, 0)
    fig.subplots_adjust(hspace=0)
    axes[1].set_ylabel("Latitude")
    # axes[0].set_backgr
    fig.suptitle(title)
    # Move xticks to align with day starts
    t_start_end = ds["Timestamp"].isel(Timestamp=[0, -1]).values.astype("datetime64[s]").astype(dt.datetime)
    times_to_mark = pd.date_range(start=t_start_end[0], end=t_start_end[1], freq="D")
    orbits_to_mark = get_orbits_at_times(ds, times_to_mark)
    axes[1].set_xticks(orbits_to_mark)
    # Replace orbit numbers with the times
    times_formatted = [_t.strftime("%Y-%m-%d") for _t in times_to_mark]
    axes[1].set_xticklabels(times_formatted)
    # Add colorbar
    cbar_ax = fig.add_axes([0.95, 0.3, 0.01, 0.4])
    sm = mpl.cm.ScalarMappable(cmap=plot_kwargs["cmap"], norm=plot_kwargs["norm"])
    sm.set_array([])
    cb = fig.colorbar(sm, cax=cbar_ax)
    cb.set_label("density [kg m$^{-3}$]")
plot_density(ds_GF1, norm_gf, "GRACE-FO 1")
../_images/f10c4dc1dcc8cacf8f68c580f9adb8741bc8073ccbe76133dc0b2e7bac3b4941.png
plot_density(ds_SwC, norm_sw, "Swarm C")
../_images/ab7e4d6638afabc961ab5db032425ae6ace028232965612efa7c499ab801383d.png