{ "cells": [ { "cell_type": "markdown", "id": "ea72df99", "metadata": { "id": "ea72df99" }, "source": [ "# Exploring TC PRIMED, Chapter 1c: The Environmental File\n", "- Creators: Naufal Razin, Chris Slocum, and Kathy Haynes\n", "- Affiliations: CIRA and NESDIS/STAR\n", "\n", "---\n", "\n", "## Overview\n", "TC PRIMED consists of two types of files: the overpass file and the environmental file. The overpass file contains all available satellite products from *one* overpass of a tropical cyclone, while the environmental file contains all available tropical cyclone information, and environmental diagnostics and fields *at synoptic times* (00, 06, 12, 18 UTC) throughout the storm's life. In this notebook, you will learn how to read data from the TC PRIMED environmental file.\n", "\n", "## Prerequisites\n", "To successfully navigate and use this notebook, you should be familiar with:\n", "- the basics of Python programming such as loading modules, assigning variables, and list/array indexing\n", "- NetCDF files and NetCDF groups (see Chapter 1a of this Learning Journey)\n", "- plotting data using `matplotlib`\n", "\n", "In addition, we will be using other Python packages to plot geospatial and meteorological data. You don't need to be fully versed in these packages, but being somewhat familiar with them would be helpful. These packages are:\n", "- [`cartopy`](https://scitools.org.uk/cartopy/docs/latest/getting_started/index.html)\n", "- [`metpy`](https://unidata.github.io/MetPy/latest/userguide/index.html)\n", "\n", "## Learning Outcomes\n", "By working through this notebook, you should be able to:\n", "- understand the data structure of a TC PRIMED environmental file\n", "- interact with (e.g., load and plot) data from a TC PRIMED environmental file" ] }, { "cell_type": "markdown", "id": "97d257fb-7876-4d38-a3c0-7f18ae4b2192", "metadata": { "id": "71e180b2" }, "source": [ "## Background\n", "Data in the TC PRIMED environmenal files are stored in five different NetCDF groups. They are:\n", "- `storm_metadata`, which contains information about the storm\n", "- `overpass_metadata`, which contains information about all available overpasses for the storm\n", "- `overpass_storm_metadata`, which contains information about the storm during all available overpasses\n", "- `diagnostics`, which contains environmental diagnostics calculated from the ECMWF fifth-generation reanalysis fields (ERA5)\n", "- `rectilinear`, which contains environmental fields from ERA5\n", "\n", "In this notebook, you will learn to load and plot data from the `storm_metadata`, `diagnostics`, and `rectilinear` groups. These groups have variables that represent different information about the storm at each synoptic time (00, 06, 12, 18 UTC) throughout the storm's life.\n", "\n", "For example, the `storm_metadata` group contains information about the storm, like its location and intensity, as analyzed by forecasters at the National Hurricane Center, The Central Pacific Hurricane Center, and the Joint Typhoon Warning Center. The `diagnostics` group contains information about the storm's environment, like the amount of atmospheric moisture available around the storm. Forecasters actively rely on these environmental variables to forecast the intensity of the storm. Finally, the `rectilinear` group contains fields of environmental data from the ECMWF fifth-generation reanalysis (ERA5).\n", "\n", "To get familiarized with these products, let's look at the TC PRIMED environmental file from Hurricane Florence (2018)." ] }, { "cell_type": "markdown", "id": "a4f3a040-9518-4063-8196-632153816797", "metadata": { "id": "71e180b2" }, "source": [ "
\n", "\"An\n", "\n", "
Figure 1. An image of Hurricane Florence (2018) captured by European Space Agency astronaut Alexander Gerst from aboard the International Space Station. Source: ESA
\n", "
" ] }, { "cell_type": "markdown", "id": "177d900e-d00f-4a92-8e08-c13782277f5d", "metadata": { "id": "71e180b2" }, "source": [ "Hurricane Florence was a long-lived hurricane that formed off the coast of Senegal and The Gambia, and tracked westward across the North Atlantic Ocean. During its track across the Atlantic, it briefly intensified to a Category 4 hurricane (on the [Saffir-Simpson Hurricane Wind Scale](https://www.nhc.noaa.gov/aboutsshws.php#:~:text=The%20Saffir%2DSimpson%20Hurricane%20Wind,Scale%20estimates%20potential%20property%20damage.)), weakened to a tropical storm, and re-intensified back to a Category 4 hurricane before making landfall along the southeastern coast of North Carolina as a high-end Category 1 hurricane. Florence brought [record-breaking rainfall](https://www.weather.gov/ilm/HurricaneFlorence) across eastern North Carolina and parts of northeastern South Carolina.\n", "\n", "Using data from the environmental file, you will plot the intensity trends of Hurricane Florence, as well as the the environmental variables within which it was embedded." ] }, { "cell_type": "markdown", "id": "79cec0cc", "metadata": { "id": "79cec0cc" }, "source": [ "## Software\n", "This tutorial uses the Python programming language and packages. We will use:\n", "- `netCDF4` to read in the TC PRIMED file\n", "- `matplotlib` to plot the data\n", "- `cartopy` to plot the data on a map\n", "- `metpy` to plot a skew-T diagram\n", "- `numpy` for simple array operations" ] }, { "cell_type": "markdown", "id": "EFM3egG3MY1M", "metadata": { "id": "EFM3egG3MY1M" }, "source": [ "### Install Packages\n", "Let's first check if we have the necessary Python packages to run this notebook. If we don't, let's install them." ] }, { "cell_type": "code", "execution_count": null, "id": "ZB4R_-jQMaYV", "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 1000 }, "executionInfo": { "elapsed": 23394, "status": "ok", "timestamp": 1686353772551, "user": { "displayName": "Naufal Razin", "userId": "00945304052336708650" }, "user_tz": 360 }, "id": "ZB4R_-jQMaYV", "outputId": "1f8b2bce-7a7a-436e-e6e5-4fc3fe95834e" }, "outputs": [], "source": [ "import subprocess, sys\n", "packages = [\"netCDF4\", \"matplotlib\", \"metpy\",\n", " \"cartopy\", \"numpy\"]\n", "# If this notebook is running on Google Colab, we know what packages\n", "# need to be installed. In addition, we would have to resolve some\n", "# incompatibility issues between shapely and cartopy.\n", "if 'google.colab' in sys.modules:\n", " !pip install netCDF4\n", " !pip install metpy\n", " !apt-get -V -y -qq install python-cartopy python3-cartopy\n", " !pip uninstall shapely -y\n", " !pip install shapely --no-binary shapely\n", " !pip install cartopy\n", "\n", "# If this note book is not running on Google Colab, check if Python\n", "# has the packages we need to run this notebook. If not, download it\n", "else:\n", " for package in packages:\n", " try:\n", " __import__(package)\n", " except ImportError:\n", " subprocess.check_call([sys.executable, '-m', 'pip', 'install', package])" ] }, { "cell_type": "markdown", "id": "BGmdmkvxPCQK", "metadata": { "id": "BGmdmkvxPCQK" }, "source": [ "Now, let's load the modules in the packages (e.g., `Dataset`) or load the packages and assign a shorter object name for the packages (e.g., `as plt`) for a cleaner use throughout the notebook." ] }, { "cell_type": "code", "execution_count": null, "id": "X5Z3kWPOPINN", "metadata": { "id": "X5Z3kWPOPINN" }, "outputs": [], "source": [ "# Load the Python packages we will use in this notebook\n", "from netCDF4 import Dataset\n", "import datetime\n", "import matplotlib.pyplot as plt\n", "import matplotlib as mpl\n", "import matplotlib.ticker as mticker\n", "import cartopy.crs as ccrs\n", "from metpy.plots import SkewT\n", "from metpy.units import units\n", "from metpy.calc import dewpoint_from_relative_humidity\n", "import numpy as np" ] }, { "cell_type": "markdown", "id": "7cda84c1", "metadata": { "id": "7cda84c1" }, "source": [ "### Read File Online\n", "Finally, let's retrieve information from the TC PRIMED file that we will use in this example. As we have mentioned above, we will learn to read and plot data from a TC PRIMED environmental file from Hurricane Florence (2018). We will use the Python `netCDF4` and `requests` packages to read and retrieve the information directly from the TC PRIMED file available on an Amazon Web Service S3 bucket as part of the NOAA Open Data Dissemination program (NODD), without downloading the file, and store the information from the file in an \"instance\" type called `DS`." ] }, { "cell_type": "code", "execution_count": null, "id": "d79c3841", "metadata": { "id": "d79c3841" }, "outputs": [], "source": [ "import requests\n", "\n", "# Specify the URL to the TC PRIMED folder on NODD\n", "NODD_URL = \"https://noaa-nesdis-tcprimed-pds.s3.amazonaws.com/v01r00/final/2018/AL/06/\"\n", "\n", "# Specify the name of the file we will use from the TC PRIMED folder on NODD\n", "FILE_NAME = \"TCPRIMED_v01r00-final_AL062018_era5_s20180830060000_e20180918120000.nc\"\n", "\n", "# Join NODD_URL and FILE_NAME to produce a complete link\n", "# Retrieve the contents of the TC PRIMED file from the complete link\n", "url_response = requests.get(NODD_URL + FILE_NAME)\n", "\n", "# Load the contents of the TC PRIMED file in an \"instance\" called DS\n", "DS = Dataset(FILE_NAME, memory=url_response.content)" ] }, { "cell_type": "markdown", "id": "b3cf487d", "metadata": { "id": "b3cf487d" }, "source": [ "## Part 1: Loading and plotting the storm metadata\n", "If you've gone through the previous two chapters, you should be somewhat familiar with the concept of NetCDF groups, instances, and variables in Python. But, for the sake of completeness, let's print out the instance from the root group of the environmental file." ] }, { "cell_type": "code", "execution_count": null, "id": "fc6a956b", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "executionInfo": { "elapsed": 7, "status": "ok", "timestamp": 1686353776616, "user": { "displayName": "Naufal Razin", "userId": "00945304052336708650" }, "user_tz": 360 }, "id": "fc6a956b", "outputId": "8789df50-76e3-4296-faa3-0a316d2cf97c" }, "outputs": [], "source": [ "# DS would automatically contain information from the root group\n", "# Print the instance of the file from the root group\n", "print(DS)" ] }, { "cell_type": "markdown", "id": "5e5d8eaf", "metadata": { "id": "5e5d8eaf" }, "source": [ "Notice, as we have discussed above, the root group contains five sub-groups. Let's print out the instance of the ```storm_metadata``` group." ] }, { "cell_type": "code", "execution_count": null, "id": "cd66a37c", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "executionInfo": { "elapsed": 4, "status": "ok", "timestamp": 1686353776616, "user": { "displayName": "Naufal Razin", "userId": "00945304052336708650" }, "user_tz": 360 }, "id": "cd66a37c", "outputId": "52002380-9a64-41ce-df27-13163ffd6ae3" }, "outputs": [], "source": [ "# Using the file instance, load the storm_metadata group as a\n", "# group instance called storm_metadata_group\n", "storm_metadata_group = DS[\"storm_metadata\"]\n", "\n", "# Print the storm_metadata group instance\n", "print(storm_metadata_group)" ] }, { "cell_type": "markdown", "id": "c2f40399", "metadata": { "id": "c2f40399" }, "source": [ "Now let's read the time and intensity variables from the `storm_metadata` group.\n" ] }, { "cell_type": "code", "execution_count": null, "id": "TRkmJEYfxu5e", "metadata": { "id": "TRkmJEYfxu5e" }, "outputs": [], "source": [ "# Using the file instance, load the time and intensity variables from the\n", "# storm_metadata group by specifying its \"path\" in the file, and using [:]\n", "# to load the variable\n", "storm_metadata_time = DS[\"storm_metadata/time\"][:]\n", "storm_metadata_intensity = DS[\"storm_metadata/intensity\"][:]" ] }, { "cell_type": "markdown", "id": "2e70f586", "metadata": { "id": "2e70f586" }, "source": [ "### Brief remark\n", "The time variable in all TC PRIMED overpass and environmental files exists in units of seconds since 1970-01-01T00:00:00. This unit of time is also referred to as the [Unix time stamp](https://en.wikipedia.org/wiki/Unix_time). To make the time values cleaner for plotting, you can convert it to a string using the Python datetime module. Let's briefly go through this process." ] }, { "cell_type": "code", "execution_count": null, "id": "766064cb", "metadata": { "id": "766064cb" }, "outputs": [], "source": [ "# For each unix time element in storm_metadata_time, convert the unix time\n", "# to a Python datetime object and assign it to the variable storm_metadata_datetime\n", "storm_metadata_datetime = [datetime.datetime.fromtimestamp(t) for t in storm_metadata_time]\n", "\n", "# Now, for each datetime object element in storm_metadata_datetime, convert the datetime\n", "# to string in the format of month/day hour\n", "storm_metadata_datetime_str = [t.strftime(\"%m/%d %H\") for t in storm_metadata_datetime]" ] }, { "cell_type": "markdown", "id": "bfc6fe13", "metadata": { "id": "bfc6fe13" }, "source": [ "Now you're ready to plot the data!" ] }, { "cell_type": "code", "execution_count": null, "id": "7404aaee", "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 744 }, "executionInfo": { "elapsed": 798, "status": "ok", "timestamp": 1686353777412, "user": { "displayName": "Naufal Razin", "userId": "00945304052336708650" }, "user_tz": 360 }, "id": "7404aaee", "outputId": "d31103a0-6488-4d4f-e3f7-fd675b8d1c6f" }, "outputs": [], "source": [ "# Set the figure size\n", "plt.figure(figsize=(12, 6))\n", "\n", "# Make the plot\n", "plt.plot(storm_metadata_datetime_str, storm_metadata_intensity, color=\"k\", lw=1)\n", "\n", "# Plot the time at every fourth time point, with the values rotated by 45 degrees\n", "plt.xticks(storm_metadata_datetime_str[::4], storm_metadata_datetime_str[::4], rotation=45, fontsize=14, ha=\"right\")\n", "\n", "plt.ylabel(\"Intensity (knots)\", fontsize=14)\n", "plt.yticks(fontsize=14)" ] }, { "cell_type": "markdown", "id": "ffe24b74", "metadata": {}, "source": [ "To identify the different Saffir-Simpson *categories* in the plot above, you can add alternating shaded boxes to the plot. You just have to specify the bounds of each category, given below:\n", "- 0 - 33 knots : Tropical Depression (TD)\n", "- 34 - 64 knots : Tropical Storm (TS)\n", "- 65 - 83 knots : Category 1 (Cat. 1)\n", "- 84 - 95 knots : Category 2 (Cat. 2)\n", "- 96 - 113 knots : Category 3 (Cat. 3)\n", "- 114 - 134 knots : Category 4 (Cat. 4)\n", "- Above 134 knots : Category 5 (Cat. 5), but for plotting purposes, let's cap it at 185\n", "\n", "Let's re-make the plot above with the Saffir-Simpson categories labeled." ] }, { "cell_type": "code", "execution_count": null, "id": "748d6fe7", "metadata": {}, "outputs": [], "source": [ "# Set the figure size\n", "plt.figure(figsize=(12, 6))\n", "\n", "# Make the plot\n", "plt.plot(storm_metadata_datetime_str, storm_metadata_intensity, color=\"k\", lw=1)\n", "\n", "# Plot the time at every fourth time point, with the values rotated by 45 degrees\n", "plt.xticks(storm_metadata_datetime_str[::4], storm_metadata_datetime_str[::4], rotation=45, fontsize=14, ha=\"right\")\n", "\n", "plt.ylabel(\"Intensity (knots)\", fontsize=14)\n", "plt.yticks(fontsize=14)\n", "\n", "# Add shaded boxes for TS, Cat 2, and Cat 4 Saffir-Simpson categories\n", "plt.axhspan(34, 64, color=\"grey\", alpha=0.25)\n", "plt.axhspan(84, 95, color=\"grey\", alpha=0.25)\n", "plt.axhspan(114, 134, color=\"grey\", alpha=0.25)\n", "\n", "# Add text labels for the Saffir-Simpson categories\n", "plt.text(storm_metadata_datetime_str[0], 48, \"TS\", \n", " fontsize=25, color=\"k\", ha=\"left\", va=\"center\")\n", "plt.text(storm_metadata_datetime_str[0], 73, \"CAT1\", \n", " fontsize=25, color=\"k\", ha=\"left\", va=\"center\")\n", "plt.text(storm_metadata_datetime_str[0], 88.5, \"CAT2\", \n", " fontsize=25, color=\"k\", ha=\"left\", va=\"center\")\n", "plt.text(storm_metadata_datetime_str[0], 103.5, \"CAT3\", \n", " fontsize=25, color=\"k\", ha=\"left\", va=\"center\")\n", "plt.text(storm_metadata_datetime_str[0], 123, \"CAT4\", \n", " fontsize=25, color=\"k\", ha=\"left\", va=\"center\")" ] }, { "cell_type": "markdown", "id": "9e98ba56", "metadata": { "id": "9e98ba56" }, "source": [ "From the figure above, you can clearly identify the periods in which Hurricane Florence intensified to a Category 4 hurricane (around September 5th), weakened to a tropical storm (around September 7th through 9th), and reintensified back to a Category 4 hurricane (around September 10th through 12th).\n", "\n", "You can also plot a track of Hurricane Florence on a map. To do that, load the storm latitude and longitude data from the ```storm_metadata``` group." ] }, { "cell_type": "code", "execution_count": null, "id": "8e687140", "metadata": { "id": "8e687140" }, "outputs": [], "source": [ "# Using the file instance, load the storm latitude and longitude\n", "# variables from the storm_metadata group by specifying its \"path\"\n", "# in the file, and using [:] to load the variable\n", "storm_metadata_latitude = DS[\"storm_metadata/storm_latitude\"][:]\n", "storm_metadata_longitude = DS[\"storm_metadata/storm_longitude\"][:]" ] }, { "cell_type": "code", "execution_count": null, "id": "6a18cb9a", "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 674 }, "executionInfo": { "elapsed": 825, "status": "ok", "timestamp": 1686353778236, "user": { "displayName": "Naufal Razin", "userId": "00945304052336708650" }, "user_tz": 360 }, "id": "6a18cb9a", "outputId": "a0c55996-4d78-45c8-946b-2bc81da91493" }, "outputs": [], "source": [ "# Set the figure size\n", "plt.figure(figsize=(15, 7.5))\n", "\n", "# Generate a map projection on which to plot the data\n", "ax = plt.axes(projection=ccrs.Mercator())\n", "\n", "# Plot the coastlines on the map\n", "ax.coastlines()\n", "\n", "# Insert latitude and longitude grid lines\n", "ax.gridlines(draw_labels=True)\n", "\n", "# Plot a black line showing the track of the storm onto the map\n", "plt.plot(storm_metadata_longitude, storm_metadata_latitude, color=\"k\", transform=ccrs.Geodetic(), lw=0.75)\n", "\n", "# Plot the location of the storm at each synoptic time\n", "plt.scatter(storm_metadata_longitude, storm_metadata_latitude, color=\"k\", transform=ccrs.Geodetic())" ] }, { "cell_type": "markdown", "id": "bcf5fea2", "metadata": { "id": "bcf5fea2" }, "source": [ "### But, wait!\n", "\n", "You can plot the storm track and intensity simultaneously on a map. To do this, you can use the contour version of `plt.scatter` and assign the intensity to the contour argument (e.g., `plt.scatter(storm_metadata_lon, storm_metadata_lat, c=storm_metadata_intensity)`).\n", "\n", "However, you can also add more parameters to overlay the hurricane intensity category on a map by customizing your own colormap. To do so, we'll use the bounds for each category given above and replicated below\n", "- 0 - 33 knots : Tropical Depression (TD)\n", "- 34 - 64 knots : Tropical Storm (TS)\n", "- 65 - 83 knots : Category 1 (Cat. 1)\n", "- 84 - 95 knots : Category 2 (Cat. 2)\n", "- 96 - 113 knots : Category 3 (Cat. 3)\n", "- 114 - 134 knots : Category 4 (Cat. 4)\n", "- Above 134 knots : Category 5 (Cat. 5), but for plotting purposes, let's cap it at 185\n", "\n", "Using these ranges, let's generate our own custom colormap." ] }, { "cell_type": "code", "execution_count": null, "id": "4160a9e4", "metadata": { "id": "4160a9e4" }, "outputs": [], "source": [ "# Specify bounds based on the Saffir-Simpson Hurricane Wind Scale categories\n", "bounds = [0, 33, 64, 83, 95, 113, 134, 185]\n", "\n", "# Specify colormap\n", "cmap = plt.cm.plasma\n", "\n", "# Generate custom colormap\n", "norm = mpl.colors.BoundaryNorm(bounds, cmap.N)" ] }, { "cell_type": "markdown", "id": "b901658a", "metadata": { "id": "b901658a" }, "source": [ "With this custom colormap, you can plot the storm location and intensity *category* simultaneously." ] }, { "cell_type": "code", "execution_count": null, "id": "96e5e812", "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 628 }, "executionInfo": { "elapsed": 836, "status": "ok", "timestamp": 1686353779071, "user": { "displayName": "Naufal Razin", "userId": "00945304052336708650" }, "user_tz": 360 }, "id": "96e5e812", "outputId": "8452d2c0-62b3-4a9e-f69c-0953d979692b" }, "outputs": [], "source": [ "# Set the figure size\n", "plt.figure(figsize=(15, 7.5))\n", "\n", "# Generate a map projection on which to plot the data\n", "ax = plt.axes(projection=ccrs.Mercator())\n", "\n", "# Plot the coastlines on the map\n", "ax.coastlines()\n", "\n", "# Insert latitude and longitude grid lines\n", "ax.gridlines(draw_labels=True)\n", "\n", "# Plot a line showing the track of the storm onto the map\n", "plt.plot(storm_metadata_longitude, storm_metadata_latitude,color=\"k\", transform=ccrs.Geodetic(), lw=0.75)\n", "\n", "# Plot a contour of storm category at each time point onto the map\n", "plt.scatter(storm_metadata_longitude, storm_metadata_latitude, c=storm_metadata_intensity, cmap=cmap, norm=norm, transform=ccrs.Geodetic(), zorder=2)\n", "\n", "# Generate and label colorbar\n", "cb = plt.colorbar(fraction=0.024)\n", "cb.set_label(\"Intensity\", size=14)\n", "\n", "# Set colorbar tick location and categories\n", "cb.set_ticks([17,49.5,74.5,90,105,124.5,160])\n", "cb.set_ticklabels([\"TD\",\"TS\",\"Cat. 1\",\"Cat. 2\",\"Cat. 3\",\"Cat. 4\",\"Cat. 5\"], size=14)" ] }, { "cell_type": "markdown", "id": "7c347165", "metadata": { "id": "7c347165" }, "source": [ "## Part 2: Loading and plotting the environmental diagnostics\n", "Tropical cyclone environmental diagnostics are a set of information that characterize the environment around a tropical cyclone. This diagnostic information come from numerical weather model output fields, with different parameters calculated at or across different radial regions and vertical levels (for a complete list, see Table 1 from Slocum et al. 2022). Forecasters rely on environmental diagnostics to better predict tropical cyclone intensity change, making them one of the most useful forecast tools for forecasting tropical cyclones.\n", "\n", "TC PRIMED contains environmental information calculated from the ECMWF version 5 reanalysis fields (ERA5). In Part 2 and Part 3 of this notebook, you will learn to plot the diagnostic parameters as well as the diagnostic fields from ERA5. Let's first begin by looking at the environmental vertical shear of the horizontal winds, or simply, the vertical shear." ] }, { "cell_type": "markdown", "id": "ef60cbb1", "metadata": { "id": "ef60cbb1" }, "source": [ "### Part 2A: The environmental vertical shear of the horizontal winds\n", "\n", "The vertical shear parameter represents the difference between the wind speed and direction at lower levels compared to the upper levels. A bigger vertical shear magnitude represents a bigger difference in the wind speed and direction between the lower and upper levels. In environments with strong vertical shear, tropical cyclones cannot maintain their structure and will most likely weaken.\n", "\n", "Let's first load and print out the diagnostics group instance." ] }, { "cell_type": "code", "execution_count": null, "id": "9aa91b22", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "executionInfo": { "elapsed": 2, "status": "ok", "timestamp": 1686353779071, "user": { "displayName": "Naufal Razin", "userId": "00945304052336708650" }, "user_tz": 360 }, "id": "9aa91b22", "outputId": "ca052138-7d40-457e-97a0-4c7250ef7133" }, "outputs": [], "source": [ "# Using the file instance, load the diagnostics group as a\n", "# group instance called diagnostics_group\n", "diagnostics_group = DS[\"diagnostics\"]\n", "\n", "# Print out the diagnostics group instance\n", "print(diagnostics_group)" ] }, { "cell_type": "markdown", "id": "5b7a8a39", "metadata": { "id": "5b7a8a39" }, "source": [ "
\n", "

Exercise 1

Read through the printout of the diagnostics group instance above. In the list of available variables, locate the variable name for the magnitude of the vertical shear. Using that variable name, complete the uncomment the template below and change the code as necessary to load and print the variable instance for the magnitude of the vertical shear.\n", "
" ] }, { "cell_type": "markdown", "id": "nUJlz85sVfMu", "metadata": { "id": "nUJlz85sVfMu" }, "source": [ "
\n", "Hint: You only need to use [:] when loading the variable. Not its instance." ] }, { "cell_type": "code", "execution_count": null, "id": "212aad4d", "metadata": { "id": "212aad4d" }, "outputs": [], "source": [ "# Using the file instance, load the variable instance for vertical\n", "# shear magnitude in the diagnostics group by specifying its \"path\"\n", "# in the file\n", "#shear_magnitude_instance = DS[\"insert_shear_magnitude_variable_here\"]\n", "\n", "# Print out the shear magnitude variable instance\n", "#print(shear_magnitude_instance)" ] }, { "cell_type": "markdown", "id": "c79f875d", "metadata": { "id": "c79f875d" }, "source": [ "From the printout above, you should see that the `shear_magnitude` variable has a dimension of `(time, layer, regions)` with a shape of `(78, 2, 2)`. In other words, there are 78 times at which the shear magnitude variable is available (recall, at each synoptic time throughout the storm's life), for two different layers, and for two different regions.\n", "\n", "These layers and regions are listed in the printout above. That is, the first entry in the `layer` dimension corresponds to vertical shear magnitude calculated between the 850 and 500 hPa layers, whereas the second entry in the `layer` dimension corresponds to vertical shear magnitude calculated between the 850 and 200 hPa layers.\n", "\n", "Similarly, the first entry in the `regions` dimension corresponds to vertical shear magnitude calculated for the region between 0 and 500 km from the storm center, while the second entry in the `regions` dimension corresponds to vertical shear magnitude calculated for the region between 200 and 800 km from the storm center.\n", "\n", "Let's plot the evolution of the vertical shear magnitude throughout the storm's life alongside the evolution of the intensity of Hurricane Florence, like we have done above. We will do this for the shear mangitude calculated for:\n", "- the layer between 850 and 200 hPa, the second entry in the `layer` dimension\n", "- the region between 0 and 500 km from the storm center the first entry in the `regions` dimension" ] }, { "cell_type": "code", "execution_count": null, "id": "96a0c987", "metadata": { "id": "96a0c987" }, "outputs": [], "source": [ "# Using the file instance, load the shear magnitude variable from the\n", "# diagnostics group by specifying its \"path\" in the file\n", "# Use the appropriate index to load the shear magnitude that we have\n", "# defined above\n", "# time index = : for all entries of the time dimension\n", "# layer index = 1 for the second entry of the layer dimension\n", "# region index = 0 for the first entry of the region dimension\n", "shear_magnitude = DS[\"diagnostics/shear_magnitude\"][:,1,0]" ] }, { "cell_type": "markdown", "id": "abf78101", "metadata": { "id": "abf78101" }, "source": [ "Before we proceed, recall that the TC PRIMED environmental files contain information about the storm and its environment at each synoptic time throughout the storm's life. Therefore, the time variable in the `storm_metadata`, `diagnostics`, and `rectilinear` groups are all the same. To plot the time evolution of the vertical shear magnitude alongside the intensity, we can either load a time variable from the `diagnostics` group, or simply use the time variable from the `storm_metadata` group that we have loaded above." ] }, { "cell_type": "code", "execution_count": null, "id": "fe51255e", "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 726 }, "executionInfo": { "elapsed": 684, "status": "ok", "timestamp": 1686353779754, "user": { "displayName": "Naufal Razin", "userId": "00945304052336708650" }, "user_tz": 360 }, "id": "fe51255e", "outputId": "c30382c5-2c13-4b4f-b4e6-f5d8cfb095e4" }, "outputs": [], "source": [ "# Set the figure size\n", "plt.figure(figsize=(12, 6))\n", "\n", "# Plot the time evolution of the intensity of Hurricane Florence\n", "# in a solid black line as \"intensity_line\"\n", "intensity_line = plt.plot(storm_metadata_datetime_str, storm_metadata_intensity, color=\"k\", lw=1, label=\"Intensity\")\n", "\n", "# Plot the time at every fourth time point, with the values rotated by 45 degrees\n", "plt.xticks(storm_metadata_datetime_str[::4], storm_metadata_datetime_str[::4], rotation=45, fontsize=14, ha=\"right\")\n", "\n", "# Add labels for the intensity\n", "plt.ylabel(\"Intensity (knots)\", fontsize=14)\n", "plt.yticks(fontsize=14)\n", "\n", "# Create a new axis that shares the same x axis as the intensity\n", "# plot above\n", "plt.twinx()\n", "\n", "# Plot the time evolution of the shear magnitude around Hurricane Florence\n", "# in dashed black line as \"shear_line\"\n", "shear_line = plt.plot(storm_metadata_datetime_str, shear_magnitude, color=\"k\", ls=\"dashed\", lw=1, label=\"Shear Magnitude\")\n", "\n", "# Add labels for the shear magnitude\n", "plt.ylabel(\"Shear Magnitude (m/s)\", fontsize=14)\n", "plt.yticks(fontsize=14)\n", "\n", "# Add legend for the lines\n", "# We need to account for the fact that we have two axis objects,\n", "# since we used \"plt.twinx()\"\n", "figure_lines = intensity_line + shear_line\n", "figure_labels = [line.get_label() for line in figure_lines]\n", "plt.legend(figure_lines, figure_labels)" ] }, { "cell_type": "markdown", "id": "baf60440", "metadata": { "id": "baf60440" }, "source": [ "The magnitude of the vertical shear is not the only environmental factor that affects hurricane intensity. But its impact is non-negligible. Notice from the plot above, as the shear magnitude decreases, Florence intensified soon after. And as the shear magnitude increases, Florence weakened soon after." ] }, { "cell_type": "markdown", "id": "068150de", "metadata": { "id": "068150de" }, "source": [ "### Part 2B: The vertical profile of temperature and dewpoint temperature\n", "In addition to scalar values that describe the tropical cyclone environment, like the shear magnitude that we looked at above, environmental information also exists in other forms, like a vertical profile of temperature and dewpoint temperature. You can obtain these vertical profiles from the TC PRIMED environmental file. We will be using the `level`, `temperature`, and `relative_humidity` variables to construct a vertical profile. Let's first look at their variable instances." ] }, { "cell_type": "code", "execution_count": null, "id": "346ddfde", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "executionInfo": { "elapsed": 3, "status": "ok", "timestamp": 1686353779754, "user": { "displayName": "Naufal Razin", "userId": "00945304052336708650" }, "user_tz": 360 }, "id": "346ddfde", "outputId": "6f1af826-c427-4d6b-956b-9773c8c3a23c" }, "outputs": [], "source": [ "# Using the file instance, load the variable instance for level,\n", "# temperature, and relative humidity in the diagnostics group by\n", "# specifying its \"path\" in the file\n", "level_instance = DS[\"diagnostics/level\"]\n", "temperature_instance = DS[\"diagnostics/temperature\"]\n", "relative_humidity_instance = DS[\"diagnostics/relative_humidity\"]\n", "\n", "# Print out the variable instances\n", "print(level_instance)\n", "print(\" \")\n", "print(temperature_instance)\n", "print(\" \")\n", "print(relative_humidity_instance)" ] }, { "cell_type": "markdown", "id": "66d35d99", "metadata": { "id": "66d35d99" }, "source": [ "The `level` variable corresponds to the vertical atmospheric levels in hPa, of which there are 21. For the `temperature` and `relative_humidity` variables, there are 78 data points for time (again, at each synoptic time throughout the storm's life), 21 vertical levels (for each atmospheric level), and averaged over two regions (0 to 500 km and 200 to 800 km).\n", "\n", "Let's plot a vertical profile of temperature and dewpoint temperature:\n", "- at the first analysis time\n", "- for the region averaged between 200 and 800 km\n", "\n", "We will need to set up the variables for plotting using metpy utilities following the [example](https://unidata.github.io/MetPy/latest/tutorials/upperair_soundings.html) from the metpy tutorial." ] }, { "cell_type": "code", "execution_count": null, "id": "ea9f81a5", "metadata": { "id": "ea9f81a5" }, "outputs": [], "source": [ "# Using the file instance, load the shear magnitude variable from the\n", "# diagnostics group by specifying its \"path\" in the file\n", "# Use the appropriate index to load the shear magnitude that we have\n", "# defined above\n", "# time index = 0 for the first analysis time\n", "# level index = : for all vertical atmospheric levels\n", "# region index = 1 for the second entry of the region dimension\n", "# Metpy does not work on the mask array that gets automatically\n", "# assigned when we load a variable from a NetCDF file. So, we must\n", "# convert the variables into a numpy array before applying a metpy\n", "# unit type to each variable\n", "diagnostics_temperature = np.array(DS[\"diagnostics/temperature\"][0, :, 1]) * units.kelvin\n", "diagnostics_relative_humidity = np.array(DS[\"diagnostics/relative_humidity\"][0, :, 1]) * units.percent\n", "diagnostics_level = np.array(DS[\"diagnostics/level\"][:]) * units.hPa" ] }, { "cell_type": "code", "execution_count": null, "id": "53ddb6f2", "metadata": { "id": "53ddb6f2" }, "outputs": [], "source": [ "# Using another metpy utility that we have loaded above,\n", "# obtain the dewpoint temperature from the temperature and\n", "# relative humidity\n", "diagnostics_dewpoint = dewpoint_from_relative_humidity(diagnostics_temperature, diagnostics_relative_humidity)" ] }, { "cell_type": "code", "execution_count": null, "id": "2a932aed", "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 723 }, "executionInfo": { "elapsed": 1197, "status": "ok", "timestamp": 1686353780950, "user": { "displayName": "Naufal Razin", "userId": "00945304052336708650" }, "user_tz": 360 }, "id": "2a932aed", "outputId": "2f35f366-1374-4d7c-ca2e-7f2f64977510" }, "outputs": [], "source": [ "# Create a new figure\n", "fig = plt.figure(figsize=(9, 9))\n", "skew = SkewT(fig)\n", "\n", "# Add the relevant special lines\n", "skew.plot_dry_adiabats()\n", "skew.plot_moist_adiabats()\n", "skew.plot_mixing_lines()\n", "\n", "# Plot the data using normal plotting functions, in this case using\n", "# log scaling in Y, as dictated by the typical meteorological plot\n", "skew.plot(diagnostics_level, diagnostics_temperature, 'darkorange', linewidth=3, label='Temperature')\n", "skew.plot(diagnostics_level, diagnostics_dewpoint, 'royalblue', linewidth=3, label='Dewpoint')\n", "fig.legend()" ] }, { "cell_type": "markdown", "id": "051a8a2b", "metadata": { "id": "051a8a2b" }, "source": [ "## Part 3: Loading and plotting the rectilinear ERA5 data\n", "Last, but not least, let's look at the environmental field variables from ERA5, stored in the `rectilinear` group of the TC PRIMED environmental file. The ERA5 environmental fields in TC PRIMED exist in the form of a box, 20-degree longitude wide and 20-degree latitude tall, centered on the model tropical cyclone center. In other words, as the model tropical cyclone moves, the box moves along with it. Therefore, there is a different set of longitude and latitude values for each analysis time. Slocum et al. (2022) discuss the differences between the model and observed tropical cyclone center.\n", "\n", "Note, however, that ERA5 has a 0.25 degree latitude and longitude resolution. So, including the longitude and latitude value at the center of the box, our 20-degree longitude by 20-degree latitude box should contain 81 longitudinal and latitudinal points, respectively.\n", "\n", "Before we look at the environmental fields, let's re-visit the intensity plot of Hurricane Florence." ] }, { "cell_type": "code", "execution_count": null, "id": "7676adcd", "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 744 }, "executionInfo": { "elapsed": 1608, "status": "ok", "timestamp": 1686353782555, "user": { "displayName": "Naufal Razin", "userId": "00945304052336708650" }, "user_tz": 360 }, "id": "7676adcd", "outputId": "8d350e35-e7b4-4a48-b9b3-709423bbd751" }, "outputs": [], "source": [ "# Set the figure size\n", "plt.figure(figsize=(12, 6))\n", "\n", "# Make the plot\n", "plt.plot(storm_metadata_datetime_str, storm_metadata_intensity, color=\"k\", lw=1)\n", "\n", "# Plot the time at every fourth time point, with the values rotated by 45 degrees\n", "plt.xticks(storm_metadata_datetime_str[::4], storm_metadata_datetime_str[::4], rotation=45, fontsize=14, ha=\"right\")\n", "\n", "plt.ylabel(\"Intensity (knots)\", fontsize=14)\n", "plt.yticks(fontsize=14)" ] }, { "cell_type": "markdown", "id": "be316219", "metadata": { "id": "be316219" }, "source": [ "From the plot above, Florence reached its peak intensity on September 11th at 12 UTC. However, before reaching its peak intensity, it underwent a period of weakening that ended on September 7th at 00 UTC. What does the low-level relative humidity field look like at these two different times? Let's load the variable and plot them!\n", "\n", "You can see a list of available variables in the `rectilinear` group by printing the group instance like we have done above. But, we will skip that step for this example." ] }, { "cell_type": "code", "execution_count": null, "id": "0de43355", "metadata": { "id": "0de43355" }, "outputs": [], "source": [ "# Load all variables\n", "rectilinear_relative_humidity = DS[\"rectilinear/relative_humidity\"][:]\n", "rectilinear_latitude = DS[\"rectilinear/latitude\"][:]\n", "rectilinear_longitude = DS[\"rectilinear/longitude\"][:]\n", "rectilinear_level = DS[\"rectilinear/level\"][:]\n", "\n", "# Since we're not using the time directly to plot, we can keep this\n", "# in its unixtime format\n", "rectilinear_time = DS[\"rectilinear/time\"][:]" ] }, { "cell_type": "markdown", "id": "4dfa147b", "metadata": { "id": "4dfa147b" }, "source": [ "As we have noted above, we are interested in two analysis times. Let's specify them below, convert them to unix time units, and create a date and time string at each time for plotting purposes." ] }, { "cell_type": "code", "execution_count": null, "id": "a5badeb9", "metadata": { "id": "a5badeb9" }, "outputs": [], "source": [ "analysis_time_1 = datetime.datetime(2018, 9, 7, 0, 0)\n", "analysis_unix_time_1 = datetime.datetime.timestamp(analysis_time_1)\n", "analysis_time_1_str = analysis_time_1.strftime(\"%Y/%m/%d %H%M UTC\")\n", "\n", "analysis_time_2 = datetime.datetime(2018, 9, 11, 12, 0)\n", "analysis_unix_time_2 = datetime.datetime.timestamp(analysis_time_2)\n", "analysis_time_2_str = analysis_time_2.strftime(\"%Y/%m/%d %H%M UTC\")" ] }, { "cell_type": "markdown", "id": "b420513f", "metadata": { "id": "b420513f" }, "source": [ "First, let's look at the list of pressure levels that we can analyze by printing out the `rectilinear_level` variable." ] }, { "cell_type": "code", "execution_count": null, "id": "3e9b7e6f", "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "executionInfo": { "elapsed": 8, "status": "ok", "timestamp": 1686353783983, "user": { "displayName": "Naufal Razin", "userId": "00945304052336708650" }, "user_tz": 360 }, "id": "3e9b7e6f", "outputId": "27c49230-9e92-48f1-a735-65346dfb8e0b" }, "outputs": [], "source": [ "print(rectilinear_level)" ] }, { "cell_type": "markdown", "id": "7a2d2967", "metadata": { "id": "7a2d2967" }, "source": [ "Then, let's create the conditions to extract the latitude, longitude, and relative humidity data at the two corresponding times and single pressure level (let's go with 850 hPa for example)." ] }, { "cell_type": "code", "execution_count": null, "id": "f53e8811", "metadata": { "id": "f53e8811" }, "outputs": [], "source": [ "level_choice = 850\n", "condition_time_1 = rectilinear_time == analysis_unix_time_1\n", "condition_time_2 = rectilinear_time == analysis_unix_time_2\n", "condition_level = rectilinear_level == level_choice" ] }, { "cell_type": "code", "execution_count": null, "id": "71af75c6", "metadata": { "id": "71af75c6" }, "outputs": [], "source": [ "relative_humidity_time_1 = np.squeeze(rectilinear_relative_humidity[condition_time_1, condition_level, :, :], axis=0)\n", "latitude_time_1 = np.squeeze(rectilinear_latitude[condition_time_1, :], axis=0)\n", "longitude_time_1 = np.squeeze(rectilinear_longitude[condition_time_1, :], axis=0)\n", "\n", "relative_humidity_time_2 = np.squeeze(rectilinear_relative_humidity[condition_time_2, condition_level, :, :], axis=0)\n", "latitude_time_2 = np.squeeze(rectilinear_latitude[condition_time_2, :], axis=0)\n", "longitude_time_2 = np.squeeze(rectilinear_longitude[condition_time_2, :], axis=0)" ] }, { "cell_type": "markdown", "id": "97cb83e0", "metadata": { "id": "97cb83e0" }, "source": [ "Finally, let's plot the low-level relative humidity fields at the two different analysis times, side-by-side! We will use [this Cartopy example](https://kpegion.github.io/Pangeo-at-AOES/examples/multi-panel-cartopy.html) to make our two-panel plot, and [this Cartopy example](https://scitools.org.uk/cartopy/docs/latest/matplotlib/gridliner.html) to clean up our plot gridline labels." ] }, { "cell_type": "code", "execution_count": null, "id": "bae1b8e7", "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 591 }, "executionInfo": { "elapsed": 8250, "status": "ok", "timestamp": 1686353792429, "user": { "displayName": "Naufal Razin", "userId": "00945304052336708650" }, "user_tz": 360 }, "id": "bae1b8e7", "outputId": "e962344e-fb72-4db7-a533-907accb3430a" }, "outputs": [], "source": [ "# Define the figure and each axis for the 3 rows and 3 columns\n", "fig, axs = plt.subplots(nrows=1,ncols=2,\n", " subplot_kw={'projection': ccrs.Mercator()},\n", " figsize=(11, 8.5))\n", "\n", "axs=axs.flatten()\n", "\n", "axs[0].coastlines()\n", "gl0 = axs[0].gridlines(draw_labels=True)\n", "gl0.top_labels=False\n", "gl0.xlocator = mticker.FixedLocator(np.arange(-180,181,2.5))\n", "gl0.right_labels=False\n", "gl0.xlocator = mticker.FixedLocator(np.arange(-90,91,2.5))\n", "axs[0].contourf(longitude_time_1, latitude_time_1, relative_humidity_time_1, transform=ccrs.PlateCarree(),\n", " levels=np.arange(0, 101, 5), cmap=\"GnBu\")\n", "axs[0].set_title(str(level_choice) + \" hPa Relative Humidity at\\n\" + analysis_time_1_str)\n", "\n", "axs[1].coastlines()\n", "gl1 = axs[1].gridlines(draw_labels=True)\n", "gl1.top_labels=False\n", "gl1.xlocator = mticker.FixedLocator(np.arange(-180,181,2.5))\n", "gl1.right_labels=False\n", "gl1.xlocator = mticker.FixedLocator(np.arange(-90,91,2.5))\n", "cs = axs[1].contourf(longitude_time_2, latitude_time_2, relative_humidity_time_2, transform=ccrs.PlateCarree(),\n", " levels=np.arange(0, 101, 5), cmap=\"GnBu\")\n", "axs[1].set_title(str(level_choice) + \" hPa Relative Humidity at\\n\" + analysis_time_2_str)\n", "\n", "# Add a colorbar axis at the bottom of the graph\n", "cbar_ax = fig.add_axes([0.2, 0.15, 0.6, 0.02])\n", "\n", "# Draw the colorbar\n", "cbar=fig.colorbar(cs, cax=cbar_ax,orientation='horizontal')" ] }, { "cell_type": "markdown", "id": "4db2e11f", "metadata": { "id": "4db2e11f" }, "source": [ "The plot above shows that at the first analysis time, right at the end of the first weakning of Hurricane Florence, there is a large area of low atmospheric relative humidity at 850 hPa, to the east of the storm center that wrapped around to the northwest of the storm center. This area of low relative humidity likely contributed to the weakening of Hurricane Florence. Conversely, at the peak of Florence's intensity, there are only smaller and discrete areas of low atmospheric relative humidity, with a concentrated area of high relative humidity near the center of the storm." ] }, { "cell_type": "markdown", "id": "axVqGvLGa2bP", "metadata": { "id": "axVqGvLGa2bP" }, "source": [ "## Practice\n", "1. *Estimates* of the minimum central pressure for a tropical cyclone are not always available. But in the case of Hurricane Florence, estimates of the minimum central pressure are available throughout the storm's life. Using the example from Part 2A, plot the evolution of the intensity and minimum central pressure of Hurricane Florence. What kind of relationship do you observe between the intensity and the minimum central pressure?\n", "2. Another factor that affects tropical cyclone intensity is the sea surface temperature over which it is located. Higher sea surface temperatures are more conducive for tropical cyclone intensification. Replicate the relative humidity plot in Part 3 by plotting the sea surface temperature field from the `rectilinear` group at the two different times. Compare the sea surface temperature plot with the relative humidity plot and the evolution of the shear magnitude that you have analyzed in Part 2B. Which of these environmental factors do you think has the biggest impact on the tropical cyclone intensity at the two different times?" ] }, { "cell_type": "markdown", "id": "piOpzHDdTVW7", "metadata": { "id": "piOpzHDdTVW7" }, "source": [ "## Close the File\n", "When loading data from a NetCDF file, **always remember to close the file**. A best practice would be to close the file immediately after loading the variable or attribute of interest. However, since we're loading various variables and attributes throughout this notebook, we will close the file at the end of this notebook using the command below." ] }, { "cell_type": "code", "execution_count": null, "id": "t4C1O6KATXqM", "metadata": { "id": "t4C1O6KATXqM" }, "outputs": [], "source": [ "DS.close()" ] }, { "cell_type": "markdown", "id": "050664e1", "metadata": { "id": "050664e1" }, "source": [ "## Final Thoughts\n", "This tutorial highlights just a subset of the variables available in the TC PRIMED environmental file, including the ways in which you can plot them. To recap, in this tutorial, you learned how to:\n", "- load and plot the storm metadata\n", "- load and plot the storm diagnostics, including a vertical profile of temperature and dewpoint temperature\n", "- load and plot a field variable from ERA5, collocated with the model storm center" ] }, { "cell_type": "markdown", "id": "dad7bbca", "metadata": { "id": "dad7bbca" }, "source": [ "## Data Statement\n", "- Razin, Muhammad Naufal; Slocum, Christopher J.; Knaff, John A.; Brown, Paula J. 2023. Tropical Cyclone PRecipitation, Infrared, Microwave, and Environmental Dataset (TC PRIMED). v01r00. NOAA National Centers for Environmental Information. https://doi.org/10.25921/dmy1-0595." ] }, { "cell_type": "markdown", "id": "faa511a3", "metadata": { "id": "faa511a3" }, "source": [ "## References\n", "- Slocum, C. J., M. N. Razin, J. A. Knaff, and S. P. Stow, 2022: Does ERA5 mark a new era for resolving the tropical cyclone environment? J. Climate., 35, 3547–3564, https://doi.org/10.1175/JCLI-D-22-0127.1." ] }, { "cell_type": "markdown", "id": "e6e561f2", "metadata": { "id": "e6e561f2" }, "source": [ "## Metadata\n", "- Language / package(s)\n", " - Python\n", " - NetCDF\n", " - Matplotlib\n", " - cartopy\n", " - metpy\n", " - numpy\n", "- Domain\n", " - NOAA\n", " - ECMWF\n", "- Application keywords\n", " - Environmental diagnostics\n", " - Reanalysis\n", "- Geophysical keywords\n", " - Tropical cyclones" ] }, { "cell_type": "code", "execution_count": null, "id": "7ddf5062", "metadata": { "id": "7ddf5062" }, "outputs": [], "source": [] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.9.15" }, "toc": { "base_numbering": 1, "nav_menu": {}, "number_sections": true, "sideBar": true, "skip_h1_title": false, "title_cell": "Table of Contents", "title_sidebar": "Contents", "toc_cell": false, "toc_position": {}, "toc_section_display": true, "toc_window_display": true } }, "nbformat": 4, "nbformat_minor": 5 }