Plotting Glider data with Python tools

In this notebook we demonstrate how to obtain and plot glider data using iris and cartopy. We will explore data from the Rutgers University RU29 Challenger glider that was launched from Ubatuba, Brazil on June 23, 2015 to travel across the Atlantic Ocean. After 282 days at sea, the Challenger was picked up off the coast of South Africa, on March 31, 2016. For more information on this ground breaking excusion see: https://marine.rutgers.edu/main/announcements/the-challenger-glider-mission-south-atlantic-mission-complete

Data collected from this glider mission are available on the IOOS Glider DAC THREDDS via OPeNDAP.

url = (
    "https://data.ioos.us/thredds/dodsC/deployments/rutgers/"
    "ru29-20150623T1046/ru29-20150623T1046.nc3.nc"
)
import iris


glider = iris.load_raw(url)
print(glider)
0: longitude / (degrees)               (-- : 1; -- : 542; -- : 483)
1: sea_water_electrical_conductivity / (S m-1) (-- : 1; -- : 542; -- : 483)
2: longitude status_flag / (no_unit)   (-- : 1; -- : 542; -- : 483)
3: eastward_sea_water_velocity / (m s-1) (-- : 1; -- : 542)
4: time / (seconds since 1970-01-01T00:00:00Z) (-- : 1; -- : 542; -- : 483)
5: sea_water_pressure / (dbar)         (-- : 1; -- : 542; -- : 483)
6: latitude / (degrees)                (-- : 1; -- : 542; -- : 483)
7: longitude status_flag / (no_unit)   (-- : 1; -- : 542; -- : 483)
8: sea_water_salinity / (1e-3)         (-- : 1; -- : 542; -- : 483)
9: latitude status_flag / (no_unit)    (-- : 1; -- : 542; -- : 483)
10: Platform Metadata / (1)             (-- : 1; -- : 542; -- : 483)
11: time status_flag / (no_unit)        (-- : 1; -- : 542; -- : 483)
12: time status_flag / (no_unit)        (-- : 1; -- : 542; -- : 483)
13: precise_lon Variable Quality Flag / (no_unit) (-- : 1; -- : 542; -- : 483)
14: northward_sea_water_velocity status_flag / (no_unit) (-- : 1; -- : 542; -- : 483)
15: precise_lat Variable Quality Flag / (no_unit) (-- : 1; -- : 542; -- : 483)
16: sea_water_density / (kg m-3)        (-- : 1; -- : 542; -- : 483)
17: latitude status_flag / (no_unit)    (-- : 1; -- : 542; -- : 483)
18: northward_sea_water_velocity / (m s-1) (-- : 1; -- : 542)
19: Trajectory Name / (unknown)         (-- : 1; -- : 64)
20: CTD Metadata / (1)                  (-- : 1; -- : 542; -- : 483)
21: WMO ID / (unknown)                  (-- : 1; -- : 64)
22: Profile ID / (unknown)              (-- : 1; -- : 542)
23: sea_water_temperature / (Celsius)   (-- : 1; -- : 542; -- : 483)
24: eastward_sea_water_velocity status_flag / (no_unit) (-- : 1; -- : 542; -- : 483)

Iris requires the data to adhere strictly to the CF-1.6 data model. That is why we see all those warnings about Missing CF-netCDF ancillary data variable. Note that if the data is not CF at all iris will refuse to load it!

The other hand, the advantage of following the CF-1.6 conventions, is that the iris cube has the proper metadata is attached it. We do not need to extract the coordinates or any other information separately . All we need to do is to request the phenomena we want, in this case sea_water_density, sea_water_temperature and sea_water_salinity.

temp = glider.extract_cube("sea_water_temperature")
salt = glider.extract_cube("sea_water_salinity")
dens = glider.extract_cube("sea_water_density")

