{ "cells": [ { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ClimateMatchAcademy/course-content/blob/main/tutorials/W1D2_StateoftheClimateOceanandAtmosphereReanalysis/student/W1D2_Tutorial5.ipynb)   \"Open" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "# Tutorial 5: Thermohaline Circulation\n", "\n", "**Week 1, Day 2: Ocean and Atmospheric Reanalysis**\n", "\n", "__Content creators:__ Aurora Basinski\n", "\n", "**Content reviewers:** Katrina Dobson, Danika Gupta, Maria Gonzalez, Will Gregory, Nahid Hasan, Sherry Mi, Beatriz Cosenza Muralles, Jenna Pearson, Chi Zhang, Ohad Zivan\n", "\n", "**Content editors:** Jenna Pearson, Chi Zhang, Ohad Zivan\n", "\n", "**Production editors:** Wesley Banfield, Jenna Pearson, Chi Zhang, Ohad Zivan\n", "\n", "**Our 2023 Sponsors:** NASA TOPS and Google DeepMind" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "# Tutorial Objectives\n", "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 occur in response to pressure gradients caused by these density variations.\n", "\n", "In this tutorial, we will use ocean surface data for 2014 to 2016 from the [Estimating the Circulation and Climate of the Ocean (ECCO)](https://www.ecco-group.org/) dataset to \n", "* Plot sea surface salinity and temperature\n", "* Understand the relationship between salinity, temperature, and ocean density\n", "* Explore the difference between linear and non-linear *equations of state*" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "# Setup" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "colab" ] }, "outputs": [], "source": [ "# installations ( uncomment and run this cell ONLY when using google colab or kaggle )\n", "\n", "# !pip install seaborn\n", "# !pip install cmocean\n", "# !pip install cartopy\n", "# !pip install geoviews\n", "# !pip install gsw" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "executionInfo": { "elapsed": 2058, "status": "error", "timestamp": 1682626613201, "user": { "displayName": "Sloane Garelick", "userId": "04706287370408131987" }, "user_tz": 240 } }, "outputs": [], "source": [ "import numpy as np\n", "import xarray as xr\n", "import matplotlib.pyplot as plt\n", "import seaborn as sns\n", "import cartopy as cart\n", "import cartopy.crs as ccrs\n", "from cartopy.mpl.ticker import LongitudeFormatter, LatitudeFormatter\n", "import cmocean\n", "import pooch\n", "import os\n", "import tempfile\n", "import gsw" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Helper functions\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @title Helper functions\n", "\n", "def pooch_load(filelocation=None, filename=None, processor=None):\n", " shared_location = \"/home/jovyan/shared/Data/tutorials/W1D2_StateoftheClimateOceanandAtmosphereReanalysis\" # this is different for each day\n", " user_temp_cache = tempfile.gettempdir()\n", "\n", " if os.path.exists(os.path.join(shared_location, filename)):\n", " file = os.path.join(shared_location, filename)\n", " else:\n", " file = pooch.retrieve(\n", " filelocation,\n", " known_hash=None,\n", " fname=os.path.join(user_temp_cache, filename),\n", " processor=processor,\n", " )\n", "\n", " return file" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Video 1: Thermohaline Circulation\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "tags": [ "remove-input" ] }, "outputs": [], "source": [ "# @title Video 1: Thermohaline Circulation\n", "\n", "from ipywidgets import widgets\n", "from IPython.display import YouTubeVideo\n", "from IPython.display import IFrame\n", "from IPython.display import display\n", "\n", "\n", "class PlayVideo(IFrame):\n", " def __init__(self, id, source, page=1, width=400, height=300, **kwargs):\n", " self.id = id\n", " if source == 'Bilibili':\n", " src = f'https://player.bilibili.com/player.html?bvid={id}&page={page}'\n", " elif source == 'Osf':\n", " src = f'https://mfr.ca-1.osf.io/render?url=https://osf.io/download/{id}/?direct%26mode=render'\n", " super(PlayVideo, self).__init__(src, width, height, **kwargs)\n", "\n", "\n", "def display_videos(video_ids, W=400, H=300, fs=1):\n", " tab_contents = []\n", " for i, video_id in enumerate(video_ids):\n", " out = widgets.Output()\n", " with out:\n", " if video_ids[i][0] == 'Youtube':\n", " video = YouTubeVideo(id=video_ids[i][1], width=W,\n", " height=H, fs=fs, rel=0)\n", " print(f'Video available at https://youtube.com/watch?v={video.id}')\n", " else:\n", " video = PlayVideo(id=video_ids[i][1], source=video_ids[i][0], width=W,\n", " height=H, fs=fs, autoplay=False)\n", " if video_ids[i][0] == 'Bilibili':\n", " print(f'Video available at https://www.bilibili.com/video/{video.id}')\n", " elif video_ids[i][0] == 'Osf':\n", " print(f'Video available at https://osf.io/{video.id}')\n", " display(video)\n", " tab_contents.append(out)\n", " return tab_contents\n", "\n", "\n", "video_ids = [('Youtube', 'cgh6bC2uj58'), ('Bilibili', 'BV1uh4y1j7wN')]\n", "tab_contents = display_videos(video_ids, W=730, H=410)\n", "tabs = widgets.Tab()\n", "tabs.children = tab_contents\n", "for i in range(len(tab_contents)):\n", " tabs.set_title(i, video_ids[i][0])\n", "display(tabs)" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "pycharm": { "name": "#%%\n" }, "tags": [ "remove-input" ] }, "outputs": [], "source": [ "# @markdown\n", "from ipywidgets import widgets\n", "from IPython.display import IFrame\n", "\n", "link_id = \"vdeq6\"\n", "\n", "download_link = f\"https://osf.io/download/{link_id}/\"\n", "render_link = f\"https://mfr.ca-1.osf.io/render?url=https://osf.io/{link_id}/?direct%26mode=render%26action=download%26mode=render\"\n", "# @markdown\n", "out = widgets.Output()\n", "with out:\n", " print(f\"If you want to download the slides: {download_link}\")\n", " display(IFrame(src=f\"{render_link}\", width=730, height=410))\n", "display(out)" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "# Section 1: Plot Surface Temperature and Salinity\n", "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: \n", "1. Salinity (higher salinity leads to greater density) and \n", "2. Temperature (lower temperature generally results in higher density),\n", "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. \n", "\n", "To develop a better understanding of how density varies across different regions, let's examine the average salinity and temperature at the ocean surface.\n", "\n", "First let's load the data." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# import preprocessed ecco data. This data is surface data that is monthly over the period 2014 to 2016.\n", "filename_theta = \"surface_theta.nc\"\n", "url_theta = \"https://osf.io/98ksr/download\"\n", "\n", "subset_theta = xr.open_dataset(pooch_load(url_theta, filename_theta))\n", "subset_theta" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "filename_salt = \"surface_salt.nc\"\n", "url_salt = \"https://osf.io/aufs2/download\"\n", "\n", "subset_salt = xr.open_dataset(pooch_load(url_salt, filename_salt))\n", "subset_salt" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# make land points NaN (not a number)\n", "subset_theta = subset_theta.where(\n", " subset_theta != 0\n", ") # change anywhere that the value is zero to NaN\n", "subset_salt = subset_salt.where(subset_salt != 0) # same\n", "subset_theta = subset_theta.THETA # choose the variable to remove the dimension\n", "subset_salt = subset_salt.SALT" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# plot Sea Surface Temprature - similar to plots we used in tutorials 2+3\n", "fig, ax = plt.subplots(\n", " subplot_kw={\"projection\": ccrs.PlateCarree()}, figsize=(11, 12), dpi=100\n", ") # this is from cartopy https://rabernat.github.io/research_computing_2018/maps-with-cartopy.html\n", "p = subset_theta.plot(\n", " vmin=0,\n", " cmap=cmocean.cm.thermal,\n", " cbar_kwargs={\n", " \"shrink\": 0.75,\n", " \"orientation\": \"horizontal\",\n", " \"extend\": \"both\",\n", " \"pad\": 0.05,\n", " \"label\": \"degree C\",\n", " },\n", " ax=ax,\n", ")\n", "ax.coastlines(color=\"grey\", lw=0.5)\n", "ax.set_xticks([-180, -120, -60, 0, 60, 120, 180], crs=ccrs.PlateCarree())\n", "ax.set_yticks([-90, -60, -30, 0, 30, 60, 90], crs=ccrs.PlateCarree())\n", "lon_formatter = LongitudeFormatter(zero_direction_label=True)\n", "lat_formatter = LatitudeFormatter()\n", "ax.add_feature(cart.feature.LAND, zorder=100, edgecolor=\"k\")\n", "ax.set_title(\"Sea Surface Temperature (2014-2016 mean)\")\n", "fig.tight_layout()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# plot Sea Surface Salinity\n", "fig, ax = plt.subplots(\n", " subplot_kw={\"projection\": ccrs.PlateCarree()}, figsize=(11, 12), dpi=100\n", ") # this is from cartopy https://rabernat.github.io/research_computing_2018/maps-with-cartopy.html\n", "p = subset_salt.plot(\n", " cmap=cmocean.cm.haline,\n", " vmin=30,\n", " cbar_kwargs={\n", " \"shrink\": 0.75,\n", " \"orientation\": \"horizontal\",\n", " \"extend\": \"both\",\n", " \"pad\": 0.05,\n", " \"label\": \"psu\",\n", " },\n", " ax=ax,\n", ")\n", "ax.coastlines(color=\"grey\", lw=0.5)\n", "ax.set_xticks([-180, -120, -60, 0, 60, 120, 180], crs=ccrs.PlateCarree())\n", "ax.set_yticks([-90, -60, -30, 0, 30, 60, 90], crs=ccrs.PlateCarree())\n", "lon_formatter = LongitudeFormatter(zero_direction_label=True)\n", "lat_formatter = LatitudeFormatter()\n", "ax.add_feature(cart.feature.LAND, zorder=100, edgecolor=\"k\")\n", "ax.set_title(\"Sea Surface Salinity (2014-2016 mean)\")\n", "fig.tight_layout()" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "# Section 2: Calculating Density from Salinity & Temperature\n", "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. \n", "\n", "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" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## Section 2.1: Linearized Equation of State\n", "Here we take the linearized equation of state from equation 1.57 in Vallis' textbook [\"*Atmospheric and Oceanic Fluid Dynamics*\"](https://www.cambridge.org/core/books/atmospheric-and-oceanic-fluid-dynamics/41379BDDC4257CBE11143C466F6428A4)\n", "\n", "$$ \\rho=\\rho_0[1-\\beta_T(T-T_0)+\\beta_S(S-S_0)+\\beta_p(p-p_0)] $$\n", "\n", "In this equation, $\\rho_0\\simeq 1027$ is a reference density, $\\beta_T \\simeq 2*10^{-4}$/K is the thermal expansion coefficient, $\\beta_S \\simeq 7.6*10^{-4}$/ppt is the haline contraction coefficient, and $\\beta_p \\simeq 4.4*10^{-10}$/Pa is the compressibility coefficient. The values with $_0$ are reference values, and here we use $T_0=283$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.*\n", "\n", "Let's now calculate a global map of surface density using this linear equation of state. Note that since we are using theta and salt *datasets*, our result will also be a dataset." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rho_linear = 1027 * (\n", " 1 - 2e-4 * (subset_theta + 273.15 - 283) + 7.6e-4 * (subset_salt - 35)\n", ")\n", "rho_linear" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# plot linearized density\n", "fig, ax = plt.subplots(\n", " subplot_kw={\"projection\": ccrs.PlateCarree()}, figsize=(11, 12), dpi=100\n", ") # this is from cartopy https://rabernat.github.io/research_computing_2018/maps-with-cartopy.html\n", "p = rho_linear.plot(\n", " cmap=cmocean.cm.dense,\n", " vmin=1021,\n", " vmax=1029,\n", " cbar_kwargs={\n", " \"shrink\": 0.75,\n", " \"orientation\": \"horizontal\",\n", " \"extend\": \"both\",\n", " \"pad\": 0.05,\n", " \"label\": \"kg/m$^3$\",\n", " },\n", " ax=ax,\n", ")\n", "ax.coastlines(color=\"grey\", lw=0.5)\n", "ax.set_xticks([-180, -120, -60, 0, 60, 120, 180], crs=ccrs.PlateCarree())\n", "ax.set_yticks([-90, -60, -30, 0, 30, 60, 90], crs=ccrs.PlateCarree())\n", "lon_formatter = LongitudeFormatter(zero_direction_label=True)\n", "lat_formatter = LatitudeFormatter()\n", "ax.add_feature(cart.feature.LAND, zorder=100, edgecolor=\"k\")\n", "ax.set_title(\"Surface density from linear equation (2014-2016 mean)\")\n", "fig.tight_layout()" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## Section 2.2: Full Nonlinear Equation of State\n", "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!\n", "\n", "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)](https://teos-10.github.io/GSW-Python/)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "CT = gsw.CT_from_pt(\n", " subset_salt, subset_theta\n", ") # get conservative temperature from potential temperature\n", "rho_nonlinear = gsw.rho(subset_salt, CT, 0)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# plot density from full nonlinear equation\n", "fig, ax = plt.subplots(\n", " subplot_kw={\"projection\": ccrs.PlateCarree()}, figsize=(11, 12), dpi=100\n", ") # this is from cartopy https://rabernat.github.io/research_computing_2018/maps-with-cartopy.html\n", "p = rho_nonlinear.plot(\n", " cmap=cmocean.cm.dense,\n", " vmin=1021,\n", " vmax=1029,\n", " cbar_kwargs={\n", " \"shrink\": 0.75,\n", " \"orientation\": \"horizontal\",\n", " \"extend\": \"both\",\n", " \"pad\": 0.05,\n", " \"label\": \"kg/m$^3$\",\n", " },\n", " ax=ax,\n", ")\n", "ax.coastlines(color=\"grey\", lw=0.5)\n", "ax.set_xticks([-180, -120, -60, 0, 60, 120, 180], crs=ccrs.PlateCarree())\n", "ax.set_yticks([-90, -60, -30, 0, 30, 60, 90], crs=ccrs.PlateCarree())\n", "lon_formatter = LongitudeFormatter(zero_direction_label=True)\n", "lat_formatter = LatitudeFormatter()\n", "ax.add_feature(cart.feature.LAND, zorder=100, edgecolor=\"k\")\n", "ax.set_title(\"Surface density from nonlinear equation (2014-2016 mean)\")\n", "fig.tight_layout()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# plot difference between linear and non-linear equations of state\n", "fig, ax = plt.subplots(\n", " subplot_kw={\"projection\": ccrs.PlateCarree()}, figsize=(11, 12), dpi=100\n", ") # this is from cartopy https://rabernat.github.io/research_computing_2018/maps-with-cartopy.html\n", "p = (rho_linear - rho_nonlinear).plot(\n", " cmap=\"coolwarm\",\n", " vmin=-3,\n", " vmax=3,\n", " cbar_kwargs={\n", " \"shrink\": 0.75,\n", " \"orientation\": \"horizontal\",\n", " \"extend\": \"both\",\n", " \"pad\": 0.05,\n", " \"label\": \"kg/m$^3$\",\n", " },\n", " ax=ax,\n", ")\n", "ax.coastlines(color=\"grey\", lw=0.5)\n", "ax.set_xticks([-180, -120, -60, 0, 60, 120, 180], crs=ccrs.PlateCarree())\n", "ax.set_yticks([-90, -60, -30, 0, 30, 60, 90], crs=ccrs.PlateCarree())\n", "lon_formatter = LongitudeFormatter(zero_direction_label=True)\n", "lat_formatter = LatitudeFormatter()\n", "ax.add_feature(cart.feature.LAND, zorder=100, edgecolor=\"k\")\n", "ax.set_title(\"Linear minus non-linear equation of state (2014-2016 mean)\")\n", "fig.tight_layout()" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "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.\n", "\n", "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.\n", "\n", "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." ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "### Questions 2.2\n", "\n", "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?\n", "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?" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "# Summary\n", "\n", "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." ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "# Resources\n", "\n", "Data for this tutorial can be accessed [here](https://www.ecco-group.org/)." ] } ], "metadata": { "colab": { "collapsed_sections": [], "include_colab_link": true, "name": "W1D2_Tutorial5", "provenance": [], "toc_visible": true }, "kernel": { "display_name": "Python 3", "language": "python", "name": "python3" }, "kernelspec": { "display_name": "climatematch", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.10.11" } }, "nbformat": 4, "nbformat_minor": 4 }