Temperature, Salinity, and Stratification

Now that we have learned the basic thermodynamics of seawater and understood how temperature and salinity combine to determine the stratifcation of water columns, we are ready to examine the global patterns of temperature, salinity and stratification. These patterns are fascinating and reveal much about the underlying ocean circulation. We will also learn how to identify distinct water masses both in geographic space and in temperature / salinity coordinates.

import xarray as xr
from matplotlib import pyplot as plt
import hvplot.xarray
import gsw
import numpy as np
import holoviews as hv
from IPython.display import Image
hv.extension('bokeh')
plt.rcParams['figure.figsize'] = (12,7)

Data Source: World Ocean Atlas

We will use data from the NOAA World Ocean Atlas, stored on the IRI Data Library at http://iridl.ldeo.columbia.edu/SOURCES/.NOAA/.NODC/.WOA09/.

World Ocean Atlas 2009 (WOA09) is a set of objectively analyzed (1° grid) climatological fields of in situ temperature, salinity, dissolved oxygen, Apparent Oxygen Utilization (AOU), percent oxygen saturation, phosphate, silicate, and nitrate at standard depth levels for annual, seasonal, and monthly compositing periods for the World Ocean. It also includes associated statistical fields of observed oceanographic profile data interpolated to standard depth levels on both 1° and 5° grids .

# basin masks
woa_mask = xr.open_dataset('http://iridl.ldeo.columbia.edu/SOURCES/.NOAA/.NODC/.WOA09/'
                             '.Masks/.basin/dods/').rename({'X':'lon', 'Y':'lat', 'Z': 'depth'})
basin_names = woa_mask.basin.attrs['CLIST'].split('\n')
basins_main = { n: basin_names[n-1] for n in [1,2,3,10] }
basins_marginal = { n: basin_names[n-1] for n in [4,5,6,7,11] }
woa_mask
<xarray.Dataset>
Dimensions:  (depth: 33, lat: 180, lon: 360)
Coordinates:
  * depth    (depth) float32 0.0 10.0 20.0 30.0 ... 4e+03 4.5e+03 5e+03 5.5e+03
  * lon      (lon) float32 0.5 1.5 2.5 3.5 4.5 ... 355.5 356.5 357.5 358.5 359.5
  * lat      (lat) float32 -89.5 -88.5 -87.5 -86.5 -85.5 ... 86.5 87.5 88.5 89.5
Data variables:
    basin    (depth, lat, lon) float32 ...
Attributes:
    Conventions:  IRIDL
# actual data
url_base = 'http://iridl.ldeo.columbia.edu/SOURCES/.NOAA/.NODC/.WOA09/.Grid-1x1/.Annual'
url_temp = url_base + '/.temperature/.t_an/dods'
url_salt = url_base + '/.salinity/.s_an/dods '
woa_temp = xr.open_dataset(url_temp)
woa_temp_nobs = xr.open_dataarray(url_base + '/.temperature/.t_dd/dods')
woa_salt = xr.open_dataset(url_salt)
woa = xr.merge([woa_temp, woa_salt, woa_temp_nobs, woa_mask]).isel(time=0)
woa.load()
<xarray.Dataset>
Dimensions:  (depth: 33, lat: 180, lon: 360)
Coordinates:
  * lon      (lon) float32 0.5 1.5 2.5 3.5 4.5 ... 355.5 356.5 357.5 358.5 359.5
  * depth    (depth) float32 0.0 10.0 20.0 30.0 ... 4e+03 4.5e+03 5e+03 5.5e+03
  * lat      (lat) float32 -89.5 -88.5 -87.5 -86.5 -85.5 ... 86.5 87.5 88.5 89.5
    time     datetime64[ns] 2008-01-01
Data variables:
    t_an     (depth, lat, lon) float32 nan nan nan nan nan ... nan nan nan nan
    s_an     (depth, lat, lon) float32 nan nan nan nan nan ... nan nan nan nan
    t_dd     (depth, lat, lon) float64 nan nan nan nan nan ... nan nan nan nan
    basin    (depth, lat, lon) float32 nan nan nan nan nan ... nan nan nan nan
Attributes:
    Conventions:  IRIDL

We will now augment the raw dataset with derived quantities, including pressure (calculated from depth), absolute salinity, conservative temperature, and potential density at various reference levels.

