Open In Colab   Open in Kaggle

Tutorial 5: Thermohaline Circulation#

Week 1, Day 2: Ocean and Atmospheric Reanalysis

Content creators: Aurora Basinski

Content reviewers: Katrina Dobson, Danika Gupta, Maria Gonzalez, Will Gregory, Nahid Hasan, Paul Heubel, Sherry Mi, Beatriz Cosenza Muralles, Jenna Pearson, Chi Zhang, Ohad Zivan

Content editors: Paul Heubel, Jenna Pearson, Chi Zhang, Ohad Zivan

Production editors: Wesley Banfield, Paul Heubel, Jenna Pearson, Konstantine Tsafatinos, Chi Zhang, Ohad Zivan

Our 2024 Sponsors: NFDI4Earth, CMIP

Tutorial Objectives#

Estimated timing of tutorial: 30 mins

In the previous tutorial, we discussed how the surface ocean’s movement is driven by wind forcing. However, the ocean can also experience movement due to density differences. The large-scale ocean movement driven by these density differences is known as the thermohaline circulation. The density of ocean water is influenced by temperature (thermo) and salinity (haline), and fluid motion occurs in response to pressure gradients caused by these density variations.

In this tutorial, we will use ocean surface data for 2014 to 2016 from the Estimating the Circulation and Climate of the Ocean (ECCO) reanalysis dataset to

  • Plot sea surface salinity and temperature,

  • Understand the relationship between salinity, temperature, and ocean density,

  • Explore the difference between linear and non-linear equations of state.

Setup#

# installations ( uncomment and run this cell ONLY when using google colab or kaggle )

# !pip install seaborn
# !pip install cmocean
# !pip install cartopy
# !pip install gsw
#import numpy as np
import xarray as xr
import matplotlib.pyplot as plt
import seaborn as sns
from cartopy import crs as ccrs, feature as cfeature
from cartopy.mpl.ticker import LongitudeFormatter, LatitudeFormatter
import cmocean as cmo
import pooch
import os
import tempfile
import gsw

Helper functions#

Hide code cell source
# @title Helper functions

def pooch_load(filelocation=None, filename=None, processor=None):
    shared_location = "/home/jovyan/shared/Data/tutorials/W1D2_StateoftheClimateOceanandAtmosphereReanalysis"  # this is different for each day
    user_temp_cache = tempfile.gettempdir()

    if os.path.exists(os.path.join(shared_location, filename)):
        file = os.path.join(shared_location, filename)
    else:
        file = pooch.retrieve(
            filelocation,
            known_hash=None,
            fname=os.path.join(user_temp_cache, filename),
            processor=processor,
        )

    return file

Figure Settings#

Hide code cell source
# @title Figure Settings
import ipywidgets as widgets  # interactive display

%config InlineBackend.figure_format = 'retina'
plt.style.use(
    "https://raw.githubusercontent.com/neuromatch/climate-course-content/main/cma.mplstyle"
)

Figure Settings#

Hide code cell source
# @title Figure Settings
import ipywidgets as widgets  # interactive display

%config InlineBackend.figure_format = 'retina'
plt.style.use(
    "https://raw.githubusercontent.com/neuromatch/climate-course-content/main/cma.mplstyle"
)

Video 1: Thermohaline Circulation#

Section 1: Plot Surface Temperature and Salinity#

In the slides, we discovered that ocean flows can be driven by density variations in addition to wind-driven circulation. One example of a density-driven flow is the thermohaline circulation. Density in the ocean is influenced by two main factors:

  1. Salinity (higher salinity leads to greater density) and

  2. Temperature (lower temperature generally results in higher density),

  3. Also, pressure affects density (higher pressure results in higher density), but it generally has a much smaller impact on ocean density than temperature and salinity.

To develop a better understanding of how density varies across different regions, let’s examine the average salinity and temperature at the ocean surface.

First let’s load the data.

