{ "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/neuromatch/climate-course-content/blob/main/tutorials/W1D2_StateoftheClimateOceanandAtmosphereReanalysis/student/W1D2_Tutorial2.ipynb)   \"Open" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "# Tutorial 2: A Lot of Weather Makes Climate - Exploring the ERA5 Reanalysis\n", "\n", "**Week 1, Day 2, Ocean-Atmosphere Reanalysis**\n", "\n", "**Content creators:** Momme Hell\n", "\n", "**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\n", "\n", "**Content editors:** Paul Heubel, Jenna Pearson, Chi Zhang, Ohad Zivan\n", "\n", "**Production editors:** Wesley Banfield, Paul Heubel, Jenna Pearson, Konstantine Tsafatinos, Chi Zhang, Ohad Zivan\n", "\n", "**Our 2024 Sponsors:** NFDI4Earth, CMIP" ] }, { "cell_type": "markdown", "metadata": { "execution": {}, "tags": [] }, "source": [ "# Tutorial Objectives\n", "\n", "*Estimated timing of tutorial:* 25 mins\n", "\n", "In the previous tutorial, we learned about the El Niño Southern Oscillation (ENSO), which is a specific atmosphere-ocean dynamical phenomenon. You will now examine the atmosphere and the ocean systems more generally.\n", "\n", "In this tutorial, you will learn to work with reanalysis data. These data combine observations and models of the Earth system and are a critical tool for weather and climate science. You will first access a specific reanalysis dataset: ECMWF's ERA5. You will then select variables and slices of interest from the preprocessed file, investigating how important climate variables change on medium-length timescales (hours to months) within a certain region.\n", "\n", "By the end of this tutorial, you will be able to:\n", "- Access reanalysis data of climatically important variables. \n", "- Plot interactive maps to explore changes on various time scales.\n", "- Compute and compare time series of different variables from reanalysis data." ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "# Setup" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {}, "tags": [ "colab" ] }, "outputs": [], "source": [ "# installations ( uncomment and run this cell ONLY when using google colab or kaggle )\n", "\n", "# !pip install cartopy\n", "# !pip install geoviews\n", "# !pip install cdsapi" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "import cdsapi\n", "import matplotlib.pyplot as plt\n", "import xarray as xr\n", "import numpy as np\n", "import geoviews as gv\n", "import geoviews.feature as gf\n", "import holoviews\n", "\n", "import os\n", "import pooch\n", "import tempfile\n", "\n", "from cartopy import crs as ccrs\n", "\n", "import warnings\n", "# Suppress warnings issued by Cartopy when downloading data files\n", "warnings.filterwarnings('ignore')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Helper functions\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "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": [ "## Figure Settings\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @title Figure Settings\n", "import ipywidgets as widgets # interactive display\n", "\n", "%config InlineBackend.figure_format = 'retina'\n", "plt.style.use(\n", " \"https://raw.githubusercontent.com/neuromatch/climate-course-content/main/cma.mplstyle\"\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Figure Settings\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "hide-input" ] }, "outputs": [], "source": [ "# @title Figure Settings\n", "import ipywidgets as widgets # interactive display\n", "\n", "%config InlineBackend.figure_format = 'retina'\n", "plt.style.use(\n", " \"https://raw.githubusercontent.com/neuromatch/climate-course-content/main/cma.mplstyle\"\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Video 1: ECMWF Reanalysis\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "cellView": "form", "execution": {}, "tags": [ "remove-input" ] }, "outputs": [], "source": [ "# @title Video 1: ECMWF Reanalysis\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', 'xn_SGxTm6LA'), ('Bilibili', 'BV1g94y1B7Yw')]\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": "markdown", "metadata": { "execution": {} }, "source": [ "# Section 1: What is Reanalysis Data?\n", "\n", "**Reanalysis** refers to the process of combining historical observations from a variety of sources, such as weather stations, satellite measurements, and ocean buoys, with numerical models to create a comprehensive and consistent record of past weather and climate conditions. Reanalysis data is a useful tool to examine the Earth's climate system over a wide range of time scales, from seasonal through decadal to century-scale changes. \n", "\n", "There are multiple Earth system reanalysis products (e.g. MERRA-2, NCEP-NCAR, JRA-55C, [see an extensive list here](https://climatedataguide.ucar.edu/climate-data/atmospheric-reanalysis-overview-comparison-tables)), and no single product fits all needs. For this tutorial, you will be using a product from the European Centre for Medium-Range Weather Forecasts (ECMWF) called **ECMWF Reanalysis v5 (ERA5)**. [This video](https://climate.copernicus.eu/climate-reanalysis) from the ECMWF provides you with a brief introduction to the ERA5 product." ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## Section 1.1: Accessing ERA5 Data\n", "\n", "You will access the data through our OSF cloud storage to simplify the downloading process. If you are keen to download the data yourself or are simply interested in exploring other variables, please have a look into the ```get_ERA5_reanalysis_data.ipynb``` notebook, where we use the Climate Data Store (CDS) API to get a subset of the huge [ECMWF ERA5 Reanalysis](https://cds.climate.copernicus.eu/cdsapp#!/dataset/reanalysis-era5-single-levels?tab=overview) data set.\n", "\n", "Let's select a specific year and month to work with, March of 2018:" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "# load data: 5 variables of ERA5 reanalysis, subregion, hourly, March 2018\n", "fname_ERA5_allvars = \"ERA5_5vars_032018_hourly_NE-US.nc\"\n", "url_ERA5_allvars = \"https://osf.io/7kcwn/download\"\n", "ERA5_allvars = xr.open_dataset(pooch_load(url_ERA5_allvars, fname_ERA5_allvars))" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "ERA5_allvars" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "You just loaded an `xarray` dataset, as introduced on the first day. This dataset contains 5 variables covering the Northeastern United States along with their respective coordinates. With this dataset, you have access to our best estimates of climate parameters with a temporal resolution of 1 hour and a spatial resolution of 1/4 degree (i.e. grid points near the Equator represent a ~25 km x 25 km region). This is a lot of data, but still just a fraction of the data available through the [full ERA5 dataset](https://cds.climate.copernicus.eu/cdsapp#!/dataset/reanalysis-era5-single-levels?tab=overview). \n" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## Section 1.2: Selecting Regions of Interest\n", "The global ERA5 data over the entire time range is so large that even just one variable would be too large to store on your computer. Here we use preprocessed slices of an example region, to load another region (i.e., a spatial subset) of the data, please check out the ```get_ERA5_reanalysis_data.ipynb``` notebook. In this first example, you will load *air surface temperature at 2 meters* data for a small region in the Northeastern United States. In later tutorials, you will have the opportunity to select a region of your choice and explore other climate variables. " ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "The magnitude of the wind vector represents the wind speed \n", "\n", "\\begin{align}\n", "||u|| = \\sqrt{u^2 + v^2}\n", "\\end{align}\n", "\n", "which you will use later in the tutorial for time series comparison and discuss in more detail in Tutorial 4. We will calculate that here and add it to our dataset." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "# compute ten-meter wind speed, the magnitude of the wind vector\n", "ERA5_allvars[\"wind_speed\"] = np.sqrt(\n", " ERA5_allvars[\"u10\"] ** 2\n", " + ERA5_allvars[\"v10\"] ** 2\n", ")\n", "# add name and units to the metadata:\n", "ERA5_allvars[\"wind_speed\"].attrs[\n", " \"long_name\"\n", "] = \"10-meter wind speed\" # assigning the long name to the attributes\n", "ERA5_allvars[\"wind_speed\"].attrs[\"units\"] = \"m/s\" # assigning units\n", "ERA5_allvars" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "# Section 2: Plotting Spatial Maps of Reanalysis Data\n", "First, let's plot the region's surface temperature for the first time step of the reanalysis dataset. To do this let's extract the 2m air temperature data from the dataset that contains all the variables." ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "ds_surface_temp_2m = ERA5_allvars.t2m\n", "ds_surface_temp_2m" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "We will be plotting this a little bit differently than you have previously plotted a map (and differently from how you will plot in most tutorials) so we can look at a few times steps interactively later. To do this we are using the packages [geoviews](https://geoviews.org) and [holoviews](https://holoviews.org/). " ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "holoviews.extension(\"bokeh\")\n", "\n", "dataset_plot = gv.Dataset(ds_surface_temp_2m.isel(time=0)) # select the first time step\n", "\n", "# create the image\n", "images = dataset_plot.to(\n", " gv.Image, [\"longitude\", \"latitude\"], [\"t2m\"], \"hour\"\n", ")\n", "\n", "# aesthetics, add coastlines etc.\n", "images.opts(\n", " cmap=\"coolwarm\",\n", " colorbar=True,\n", " width=600,\n", " height=400,\n", " projection=ccrs.PlateCarree(),\n", " clabel=\"2m Air Temperature (K)\",\n", ") * gf.coastline" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "In the above figure, coastlines are shown as black lines. Most of the selected region is land, with some ocean (lower right) and a lake (top middle).\n", "\n", "Next, we will examine variability at two different frequencies using interactive plots:\n", "\n", "1. **Hourly variability** \n", "2. **Daily variability** \n", "\n", "Note that in the previous tutorial, you computed the monthly variability, or *climatology*, but here you only have one month of data loaded (March 2018). If you are curious about longer timescales you will visit this in the next tutorial!" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {}, "tags": [] }, "outputs": [], "source": [ "# average temperatures over the whole month after grouping by the hour\n", "ds_surface_temp_2m_hour = ds_surface_temp_2m.groupby(\"time.hour\").mean()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {}, "tags": [] }, "outputs": [], "source": [ "# interactive plot of hourly frequency of surface temperature\n", "# this cell may take a little longer as it contains several maps in a single plotting function\n", "dataset_plot = gv.Dataset(\n", " ds_surface_temp_2m_hour.isel(hour=slice(0, 12))\n", ") # only the first 12 time steps (midnight to noon), as it is a time-consuming task\n", "images = dataset_plot.to(\n", " gv.Image, [\"longitude\", \"latitude\"], [\"t2m\"], \"hour\"\n", ")\n", "images.opts(\n", " cmap=\"coolwarm\",\n", " colorbar=True,\n", " width=600,\n", " height=400,\n", " projection=ccrs.PlateCarree(),\n", " clabel=\"2m Air Temperature (K)\",\n", ") * gf.coastline" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "# average temperatures over the whole day after grouping by the day\n", "ds_surface_temp_2m_day = ds_surface_temp_2m.groupby(\"time.day\").mean()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {}, "tags": [] }, "outputs": [], "source": [ "# interactive plot of hourly frequency of surface temperature\n", "# this cell may take a little longer as it contains several maps in a single plotting function\n", "dataset_plot = gv.Dataset(\n", " ds_surface_temp_2m_hour.isel(hour=slice(0, 12))\n", ") # only the first 12 time steps (midnight to noon), as it is a time-consuming task\n", "images = dataset_plot.to(\n", " gv.Image, [\"longitude\", \"latitude\"], [\"t2m\"], \"hour\"\n", ")\n", "images.opts(\n", " cmap=\"coolwarm\",\n", " colorbar=True,\n", " width=600,\n", " height=400,\n", " projection=ccrs.PlateCarree(),\n", " clabel=\"2m Air Temperature (K)\",\n", ") * gf.coastline" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "# average temperatures over the whole day after grouping by the day\n", "ds_surface_temp_2m_day = ds_surface_temp_2m.groupby(\"time.day\").mean()" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "# interactive plot of daily frequency of surface temperature\n", "# this cell may take a little longer as it contains several maps in a single plotting function holoviews.extension('bokeh')\n", "dataset_plot = gv.Dataset(\n", " ds_surface_temp_2m_day.isel(day=slice(0, 10))\n", ") # only the first 10 time steps, as it is a time-consuming task\n", "images = dataset_plot.to(\n", " gv.Image, [\"longitude\", \"latitude\"], [\"t2m\"], \"day\"\n", ")\n", "images.opts(\n", " cmap=\"coolwarm\",\n", " colorbar=True,\n", " width=600,\n", " height=400,\n", " projection=ccrs.PlateCarree(),\n", " clabel=\"2m Air Temperature (K)\",\n", ") * gf.coastline" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "### Question 2\n", "1. What differences do you notice between the hourly and daily interactive plots, and are there any interesting spatial patterns of these temperature changes?" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "execution": {} }, "source": [ "[*Click for solution*](https://github.com/neuromatch/climate-course-content/tree/main/tutorials/W1D2_StateoftheClimateOceanandAtmosphereReanalysis/solutions/W1D2_Tutorial2_Solution_64cd961b.py)\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "# Section 3: Plotting Time Series of Reanalysis Data\n", "\n", "## Section 3.1: Surface Air Temperature Time Series\n", "\n", "You have demonstrated that there are a lot of changes in surface temperature within a day and between days. It is crucial to understand this *temporal variability* in the data when performing climate analysis.\n", "\n", "Rather than plotting interactive spatial maps for different timescales, in this last section, you will create a time series of surface air temperature from the data you have already examined to look at variability on longer than daily timescales. Instead of taking the mean in ***time*** to create *maps*, you will now take the mean in ***space*** to create *time series*.\n", "\n", "*Note that the spatially averaged data will now only have a time coordinate, making it a time series (ts).*" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {}, "tags": [] }, "outputs": [], "source": [ "# find weights (this is a regular grid so we can use cos(latitude))\n", "weights = np.cos(np.deg2rad(ds_surface_temp_2m.latitude))\n", "weights.name = \"weights\"\n", "# take the weighted spatial mean since the latitude range of the region of interest is large\n", "ds_surface_temp_2m_ts = ds_surface_temp_2m.weighted(weights).mean([\"longitude\", \"latitude\"])\n", "ds_surface_temp_2m_ts" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "# plot the time series of surface temperature\n", "fig, ax = plt.subplots()\n", "\n", "ax.plot(ds_surface_temp_2m_ts.time, ds_surface_temp_2m_ts)\n", "\n", "# aesthetics\n", "ax.set_xlabel(\"Time (hours)\")\n", "ax.set_ylabel(\"2m Air \\nTemperature (K)\")\n", "ax.xaxis.set_tick_params(rotation=45)\n", "ax.grid(True)" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "### Questions 3.1\n", "1. What is the dominant source of the high frequency (short timescale) variability? \n", "2. What drives the lower frequency variability? \n", "3. Would the ENSO variablity that you computed in the previous tutorial show up here? Why or why not?" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "execution": {} }, "source": [ "[*Click for solution*](https://github.com/neuromatch/climate-course-content/tree/main/tutorials/W1D2_StateoftheClimateOceanandAtmosphereReanalysis/solutions/W1D2_Tutorial2_Solution_073b07d9.py)\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "## Section 3.2: Comparing Time Series of Multiple Variables\n", "\n", "Below you will calculate the time series of the surface air temperature which we just plotted, alongside the time series of several other ERA5 variables for the same period and region: 10-meter wind speed (```wind_speed```), atmospheric surface pressure (```sp```), and sea surface temperature (```sst```). " ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "ERA5_allvars_ts = ERA5_allvars.weighted(weights).mean([\"longitude\", \"latitude\"])\n", "ERA5_allvars_ts" ] }, { "cell_type": "code", "execution_count": null, "metadata": { "execution": {} }, "outputs": [], "source": [ "plot_vars = [\n", " \"t2m\", # air temperature at 2 meters\n", " \"wind_speed\", # magnitude of the wind vector, cf. Section 1\n", " \"sp\", # surface air pressure\n", " \"sst\", # sea surface temperature\n", "]\n", "\n", "fig, ax_list = plt.subplots(len(plot_vars), 1, sharex=True)\n", "\n", "for var, ax in zip(plot_vars, ax_list): # loop through variables and figure axes\n", " legend_entry = ERA5_allvars[var].attrs[\"long_name\"] # create legend entry\n", " ax.plot(ERA5_allvars_ts.time, ERA5_allvars_ts[var], label=legend_entry) # plot time series\n", "\n", " # aesthetics\n", " ax.set_ylabel(f'{var.capitalize()}\\n({ERA5_allvars[var].attrs[\"units\"]})') # add ylabel w/ units\n", " ax.xaxis.set_tick_params(rotation=45) # rotate dates of xticks\n", " ax.legend(loc='upper center') # add legend with shared location (upper center)" ] }, { "cell_type": "markdown", "metadata": { "execution": {}, "tags": [] }, "source": [ "### Questions 3.2\n", "\n", "Which variable shows variability that is dominated by:\n", "1. The diurnal cycle?\n", "2. The synoptic [~5 day] scale?\n", "3. A mix of these two timescales?\n", "4. Longer timescales?" ] }, { "cell_type": "markdown", "metadata": { "colab_type": "text", "execution": {} }, "source": [ "[*Click for solution*](https://github.com/neuromatch/climate-course-content/tree/main/tutorials/W1D2_StateoftheClimateOceanandAtmosphereReanalysis/solutions/W1D2_Tutorial2_Solution_e28a5729.py)\n", "\n" ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "# Summary\n", "\n", "In this tutorial, you learned how to access and process ERA5 reanalysis data. You are now able to select specific slices within the reanalysis dataset and perform operations such as taking spatial and temporal averages to plot them interactively.\n", "\n", "You also looked at different climate variables to distinguish and identify the variability present at different timescales." ] }, { "cell_type": "markdown", "metadata": { "execution": {} }, "source": [ "# Resources\n", "\n", "Data for this tutorial can be accessed [here](https://cds.climate.copernicus.eu/cdsapp#!/dataset/reanalysis-era5-single-levels?tab=overview). We summarized the download procedure in a separate notebook named ```get_ERA5_reanalysis_data.ipynb```." ] } ], "metadata": { "colab": { "collapsed_sections": [], "include_colab_link": true, "name": "W1D2_Tutorial2", "provenance": [], "toc_visible": true }, "kernel": { "display_name": "Python 3", "language": "python", "name": "python3" }, "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.19" }, "toc-autonumbering": true }, "nbformat": 4, "nbformat_minor": 4 }