print(temp)
sea_water_temperature / (Celsius)         (-- : 1; -- : 542; -- : 483)
    Auxiliary coordinates:
        latitude                              x       x         -
        longitude                             x       x         -
        time                                  x       x         -
        depth                                 x       x         x
    Ancillary variables:
        sea_water_temperature status_flag     x       x         x
    Attributes:
        Conventions                       Unidata Dataset Discovery v1.0, COARDS, CF-1.6
        DODS.dimName                      wmo_id_strlen
        DODS.strlen                       7
        Easternmost_Easting               13.591759500847711
        Metadata_Conventions              Unidata Dataset Discovery v1.0, COARDS, CF-1.6
        Northernmost_Northing             -25.492669785275247
        Southernmost_Northing             -37.340890399992446
        Westernmost_Easting               -44.92195338434748
        _ChunkSizes                       1
        acknowledgment                    This deployment supported by funding from the G.  Unger Vetelsen Foundation...
        actual_range                      array([ 3.744 , 24.5387], dtype=float32)
        cdm_data_type                     TrajectoryProfile
        cdm_profile_variables             time_uv,lat_uv,lon_uv,u,v,profile_id,time,latitude,longitude
        cdm_trajectory_variables          trajectory,wmo_id
        colorBarMaximum                   32.0
        colorBarMinimum                   0.0
        comment                           Glider operatored by the Rutgers University Coastal Ocean Observation Lab,...
        contributor_name                  Scott Glenn, Oscar Schofield, Josh Kohut, Antonio Ramos, Sebastian Swart,...
        contributor_role                  Principal Investigator, Principal Investigator, Principal Investigator,...
        creator_email                     kerfoot@marine.rutgers.edu
        creator_name                      John Kerfoot
        creator_url                       http://rucool.marine.rutgers.edu
        date_created                      2016-03-31T06:16:37Z
        date_issued                       2016-03-31T06:16:37Z
        featureType                       TrajectoryProfile
        format_version                    IOOS_Glider_NetCDF_v2.0.nc
        geospatial_lat_max                -25.492669785275247
        geospatial_lat_min                -37.340890399992446
        geospatial_lat_units              degrees_north
        geospatial_lon_max                13.591759500847711
        geospatial_lon_min                -44.92195338434748
        geospatial_lon_units              degrees_east
        geospatial_vertical_max           983.17
        geospatial_vertical_min           0.61
        geospatial_vertical_positive      down
        geospatial_vertical_units         m
        gts_ingest                        true
        history                           '2016-03-31T06:16:37Z /home/kerfoot/slocum/matlab/spt/export/nc/IOOS/DAC/writeIoosGliderFlatNc.m\n2021-10-15T13:26:31Z...
        id                                ru29-20160331T0855
        infoUrl                           https://gliders.ioos.us/erddap/
        institution                       Rutgers University
        instrument                        instrument_ctd
        ioos_category                     Temperature
        ioos_dac_checksum                 fe452cc3a1bd121d6ba03cd41c4c004c
        ioos_dac_completed                True
        keywords                          AUVS > Autonomous Underwater Vehicles, Earth Science > Oceans > Ocean Pressure...
        keywords_vocabulary               GCMD Science Keywords
        license                           This data may be redistributed and used without restriction.  Data provided...
        naming_authority                  edu.rutgers.marine
        observation_type                  measured
        platform                          platform
        platform_type                     Slocum Glider
        processing_level                  Timestamp and gps positions checked for validity.
        project                           Challenger
        publisher_email                   kerfoot@marine.rutgers.edu
        publisher_name                    John Kerfoot
        publisher_url                     http://rucool.marine.rutgers.edu
        sea_name                          South Atlantic Ocean
        source                            Observational data from a profiling glider
        sourceUrl                         (local files)
        standard_name_vocabulary          CF-v25
        subsetVariables                   trajectory,wmo_id,time_uv,lat_uv,lon_uv,u,v,profile_id,time,latitude,l...
        summary                           "Third leg of the ru29 Challenger mission from Brazil to\n            South...
        time_coverage_end                 2016-03-31T09:25:31Z
        time_coverage_start               2015-06-23T10:57:59Z
        title                             ru29-20150623T1046
        valid_max                         40.0
        valid_min                         -5.0

Glider data is not something trivial to visualize. The very first thing to do is to plot the glider track to check its path.

import numpy.ma as ma

T = temp.data.squeeze()
S = salt.data.squeeze()
D = dens.data.squeeze()

x = temp.coord(axis="X").points.squeeze()
y = temp.coord(axis="Y").points.squeeze()
z = temp.coord(axis="Z")
t = temp.coord(axis="T")

vmin, vmax = z.attributes["actual_range"]

z = ma.masked_outside(z.points.squeeze(), vmin, vmax)
t = t.units.num2date(t.points.squeeze())

location = y.mean(), x.mean()  # Track center.
locations = list(zip(y, x))  # Track points.
import folium

tiles = (
    "http://services.arcgisonline.com/arcgis/rest/services/"
    "World_Topo_Map/MapServer/MapServer/tile/{z}/{y}/{x}"
)

m = folium.Map(location, tiles=tiles, attr="ESRI", zoom_start=4)

folium.CircleMarker(locations[0], fill_color="green", radius=10).add_to(m)
folium.CircleMarker(locations[-1], fill_color="red", radius=10).add_to(m)

line = folium.PolyLine(
    locations=locations,
    color="orange",
    weight=8,
    opacity=0.6,
    popup="Slocum Glider ru29 Deployed on 2015-06-23",
).add_to(m)

m
Make this Notebook Trusted to load map: File -> Trust Notebook

One might be interested in a the individual profiles of each dive. Lets extract the deepest dive and plot it.

import numpy as np