woa['pres'] = gsw.p_from_z(-woa.depth, 0)
woa['s_a'] = gsw.SA_from_SP(woa.s_an, woa.pres, woa.lon, woa.lat)
woa['c_t'] = gsw.CT_from_t(woa.s_a, woa.t_an, woa.pres)
woa['sigma_0'] = gsw.sigma0(woa.s_a, woa.c_t)
woa['sigma_2'] = gsw.sigma2(woa.s_a, woa.c_t)
woa['sigma_4'] = gsw.sigma4(woa.s_a, woa.c_t)
woa
<xarray.Dataset>
Dimensions:  (depth: 33, lat: 180, lon: 360)
Coordinates:
  * lon      (lon) float32 0.5 1.5 2.5 3.5 4.5 ... 355.5 356.5 357.5 358.5 359.5
  * depth    (depth) float32 0.0 10.0 20.0 30.0 ... 4e+03 4.5e+03 5e+03 5.5e+03
  * lat      (lat) float32 -89.5 -88.5 -87.5 -86.5 -85.5 ... 86.5 87.5 88.5 89.5
    time     datetime64[ns] 2008-01-01
Data variables:
    t_an     (depth, lat, lon) float32 nan nan nan nan nan ... nan nan nan nan
    s_an     (depth, lat, lon) float32 nan nan nan nan nan ... nan nan nan nan
    t_dd     (depth, lat, lon) float64 nan nan nan nan nan ... nan nan nan nan
    basin    (depth, lat, lon) float32 nan nan nan nan nan ... nan nan nan nan
    pres     (depth) float64 0.0 10.06 20.11 ... 4.573e+03 5.087e+03 5.602e+03
    s_a      (depth, lat, lon) float64 nan nan nan nan nan ... nan nan nan nan
    c_t      (depth, lat, lon) float64 nan nan nan nan nan ... nan nan nan nan
    sigma_0  (depth, lat, lon) float64 nan nan nan nan nan ... nan nan nan nan
    sigma_2  (depth, lat, lon) float64 nan nan nan nan nan ... nan nan nan nan
    sigma_4  (depth, lat, lon) float64 nan nan nan nan nan ... nan nan nan nan
Attributes:
    Conventions:  IRIDL

Before diving into the actual data, let’s first examine the number of observations in each grid cell. This shows how some parts of the ocean are much better sampled than others.

woa.t_dd.where(woa.t_dd > 0).hvplot('lon', 'lat', logz=True, width=700, height=400)

Later we will split up the ocean by basin. Here is how WOA defines the different basins.

woa.basin.where(woa.basin<20).hvplot('lon', 'lat', logz=True, width=700, height=400, cmap='Category20')
print(woa.basin.attrs['CLIST'])
Atlantic Ocean
Pacific Ocean 
Indian Ocean
Mediterranean Sea
Baltic Sea
Black Sea
Red Sea
Persian Gulf
Hudson Bay
Southern Ocean
Arctic Ocean
Sea of Japan
Kara Sea
Sulu Sea
Baffin Bay
East Mediterranean
West Mediterranean
Sea of Okhotsk
Banda Sea
Caribbean Sea
Andaman Basin
North Caribbean
Gulf of Mexico
Beaufort Sea
South China Sea
Barents Sea
Celebes Sea
Aleutian Basin
Fiji Basin
North American Basin
West European Basin
Southeast Indian Basin
Coral Sea
East Indian Basin
Central Indian Basin
Southwest Atlantic Basin
Southeast Atlantic Basin
Southeast Pacific Basin
Guatemala Basin
East Caroline Basin
Marianas Basin
Philippine Sea
Arabian Sea
Chile Basin
Somali Basin
Mascarene Basin
Crozet Basin
Guinea Basin
Brazil Basin
Argentine Basin
Tasman Sea
Atlantic Indian Basin
Caspian Sea
Sulu Sea II
Venezuela Basin
Bay of Bengal
Java Sea
East Indian Atlantic Basin

3D Temperature