# import preprocessed ecco data. This is surface data of monthly resolution over the period 2014 to 2016.
# load potential temperature theta
filename_theta = "surface_theta.nc"
url_theta = "https://osf.io/98ksr/download"

subset_theta = xr.open_dataset(pooch_load(url_theta, filename_theta))
subset_theta
Downloading data from 'https://osf.io/98ksr/download' to file '/tmp/surface_theta.nc'.
SHA256 hash of downloaded file: 2d8db6d497acec50c4d0753ba69c610231dc02294ab1dc99640e0fce0285d369
Use this value as the 'known_hash' argument of 'pooch.retrieve' to ensure that the file hasn't changed if it is downloaded again in the future.
<xarray.Dataset> Size: 2MB
Dimensions:    (longitude: 720, latitude: 360)
Coordinates:
    i          (longitude) int64 6kB ...
    k          int64 8B ...
    j          (latitude) int64 3kB ...
  * latitude   (latitude) float64 3kB -89.75 -89.25 -88.75 ... 88.75 89.25 89.75
  * longitude  (longitude) float64 6kB -179.8 -179.2 -178.8 ... 179.2 179.8
    Z          float32 4B ...
Data variables:
    THETA      (latitude, longitude) float64 2MB ...
# load salinity
filename_salt = "surface_salt.nc"
url_salt = "https://osf.io/aufs2/download"

subset_salt = xr.open_dataset(pooch_load(url_salt, filename_salt))
subset_salt
Downloading data from 'https://osf.io/aufs2/download' to file '/tmp/surface_salt.nc'.
SHA256 hash of downloaded file: 1eb3c4bd72131220a732692fdd8a20ae52bbc1e7055b41742d399d4fe4bb8129
Use this value as the 'known_hash' argument of 'pooch.retrieve' to ensure that the file hasn't changed if it is downloaded again in the future.
<xarray.Dataset> Size: 2MB
Dimensions:    (longitude: 720, latitude: 360)
Coordinates:
    i          (longitude) int64 6kB ...
    k          int64 8B ...
    j          (latitude) int64 3kB ...
  * latitude   (latitude) float64 3kB -89.75 -89.25 -88.75 ... 88.75 89.25 89.75
  * longitude  (longitude) float64 6kB -179.8 -179.2 -178.8 ... 179.2 179.8
    Z          float32 4B ...
Data variables:
    SALT       (latitude, longitude) float64 2MB ...
# make land points NaN (not a number)
subset_theta = subset_theta.where(
    subset_theta != 0
)  # change anywhere that the value is zero to NaN
subset_salt = subset_salt.where(subset_salt != 0)  # same
subset_theta = subset_theta.THETA  # choose the variable to remove the dimension
subset_salt = subset_salt.SALT
# plot Sea Surface Temprature - similar to plots we used in tutorials 2+3
fig, ax = plt.subplots(
    subplot_kw={"projection": ccrs.PlateCarree()}, dpi=100
)  # this is from cartopy https://rabernat.github.io/research_computing_2018/maps-with-cartopy.html
p = subset_theta.plot(
    vmin=0,
    cmap=cmo.cm.thermal,
    cbar_kwargs={
        "shrink": 0.75,
        "orientation": "horizontal",
        "extend": "both",
        "pad": 0.185,
        "label": "°C",
    },
    ax=ax,
)
ax.coastlines(color="grey", lw=0.5)
ax.set_xticks([-180, -120, -60, 0, 60, 120, 180], crs=ccrs.PlateCarree())
ax.set_yticks([-90, -60, -30, 0, 30, 60, 90], crs=ccrs.PlateCarree())
lon_formatter = LongitudeFormatter(zero_direction_label=True)
lat_formatter = LatitudeFormatter()
ax.add_feature(cfeature.LAND, zorder=100, edgecolor="k")
ax.set_title("Sea Surface Temperature (2014 – 2016 mean)")
Text(0.5, 1.0, 'Sea Surface Temperature (2014 – 2016 mean)')
/opt/hostedtoolcache/Python/3.9.19/x64/lib/python3.9/site-packages/cartopy/io/__init__.py:241: DownloadWarning: Downloading: https://naturalearth.s3.amazonaws.com/110m_physical/ne_110m_land.zip
  warnings.warn(f'Downloading: {url}', DownloadWarning)