# Find the deepest profile.
idx = np.nonzero(~T[:, -1].mask)[0][0]
%matplotlib inline
import matplotlib.pyplot as plt

ncols = 3
fig, (ax0, ax1, ax2) = plt.subplots(
    sharey=True, sharex=False, ncols=ncols, figsize=(3.25 * ncols, 5)
)

kw = dict(linewidth=2, color="cornflowerblue", marker=".")
ax0.plot(T[idx], z[idx], **kw)
ax1.plot(S[idx], z[idx], **kw)
ax2.plot(D[idx] - 1000, z[idx], **kw)


def spines(ax):
    ax.spines["right"].set_color("none")
    ax.spines["bottom"].set_color("none")
    ax.xaxis.set_ticks_position("top")
    ax.yaxis.set_ticks_position("left")


[spines(ax) for ax in (ax0, ax1, ax2)]

ax0.set_ylabel("Depth (m)")
ax0.set_xlabel("Temperature ({})".format(temp.units))
ax0.xaxis.set_label_position("top")

ax1.set_xlabel("Salinity ({})".format(salt.units))
ax1.xaxis.set_label_position("top")

ax2.set_xlabel("Density ({})".format(dens.units))
ax2.xaxis.set_label_position("top")

ax0.invert_yaxis()
../../../_images/2016-11-15-glider_data_example_10_0.png

We can also visualize the whole track as a cross-section.

import numpy as np
import seawater as sw
from mpl_toolkits.axes_grid1.inset_locator import inset_axes


def distance(x, y, units="km"):
    dist, pha = sw.dist(x, y, units=units)
    return np.r_[0, np.cumsum(dist)]


def plot_glider(
    x, y, z, t, data, cmap=plt.cm.viridis, figsize=(9, 3.75), track_inset=False
):

    fig, ax = plt.subplots(figsize=figsize)
    dist = distance(x, y, units="km")
    z = np.abs(z)
    dist, z = np.broadcast_arrays(dist[..., np.newaxis], z)
    cs = ax.pcolor(dist, z, data, cmap=cmap, snap=True)
    kw = dict(orientation="vertical", extend="both", shrink=0.65)
    cbar = fig.colorbar(cs, **kw)

    if track_inset:
        axin = inset_axes(ax, width="25%", height="30%", loc=4)
        axin.plot(x, y, "k.")
        start, end = (x[0], y[0]), (x[-1], y[-1])
        kw = dict(marker="o", linestyle="none")
        axin.plot(*start, color="g", **kw)
        axin.plot(*end, color="r", **kw)
        axin.axis("off")

    ax.invert_yaxis()
    ax.set_xlabel("Distance (km)")
    ax.set_ylabel("Depth (m)")
    return fig, ax, cbar
from palettable import cmocean

haline = cmocean.sequential.Haline_20.mpl_colormap
thermal = cmocean.sequential.Thermal_20.mpl_colormap
dense = cmocean.sequential.Dense_20.mpl_colormap


fig, ax, cbar = plot_glider(x, y, z, t, S, cmap=haline, track_inset=False)
cbar.ax.set_xlabel("(g kg$^{-1}$)")
cbar.ax.xaxis.set_label_position("top")
ax.set_title("Salinity")

fig, ax, cbar = plot_glider(x, y, z, t, T, cmap=thermal, track_inset=False)
cbar.ax.set_xlabel(r"($^\circ$C)")
cbar.ax.xaxis.set_label_position("top")
ax.set_title("Temperature")

fig, ax, cbar = plot_glider(x, y, z, t, D - 1000, cmap=dense, track_inset=False)
cbar.ax.set_xlabel(r"(kg m$^{-3}$C)")
cbar.ax.xaxis.set_label_position("top")
ax.set_title("Density")

print("Data collected from {} to {}".format(t[0], t[-1]))
/tmp/ipykernel_19098/3856466220.py:19: MatplotlibDeprecationWarning: shading='flat' when X and Y have the same dimensions as C is deprecated since 3.3.  Either specify the corners of the quadrilaterals with X and Y, or pass shading='auto', 'nearest' or 'gouraud', or set rcParams['pcolor.shading'].  This will become an error two minor releases later.
  cs = ax.pcolor(dist, z, data, cmap=cmap, snap=True)
Data collected from 2015-06-23 10:57:59 to 2016-03-31 09:25:31
../../../_images/2016-11-15-glider_data_example_13_2.png ../../../_images/2016-11-15-glider_data_example_13_3.png ../../../_images/2016-11-15-glider_data_example_13_4.png

Glider cross-section also very be useful but we need to be careful when interpreting those due to the many turns the glider took, and the time it took to complete the track.

Note that the x-axis can be either time or distance. Note that this particular track took ~281 days to complete!

For those interested into more fancy ways to plot glider data check @lukecampbell’s profile_plots.py script.