def map_section_plot(data, label, data_range, levels, cmap='viridis'):
    ds = hv.Dataset(data)
    img = ds.to(hv.Image, ['lon', 'lat'], label=label)
    img = img.redim.range(**{data.name: data_range})

    posx = hv.streams.PointerX()
    vline = hv.DynamicMap(lambda x: hv.VLine(x or 180.5), streams=[posx])

    lats = np.arange(-90, 91)
    def cross_section(x):
        mesh = hv.QuadMesh((lats, woa.depth, data.sel(lon=x if x else 180, method='nearest').data[:-1]),
                           kdims=['lat', 'depth'], label=(label + ' Section'))
        contours = hv.operation.contours(mesh, levels=levels,
                                         overlaid=False, filled=True)
        return contours

    
    crosssection = hv.DynamicMap(cross_section, streams=[posx])
    layout = (img * vline) + crosssection 
    
    # set view options
    dict_spec = {'Image': {'plot': dict(colorbar=True, toolbar='above', width=600),
                           'style': dict(cmap=cmap)},
                 'NdOverlay': {'plot': dict(bgcolor='grey', invert_yaxis=True, width=600, height=200)},
                 'Layout': {'plot': dict(show_title=False)},
                 'Polygons': {'plot': dict(colorbar=True, invert_yaxis=True, width=600),
                              'style': dict(cmap=cmap, line_width=0.1)}
                 }
    layout.cols(1)
    layout = layout.opts(dict_spec)   
    return layout
map_section_plot(woa.t_an, 'Temperature', (-2,30), np.arange(-2,31), cmap='RdBu_r')

Observations

  • The warmest water is near the surface in the tropical regions, forming a bowl-shaped lens in the upper ~800 meters. This is called the thermocline.

  • There are clear zonal asymmetries in surface temperature. The western boundaries of the basins tend to be warmer, with colder water near the eastern boundaries.

  • There is something special going on at the equator, especially in the Pacific. It’s relatively cold, and the thermocline is shallow.

  • Below 1000 m, the water is mostly very cold (< 5 C).

  • In the polar regions, the coldest water is actually found near the surface! There is a mid-depth temperature maximum.

3D Salinity

map_section_plot(woa.s_an, 'Salinity', (32,38), np.arange(32,38.1, 0.25))

Observations

  • Salinity has a more complex structure than temperature.

  • The Atlantic is overall saltier than the Pacific.

  • The saltiest parts of the oceans are the extra-tropical-latitudes.

  • The equator is relatively fresh near the surface.

  • Salinity sections across the Atlantic reveal the interleaving of several different water masses including

    • Mediterranean Water

    • North Atlantic Deep Water

    • Antarctic Intermediate Water

    • Antarctic Bottom Water

3D Potential Density

map_section_plot(woa.sigma_0, 'Potential Density (sigma0)', (22.,28.), np.arange(22,28.1, 0.25))
map_section_plot(woa.sigma_4, 'Potential Density (sigma4)', (40.,46.5), np.arange(40,46.5, 0.25))

Observations

Potential density reflects the mechanical stratification of the ocean most closely of any of the quantities we have examined so far.

  • For the most part, potential density is monotonically increasing with depth.

  • BUT there are exceptions. Each potential density variable shows density “inversions” far from its reference level.

  • Potential density surface approximately represent pathways that water parcels can move along without experiencing a restoring force. Therefore, they serve as connections between the surface and deep ocean.

  • These connections are strongest in the Southern Ocean region.

Image('02_images/neutral_plane.png')
_images/02-c_ocean_temperature_salinity_stratification_23_0.png
woa['thick'] = woa.depth.diff(dim='depth')
woa['thick'][0] = 5
# add an area element
earth_radius =  6.371e6
tot_area = (4 * np.pi * earth_radius**2)
woa['area'] = earth_radius**2 * np.radians(1.) * np.radians(1.) * np.cos(np.radians(woa.lat))
woa['vol'] = woa.thick * woa.area
vol_full, _ = xr.broadcast(woa.vol, woa.lon)
fig, (ax1, ax2) = plt.subplots(2)

ax1.hist(woa.c_t.where(woa.c_t).values.ravel(), bins=100, density=True, range=(-2,30),
         weights=vol_full.where(woa.c_t).values.ravel());
ax2.hist(woa.s_a.where(woa.c_t).values.ravel(), bins=100, density=True, range=(32,38),
         weights=vol_full.where(woa.s_a).values.ravel());
ax1.set_xlabel(r'Temperature ($^\circ$C)')
ax2.set_xlabel("Salinity (g / kg)")
plt.tight_layout()
plt.close()

Temperature and Salinity Probability Distributions