../../../_images/323a8090bdac658c344b8c1a8f34aab296c48ac4d1443b03a45d243652c650b9.png
Click here for a description of the plot Sea surface temperature on average decreases with increasing latitude, so the tropics show temperatures of about $30°\text{C}$. Along western coastlines, it is additionally decreased due to the upwelling of low-temperature water masses caused by Ekman pumping (see also previous Tutorial 4). Water masses in polar regions are below $0°\text{C}$ as the salinity decreases the freezing point.
# plot Sea Surface Salinity
fig, ax = plt.subplots(
    subplot_kw={"projection": ccrs.PlateCarree()}, dpi=100
)  # this is from cartopy https://rabernat.github.io/research_computing_2018/maps-with-cartopy.html
p = subset_salt.plot(
    cmap=cmo.cm.haline,
    robust=True,
    #vmin=30,
    cbar_kwargs={
        "shrink": 0.75,
        "orientation": "horizontal",
        "extend": "both",
        "pad": 0.175,
        "label": "psu",
    },
    ax=ax,
)
ax.coastlines(color="grey", lw=0.5)
ax.set_xticks([-180, -120, -60, 0, 60, 120, 180], crs=ccrs.PlateCarree())
ax.set_yticks([-90, -60, -30, 0, 30, 60, 90], crs=ccrs.PlateCarree())
lon_formatter = LongitudeFormatter(zero_direction_label=True)
lat_formatter = LatitudeFormatter()
ax.add_feature(cfeature.LAND, zorder=100, edgecolor="k")
ax.set_title("Sea Surface Salinity (2014 – 2016 mean)")
Text(0.5, 1.0, 'Sea Surface Salinity (2014 – 2016 mean)')
../../../_images/254a1111647f7327098c56cf76a77308a33cf2012cb8962a7333a28d5fd3273c.png
Click here for a description of the plot The mean sea surface salinity varies around the average of $35\perthousand$ with high salinities in the subtropics, the regions of strongest evaporation and minimal precipitation. In contrast, less saline water masses can be found in the polar regions where the precipitation (and sea ice melting) induce a freshwater influx.

Section 2: Calculating Density from Salinity & Temperature#

The equation relating ocean water density to other water properties is called the equation of state. It is a non-linear function of temperature, salinity, and pressure. This can be expressed as \(\rho=\rho(T,S,p)\). Here we will show two ways to calculate the density.

The first is a linear approximation to the equation of state. We will then show how to calculate the full, non-linear equation of state using the gsw package.

Note that the potential temperature \(\theta\) is commonly used in oceanographic calculations, which is why our temperature file is called surface_theta.nc and the corresponding variable THETA. For the following linearization, we follow the probably more familiar \(T\) convention and neglect their difference.

Section 2.1: Linearized Equation of State#

Here we take the linearized equation of state from equation 1.57 in Vallis’ textbook Atmospheric and Oceanic Fluid Dynamics

\[ \rho=\rho_0[1-\beta_T(T-T_0)+\beta_S(S-S_0)+\beta_p(p-p_0)] \]

In this equation, \(\rho_0\simeq 1027\) is a reference density, \(\beta_T \simeq 2*10^{-4} /\text{K} \) is the thermal expansion coefficient, \(\beta_S \simeq 7.6*10^{-4}/\text{ppt}\) is the haline contraction coefficient, and \(\beta_p \simeq 4.4*10^{-10}/\text{Pa}\) is the compressibility coefficient. The values with subscript \(_0\) are reference values, and here we use \(T_0 = 283 \text{K}\) and \(S_0=35\). Since surface pressure rarely changes by more than a few percent, let’s assume that the pressure at the surface is equal to the reference pressure at every point (\(\beta_p(p-p_0)=0\)). The non-linearities in the full equation of state (which we will use later) arise because, in reality, the \(\beta\) terms themselves vary with pressure, salinity, and temperature.

