{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Sea Ice Comparisons to Observations\n", "\n", "This script shows how to load and plot sea ice concentration from CICE output and compare it to the NSIDC CDR (National Snow and Ice Data Centre, Climate Data Record) dataset\n", "\n", "This notebook uses the _ACCESS-NRI Intake Catalog_ following the examples in [Tutorials/Using the Intake Catalog](https://cosima-recipes.readthedocs.io/en/latest/01-Cooking-Tutorials/01-Basics/02-ACCESS-NRI_Intake_Catalog.html). \n", "\n", "Requirements: The runs analysed here are only in access-nri-intake-catalog version 0.3.1 or newer, and xarray.DataTree introduced in xarray version v2024.10.0. This was tested using `conda/analysis3-25.05` from `/g/data/xp65/public/modules` and a _medium_ instance on _normalsr_ queue (although we recommend a larger instance if making iterative changes). " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**OM2 Experiments:**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "These are the ACCESS-OM2 runs we are going to use, we can compare results from prototype runs forced with ERA5 against normal runs using JRA55do, as [described on the ACCESS_HIVE](https://forum.access-hive.org.au/t/era-5-forced-access-om2-simulations/1103/5). To compare against the observational datasets, we use IAF (Inter-Annual Forcing). _N.B._ The JRA55do runs used here a slightly different to the typical (e.g. _025deg_jra55_iaf_omip2_cycle6_) in the model version used and the timeframes evaluated." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "RUNS = {\n", " \"025deg_era5\": [\"025deg_era5_iaf\"], # (our name: run name(s))\n", " \"025deg_jra55\": [\"025deg_jra55_iaf_era5comparison\"],\n", " \"1deg_era5\": [\"1deg_era5_iaf\"],\n", " \"1deg_jra55\": [\"1deg_jra55_iaf_era5comparison\"],\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We are going to look at Sea Ice Concentration and Sea Ice Volume" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [], "source": [ "VARS = [\"aice_m\", \"hi_m\" ] # ice area fraction or sea ice concentration, ice thickness averaged by grid cell area\n", "VARS_2D = [\"area_t\", \"geolat_t\", \"geolon_t\"]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Observational Data:**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Sea Ice concentration is measured through passive microwave remote sensing. We are going to use the NSIDC CDR Dataset (described at [nsidc.org](https://nsidc.org/data/g02202/versions/4))" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [], "source": [ "OBS_TIME_SLICE = slice(\"1979\", \"2022\")\n", "sh_obs_url = \"https://polarwatch.noaa.gov/erddap/griddap/nsidcG02202v4shmday\"\n", "nh_obs_url = \"https://polarwatch.noaa.gov/erddap/griddap/nsidcG02202v4nhmday\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Load depenencies:**" ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "import intake\n", "from xarray import DataTree, map_over_datasets\n", "\n", "from dask.distributed import Client\n", "\n", "import xarray as xr\n", "import numpy as np\n", "from datetime import timedelta\n", "import cf_xarray as cfxr\n", "import xesmf\n", "\n", "# plotting\n", "import matplotlib.pyplot as plt\n", "import cartopy.crs as ccrs\n", "import cmocean.cm as cmo\n", "import matplotlib.lines as mlines" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A standard way to calculate climatologies. (We start in 1991 as earlier decades are influenced by model spin-up for 0.25deg runs which only start in 1980.)" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [], "source": [ "CLIMAT_TIME_SLICE = slice(\"1991\", \"2020\")\n", "\n", "\n", "def climatology(ds):\n", " return ds.sel(time=CLIMAT_TIME_SLICE).groupby(\"time.month\").mean(\"time\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Start a dask client" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/distributed/node.py:187: UserWarning: Port 8787 is already in use.\n", "Perhaps you already have a cluster running?\n", "Hosting the HTTP server on port 45847 instead\n", " warnings.warn(\n" ] } ], "source": [ "client = Client(threads_per_worker = 1)\n", "\n", "client.dashboard_link" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Open the _ACCESS-NRI Intake Catalog_" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "catalog = intake.cat.access_nri" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Load the ACCESS-OM2 results" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For CICE data in OM2, we need to do some wrangling to make it easier to deal with. This is described in more detail in [DocumentedExamples/SeaIce_Plot_Example](https://cosima-recipes.readthedocs.io/en/latest/02-Easy-Recipes/Sea_Ice_Coordinates.html). Its included in this function:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "def open_by_name(name, vars):\n", " \"\"\"Return a dataset for the requested name and vars\"\"\"\n", "\n", " return (\n", " catalog[name]\n", " .search(variable=vars)\n", " .to_dask(\n", " xarray_open_kwargs={\n", " \"chunks\": -1 ,\n", " \"decode_coords\": False,\n", " },\n", " xarray_combine_by_coords_kwargs={\n", " \"compat\": \"override\",\n", " \"data_vars\": \"minimal\",\n", " \"coords\": \"minimal\",\n", " },\n", " )\n", " )" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "def open_by_experiment(exp_name, vars):\n", " \"\"\"Concatenate any datasets provided for this experiment into one ds, and add area and geo coordinates\"\"\"\n", "\n", " # get the data for each run of this config\n", " cice_ds = xr.concat(\n", " [open_by_name(iName, vars) for iName in RUNS[exp_name]], dim=\"time\"\n", " )\n", "\n", " # We also want the area/lat/lon fields, these are separate datasets, so lets merge them.\n", " area_ds = xr.merge(\n", " [open_by_name(RUNS[exp_name][0], iVar) for iVar in VARS_2D]\n", " ).load()\n", "\n", " # Label the lats and lons\n", " cice_ds.coords[\"ni\"] = area_ds[\"xt_ocean\"].values\n", " cice_ds.coords[\"nj\"] = area_ds[\"yt_ocean\"].values\n", "\n", " # Copy attributes for cf compliance\n", " cice_ds.ni.attrs = area_ds.xt_ocean.attrs\n", " cice_ds.nj.attrs = area_ds.yt_ocean.attrs\n", "\n", " cice_ds = cice_ds.rename(({\"ni\": \"xt_ocean\", \"nj\": \"yt_ocean\"}))\n", "\n", " # Add the geolon, geolat, and area as extra co-ordinates fields from area_t\n", "\n", " cice_ds = cice_ds.assign_coords(\n", " {\n", " \"geolat_t\": area_ds.geolat_t,\n", " \"geolon_t\": area_ds.geolon_t,\n", " \"area_t\": area_ds.area_t,\n", " }\n", " )\n", "\n", " # cice timestamps are also misleading:\n", " cice_ds[\"time\"] = cice_ds.time.to_pandas() - timedelta(minutes=1)\n", "\n", " return cice_ds" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Because the dimensions are different for different experiments, they would not fit in a Dataset, a DataTree is required. The DataTree has a group for each experiment, which contains a xarray dataset with the data for that experiment. " ] }, { "cell_type": "code", "execution_count": 10, "metadata": { "scrolled": true }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", " records = grouped.get_group(internal_key).to_dict(orient='records')\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", " records = grouped.get_group(internal_key).to_dict(orient='records')\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/source.py:306: ConcatenationWarning: Attempting to concatenate datasets without valid dimension coordinates: retaining only first dataset. Request valid dimension coordinate to silence this warning.\n", " warnings.warn(\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", " records = grouped.get_group(internal_key).to_dict(orient='records')\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/source.py:306: ConcatenationWarning: Attempting to concatenate datasets without valid dimension coordinates: retaining only first dataset. Request valid dimension coordinate to silence this warning.\n", " warnings.warn(\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", " records = grouped.get_group(internal_key).to_dict(orient='records')\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/source.py:306: ConcatenationWarning: Attempting to concatenate datasets without valid dimension coordinates: retaining only first dataset. Request valid dimension coordinate to silence this warning.\n", " warnings.warn(\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", " records = grouped.get_group(internal_key).to_dict(orient='records')\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", " records = grouped.get_group(internal_key).to_dict(orient='records')\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/source.py:306: ConcatenationWarning: Attempting to concatenate datasets without valid dimension coordinates: retaining only first dataset. Request valid dimension coordinate to silence this warning.\n", " warnings.warn(\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", " records = grouped.get_group(internal_key).to_dict(orient='records')\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/source.py:306: ConcatenationWarning: Attempting to concatenate datasets without valid dimension coordinates: retaining only first dataset. Request valid dimension coordinate to silence this warning.\n", " warnings.warn(\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", " records = grouped.get_group(internal_key).to_dict(orient='records')\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/source.py:306: ConcatenationWarning: Attempting to concatenate datasets without valid dimension coordinates: retaining only first dataset. Request valid dimension coordinate to silence this warning.\n", " warnings.warn(\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", " records = grouped.get_group(internal_key).to_dict(orient='records')\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", " records = grouped.get_group(internal_key).to_dict(orient='records')\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/source.py:306: ConcatenationWarning: Attempting to concatenate datasets without valid dimension coordinates: retaining only first dataset. Request valid dimension coordinate to silence this warning.\n", " warnings.warn(\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", " records = grouped.get_group(internal_key).to_dict(orient='records')\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/source.py:306: ConcatenationWarning: Attempting to concatenate datasets without valid dimension coordinates: retaining only first dataset. Request valid dimension coordinate to silence this warning.\n", " warnings.warn(\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", " records = grouped.get_group(internal_key).to_dict(orient='records')\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/source.py:306: ConcatenationWarning: Attempting to concatenate datasets without valid dimension coordinates: retaining only first dataset. Request valid dimension coordinate to silence this warning.\n", " warnings.warn(\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", " records = grouped.get_group(internal_key).to_dict(orient='records')\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", " records = grouped.get_group(internal_key).to_dict(orient='records')\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/source.py:306: ConcatenationWarning: Attempting to concatenate datasets without valid dimension coordinates: retaining only first dataset. Request valid dimension coordinate to silence this warning.\n", " warnings.warn(\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", " records = grouped.get_group(internal_key).to_dict(orient='records')\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/source.py:306: ConcatenationWarning: Attempting to concatenate datasets without valid dimension coordinates: retaining only first dataset. Request valid dimension coordinate to silence this warning.\n", " warnings.warn(\n", "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/core.py:301: FutureWarning: When grouping with a length-1 list-like, you will need to pass a length-1 tuple to get_group in a future version of pandas. Pass `(name,)` instead of `name` to silence this warning.\n", " records = grouped.get_group(internal_key).to_dict(orient='records')\n" ] }, { "name": "stdout", "output_type": "stream", "text": [ "CPU times: user 33.8 s, sys: 7.42 s, total: 41.2 s\n", "Wall time: 37.3 s\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "/g/data/xp65/public/apps/med_conda/envs/analysis3-25.08/lib/python3.11/site-packages/intake_esm/source.py:306: ConcatenationWarning: Attempting to concatenate datasets without valid dimension coordinates: retaining only first dataset. Request valid dimension coordinate to silence this warning.\n", " warnings.warn(\n" ] } ], "source": [ "%%time\n", "\n", "si_name_ds_pairs = dict()\n", "for run in RUNS.keys():\n", " si_name_ds_pairs[run] = open_by_experiment(run, VARS)" ] }, { "cell_type": "code", "execution_count": 11, "metadata": { "scrolled": true }, "outputs": [], "source": [ "si_dt = DataTree.from_dict(si_name_ds_pairs)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "def match_timestamps_to_CDR(ds):\n", "\n", " # root dataset in datatree is not used\n", " if ds is None or 'time' not in ds:\n", " return ds\n", "\n", " # make a copy, datatree doesn't allow manipulating in place \n", " cice_ds = ds.copy()\n", "\n", " # we are going to use the same timestamps as NSIDC\n", " cice_ds[\"time\"] = [\n", " np.datetime64(str(i)[0:7] + \"-01T00:00:00.000000000\")\n", " for i in ds.time.values\n", " ]\n", "\n", " return cice_ds" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "si_dt = si_dt.map_over_datasets(match_timestamps_to_CDR)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The result is a datatree, with a dataset for each experiment and timestamps which align with the observational timestamps" ] }, { "cell_type": "code", "execution_count": 14, "metadata": { "scrolled": true }, "outputs": [ { "data": { "text/html": [ "
<xarray.DatasetView> Size: 0B\n",
"Dimensions: ()\n",
"Data variables:\n",
" *empty*