fig
_images/02-c_ocean_temperature_salinity_stratification_27_0.png
from matplotlib.colors import LogNorm
fig, ax = plt.subplots()
hb = ax.hexbin(woa.s_an.values.ravel(), woa.c_t.values.ravel(),
           C=vol_full.values.ravel(), reduce_C_function=np.sum,
           extent=(30,40,-3,30), gridsize=50, bins='log',
           cmap='RdYlBu_r')
plt.colorbar(hb)
ax.set_ylabel(r'Temperature ($^\circ$C)')
ax.set_xlabel("Salinity (g / kg)")
plt.close()

Joint T/S Distribution

fig
_images/02-c_ocean_temperature_salinity_stratification_30_0.png
#pal = seaborn.palettes.color_palette(n_colors=len(basins_main))
fig, ax = plt.subplots()
for (bnum, bname) in basins_main.items():
    ax.plot(
        woa.s_an.where(woa.basin==bnum).values.ravel(),
        woa.c_t.where(woa.basin==bnum).values.ravel(),
        '.',  alpha=0.02, label=bname
    )
ax.set_xlim(30,38)
ax.legend(loc='upper left', frameon=True)
plt.close()
# blue green red purple

T/S Distribution by Basin

fig
_images/02-c_ocean_temperature_salinity_stratification_33_0.png

Thermohaline Circulation

Ref Ballarotta Et Al. 2014

THC

# the goods are here
# https://www.nodc.noaa.gov/OC5/3M_HEAT_CONTENT/
# http://data.nodc.noaa.gov/thredds/catalog/woa/heat_content/heat_content/catalog.html
url = ('https://data.nodc.noaa.gov/thredds/dodsC/woa/heat_content/'
       'heat_content/heat_content_anomaly_0-700_pentad.nc')
hc = xr.open_dataset(url, decode_times=False).load()
hc.time.attrs['calendar'] = '360_day'
hc = xr.decode_cf(hc)
hc
<xarray.Dataset>
Dimensions:             (depth: 1, lat: 180, lon: 360, nbounds: 2, time: 62)
Coordinates:
  * lat                 (lat) float32 -89.5 -88.5 -87.5 -86.5 ... 87.5 88.5 89.5
  * lon                 (lon) float32 -179.5 -178.5 -177.5 ... 177.5 178.5 179.5
  * time                (time) object 1957-07-01 00:00:00 ... 2018-07-01 00:0...
Dimensions without coordinates: depth, nbounds
Data variables: (12/31)
    crs                 int32 ...
    lat_bnds            (lat, nbounds) float32 ...
    lon_bnds            (lon, nbounds) float32 ...
    depth_bnds          (depth, nbounds) float32 ...
    climatology_bounds  (time, nbounds) float32 ...
    h18_hc              (time, depth, lat, lon) float32 ...
    ...                  ...
    pent_h22_se_IO      (time) float32 ...
    pent_h22_NI         (time) float32 ...
    pent_h22_se_NI      (time) float32 ...
    pent_h22_SI         (time) float32 ...
    pent_h22_se_SI      (time) float32 ...
    basin_mask          (lat, lon) float64 ...
Attributes: (12/45)
    Conventions:                     CF-1.6
    title:                           Ocean Heat Content anomalies from WOA09 ...
    summary:                         Mean ocean variable anomaly from in situ...
    references:                      Levitus, S., J. I. Antonov, T. P. Boyer,...
    institution:                     National Oceanographic Data Center(NODC)
    comment:                         
    ...                              ...
    publisher_name:                  US NATIONAL OCEANOGRAPHIC DATA CENTER
    publisher_url:                   http://www.nodc.noaa.gov/
    publisher_email:                 NODC.Services@noaa.gov
    license:                         These data are openly available to the p...
    Metadata_Conventions:            Unidata Dataset Discovery v1.0
    metadata_link:                   http://www.nodc.noaa.gov/OC5/3M_HEAT_CON...
fig, ax = plt.subplots()
hc['h18_hc'].sum(dim=('lon','lat','depth'), keep_attrs=True).plot(ax=ax, marker='o')
plt.title('Ocean Heat Content Anomaly')
plt.grid()
plt.close()

Ocean Warming

We have been looking at climatologies. But we can’t forget that the ocean is warming!

Data from https://www.nodc.noaa.gov/OC5/3M_HEAT_CONTENT/.

fig
_images/02-c_ocean_temperature_salinity_stratification_38_0.png