Let’s now calculate a global map of surface density using this linear equation of state. Note that since we are using temperature and salinity datasets, our result will also be a dataset.

rho_linear = 1027 * (
    1 - 2e-4 * (subset_theta + 273.15 - 283) + 7.6e-4 * (subset_salt - 35)
)
rho_linear
<xarray.DataArray (latitude: 360, longitude: 720)> Size: 2MB
array([[          nan,           nan,           nan, ...,           nan,
                  nan,           nan],
       [          nan,           nan,           nan, ...,           nan,
                  nan,           nan],
       [          nan,           nan,           nan, ...,           nan,
                  nan,           nan],
       ...,
       [1027.0871889 , 1027.0871889 , 1027.0871889 , ..., 1027.0871889 ,
        1027.0871889 , 1027.0871889 ],
       [1027.11507147, 1027.11507147, 1027.11507147, ..., 1027.11507147,
        1027.11507147, 1027.11507147],
       [1027.17755495, 1027.17755495, 1027.17755495, ..., 1027.17755495,
        1027.17755495, 1027.17755495]])
Coordinates:
    i          (longitude) int64 6kB 0 1 2 3 4 5 6 ... 714 715 716 717 718 719
    k          int64 8B 0
    j          (latitude) int64 3kB 0 1 2 3 4 5 6 ... 354 355 356 357 358 359
  * latitude   (latitude) float64 3kB -89.75 -89.25 -88.75 ... 88.75 89.25 89.75
  * longitude  (longitude) float64 6kB -179.8 -179.2 -178.8 ... 179.2 179.8
    Z          float32 4B -5.0
# plot linearized density
fig, ax = plt.subplots(
    subplot_kw={"projection": ccrs.PlateCarree()}, dpi=100
)  # this is from cartopy https://rabernat.github.io/research_computing_2018/maps-with-cartopy.html
p = rho_linear.plot(
    cmap=cmo.cm.dense,
    vmin=1021,
    vmax=1029,
    cbar_kwargs={
        "shrink": 0.75,
        "orientation": "horizontal",
        "extend": "both",
        "pad": 0.15,
        "label": "Density (kg/m$^3$)",
    },
    ax=ax,
)
ax.coastlines(color="grey", lw=0.5)
ax.set_xticks([-180, -120, -60, 0, 60, 120, 180], crs=ccrs.PlateCarree())
ax.set_yticks([-90, -60, -30, 0, 30, 60, 90], crs=ccrs.PlateCarree())
lon_formatter = LongitudeFormatter(zero_direction_label=True)
lat_formatter = LatitudeFormatter()
ax.add_feature(cfeature.LAND, zorder=100, edgecolor="k")
ax.set_title("Surface density from linear equation (2014 – 2016 mean)")
Text(0.5, 1.0, 'Surface density from linear equation (2014 – 2016 mean)')
../../../_images/325b1752d7756acd2226196937556a6036db7e82cbbf743ae2c044a41b452d63.png

Section 2.2: Full Nonlinear Equation of State#

The full, non-linear equation of state is more complicated than the linear equation we just used. It contains dozens of equations which are impractical to code in this tutorial. Fortunately packages exist to do this calculation!

Here we will compute surface density from the full nonlinear equation in python using the gsw package which is a Python implementation of the Thermodynamic Equation of Seawater 2010 (TEOS-10)

CT = gsw.CT_from_pt(
    subset_salt, subset_theta
)  # get conservative temperature from potential temperature
rho_nonlinear = gsw.rho(subset_salt, CT, 0)
# plot density from full nonlinear equation
fig, ax = plt.subplots(
    subplot_kw={"projection": ccrs.PlateCarree()}, figsize=(11, 12), dpi=100
)  # this is from cartopy https://rabernat.github.io/research_computing_2018/maps-with-cartopy.html
p = rho_nonlinear.plot(
    cmap=cmo.cm.dense,
    vmin=1021,
    vmax=1029,
    cbar_kwargs={
        "shrink": 0.75,
        "orientation": "horizontal",
        "extend": "both",
        "pad": 0.05,
        "label": "Density (kg/m$^3$)",
    },
    ax=ax,
)
ax.coastlines(color="grey", lw=0.5)
ax.set_xticks([-180, -120, -60, 0, 60, 120, 180], crs=ccrs.PlateCarree())
ax.set_yticks([-90, -60, -30, 0, 30, 60, 90], crs=ccrs.PlateCarree())
lon_formatter = LongitudeFormatter(zero_direction_label=True)
lat_formatter = LatitudeFormatter()
ax.add_feature(cfeature.LAND, zorder=100, edgecolor="k")
ax.set_title("Surface density from nonlinear equation (2014-2016 mean)")
fig.tight_layout()
../../../_images/d22144d63650dbed0e8da7cfaa70d2696ada183088dd32461abe2cb6df1031c7.png
# plot difference between linear and non-linear equations of state
fig, ax = plt.subplots(
    subplot_kw={"projection": ccrs.PlateCarree()}, figsize=(11, 12), dpi=100
)  # this is from cartopy https://rabernat.github.io/research_computing_2018/maps-with-cartopy.html
p = (rho_linear - rho_nonlinear).plot(
    cmap="coolwarm",
    vmin=-3,
    vmax=3,
    cbar_kwargs={
        "shrink": 0.75,
        "orientation": "horizontal",
        "extend": "both",
        "pad": 0.05,
        "label": "Density difference (kg/m$^3$)",
    },
    ax=ax,
)
ax.coastlines(color="grey", lw=0.5)
ax.set_xticks([-180, -120, -60, 0, 60, 120, 180], crs=ccrs.PlateCarree())
ax.set_yticks([-90, -60, -30, 0, 30, 60, 90], crs=ccrs.PlateCarree())
lon_formatter = LongitudeFormatter(zero_direction_label=True)
lat_formatter = LatitudeFormatter()
ax.add_feature(cfeature.LAND, zorder=100, edgecolor="k")
ax.set_title("Linear minus non-linear equation of state (2014-2016 mean)")
fig.tight_layout()
../../../_images/39571182fa4a625713d96c7448a14db79aa7e225de483905292372bce44aa557.png

Upon comparing the two equations of state, we observe that they are generally similar, but certain differences arise. These differences stem from the nonlinearity of the equation of state, where the haline contraction coefficient and thermal expansion coefficient are not constant as assumed in our linear equation of state.

Irrespective of the method used to calculate density, we notice the presence of horizontal density variations (gradients) at the ocean surface. For instance, seawater tends to be less dense in the subtropics and denser near the poles. These density differences play a crucial role in driving ocean currents, as we discussed in the slides.

These findings emphasize the significant density gradients in the ocean, which shape oceanic circulation patterns. The nonlinearity in the equation of state contributes to these density variations, which in turn also influences the movement of water masses and the formation of currents.

Questions 2.2#

  1. Considering the nonlinear equation of state and TEOS-10, how do changes in ocean salinity and temperature uniquely impact the haline contraction and thermal expansion coefficients, thereby affecting density and ocean currents?

  2. One place that deep convection, a critical component of thermohaline circulation occurs, is in the North Atlantic Ocean to the south of Greenland. Based on the density maps you made, does it make sense that this would be an ideal location for a deepwater mass to form?

Click for solution

Summary#

In this tutorial, you explored sea surface salinity and temperature data from 2014 to 2016, and how those contribute to surface density patterns through the equation of state. You also compared the linear and non-linear equation of state and analyzed their differences.

Resources#

Data for this tutorial can be accessed here.