From 820a68f50b737ab66e68c20d015193cf79b9e311 Mon Sep 17 00:00:00 2001 From: Tom Vo Date: Thu, 22 Aug 2024 10:19:57 -0700 Subject: [PATCH] CDAT Migration Phase 2: Refactor `enso_diags` set (#832) --- .../regression-test-json.ipynb | 370 ++++ .../regression-test-netcdf.ipynb | 712 ++++++++ .../662-area-mean-time-series-rerun/run.cfg | 140 ++ .../run_script.py | 12 + .../663-enso-diags/qa.py | 64 + .../663-enso-diags/regression_test.ipynb | 1593 +++++++++++++++++ .../663-enso-diags/regression_test_png.ipynb | 339 ++++ .../663-enso-diags/run.cfg | 140 ++ .../663-enso-diags/run_script.py | 14 + e3sm_diags/driver/enso_diags_driver.py | 890 +++++---- e3sm_diags/driver/qbo_driver.py | 6 +- e3sm_diags/driver/utils/dataset_xr.py | 138 +- e3sm_diags/driver/utils/io.py | 42 +- e3sm_diags/plot/cartopy/enso_diags_plot.py | 463 ----- e3sm_diags/plot/enso_diags_plot.py | 379 ++++ e3sm_diags/plot/utils.py | 83 +- .../driver/utils/test_dataset_xr.py | 81 +- 17 files changed, 4507 insertions(+), 959 deletions(-) create mode 100644 auxiliary_tools/cdat_regression_testing/663-enso-diags/662-area-mean-time-series-rerun/regression-test-json.ipynb create mode 100644 auxiliary_tools/cdat_regression_testing/663-enso-diags/662-area-mean-time-series-rerun/regression-test-netcdf.ipynb create mode 100644 auxiliary_tools/cdat_regression_testing/663-enso-diags/662-area-mean-time-series-rerun/run.cfg create mode 100644 auxiliary_tools/cdat_regression_testing/663-enso-diags/662-area-mean-time-series-rerun/run_script.py create mode 100644 auxiliary_tools/cdat_regression_testing/663-enso-diags/qa.py create mode 100644 auxiliary_tools/cdat_regression_testing/663-enso-diags/regression_test.ipynb create mode 100644 auxiliary_tools/cdat_regression_testing/663-enso-diags/regression_test_png.ipynb create mode 100644 auxiliary_tools/cdat_regression_testing/663-enso-diags/run.cfg create mode 100644 auxiliary_tools/cdat_regression_testing/663-enso-diags/run_script.py delete mode 100644 e3sm_diags/plot/cartopy/enso_diags_plot.py create mode 100644 e3sm_diags/plot/enso_diags_plot.py diff --git a/auxiliary_tools/cdat_regression_testing/663-enso-diags/662-area-mean-time-series-rerun/regression-test-json.ipynb b/auxiliary_tools/cdat_regression_testing/663-enso-diags/662-area-mean-time-series-rerun/regression-test-json.ipynb new file mode 100644 index 000000000..cc27db393 --- /dev/null +++ b/auxiliary_tools/cdat_regression_testing/663-enso-diags/662-area-mean-time-series-rerun/regression-test-json.ipynb @@ -0,0 +1,370 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# CDAT Migration Regression Testing Notebook (`.json` metrics)\n", + "\n", + "This notebook is used to perform regression testing between the development and\n", + "production versions of a diagnostic set.\n", + "\n", + "## How it works\n", + "\n", + "It compares the relative differences (%) between two sets of `.json` files in two\n", + "separate directories, one for the refactored code and the other for the `main` branch.\n", + "\n", + "It will display metrics values with relative differences >= 2%. Relative differences are used instead of absolute differences because:\n", + "\n", + "- Relative differences are in percentages, which shows the scale of the differences.\n", + "- Absolute differences are just a raw number that doesn't factor in\n", + " floating point size (e.g., 100.00 vs. 0.0001), which can be misleading.\n", + "\n", + "## How to use\n", + "\n", + "PREREQUISITE: The diagnostic set's metrics stored in `.json` files in two directories\n", + "(dev and `main` branches).\n", + "\n", + "1. Make a copy of this notebook under `auxiliary_tools/cdat_regression_testing/`.\n", + "2. Run `mamba create -n cdat_regression_test -y -c conda-forge \"python<3.12\" xarray netcdf4 dask pandas matplotlib-base ipykernel`\n", + "3. Run `mamba activate cdat_regression_test`\n", + "4. Update `DEV_PATH` and `MAIN_PATH` in the copy of your notebook.\n", + "5. Run all cells IN ORDER.\n", + "6. Review results for any outstanding differences (>= 2%).\n", + " - Debug these differences (e.g., bug in metrics functions, incorrect variable references, etc.)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup Code\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import glob\n", + "from typing import List\n", + "\n", + "import pandas as pd\n", + "\n", + "from auxiliary_tools.cdat_regression_testing.utils import (\n", + " get_num_metrics_above_diff_thres,\n", + " get_rel_diffs,\n", + " update_diffs_to_pct,\n", + " highlight_large_diffs,\n", + ")\n", + "\n", + "SET_NAME = \"area_mean_time_series\"\n", + "SET_DIR = \"663-area-mean-time-series-rerun\"\n", + "\n", + "DEV_PATH = f\"/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/{SET_DIR}/{SET_NAME}/**\"\n", + "MAIN_PATH = f\"/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/{SET_NAME}/**\"\n", + "\n", + "DEV_GLOB = sorted(glob.glob(DEV_PATH + \"/*.json\"))\n", + "MAIN_GLOB = sorted(glob.glob(MAIN_PATH + \"/*.json\"))\n", + "\n", + "if len(DEV_GLOB) == 0 or len(MAIN_GLOB) == 0:\n", + " raise IOError(\"No files found at DEV_PATH and/or MAIN_PATH.\")\n", + "\n", + "if len(DEV_GLOB) != len(MAIN_GLOB):\n", + " raise IOError(\"Number of files do not match at DEV_PATH and MAIN_PATH.\")" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def get_metrics(filepaths: List[str]) -> pd.DataFrame:\n", + " \"\"\"Get the metrics using a glob of `.json` metric files in a directory.\n", + "\n", + " Parameters\n", + " ----------\n", + " filepaths : List[str]\n", + " The filepaths for metrics `.json` files.\n", + "\n", + " Returns\n", + " -------\n", + " pd.DataFrame\n", + " The DataFrame containing the metrics for all of the variables in\n", + " the results directory.\n", + " \"\"\"\n", + " metrics = []\n", + "\n", + " for filepath in filepaths:\n", + " df = pd.read_json(filepath)\n", + "\n", + " filename = filepath.split(\"/\")[-1]\n", + " var_key = filename.split(\"-\")[0]\n", + " region = filename.split(\"-\")[-1]\n", + "\n", + " # Add the variable key to the MultiIndex and update the index\n", + " # before stacking to make the DataFrame easier to parse.\n", + " multiindex = pd.MultiIndex.from_product([[var_key], [region], [*df.index]])\n", + " df = df.set_index(multiindex)\n", + " df.stack()\n", + "\n", + " metrics.append(df)\n", + "\n", + " df_final = pd.concat(metrics)\n", + "\n", + " return df_final" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "metrics = []\n", + "\n", + "for filepath in DEV_GLOB:\n", + " df = pd.read_json(filepath)\n", + "\n", + " filename = filepath.split(\"/\")[-1]\n", + " var_key = filename.split(\"-\")[0]\n", + "\n", + " # Add the variable key to the MultiIndex and update the index\n", + " # before stacking to make the DataFrame easier to parse.\n", + " multiindex = pd.MultiIndex.from_product([[var_key], [filename], [*df.index]])\n", + " df = df.set_index(multiindex)\n", + " df.stack()\n", + "\n", + " metrics.append(df)\n", + "\n", + "df_final = pd.concat(metrics)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Get the metrics for the development and `main` branches and their differences.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "df_metrics_dev = get_metrics(DEV_GLOB)\n", + "df_metrics_main = get_metrics(MAIN_GLOB)\n", + "df_metrics_diffs = get_rel_diffs(df_metrics_dev, df_metrics_main)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Filter differences to those above maximum threshold (2%).\n", + "\n", + "All values below maximum threshold will be labeled as `NaN`.\n", + "\n", + "- **If all cells in a row are NaN (< 2%)**, the entire row is dropped to make the results easier to parse.\n", + "- Any remaining NaN cells are below < 2% difference and **should be ignored**.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "df_metrics_diffs_thres = df_metrics_diffs[df_metrics_diffs >= 0.02]\n", + "df_metrics_diffs_thres = df_metrics_diffs_thres.dropna(\n", + " axis=0, how=\"all\", ignore_index=False\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 3. Combine all DataFrames to get the final result.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "df_metrics_all = pd.concat(\n", + " [df_metrics_dev.add_suffix(\"_dev\"), df_metrics_main.add_suffix(\"_main\")],\n", + " axis=1,\n", + " join=\"outer\",\n", + ")\n", + "df_final = df_metrics_diffs_thres.join(df_metrics_all)\n", + "df_final = update_diffs_to_pct(df_final, [\"e3sm_v2 (0051-0060) DIFF (%)\"])" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
e3sm_v2 (0051-0060) DIFF (%)e3sm_v2 (0051-0060)_deve3sm_v2 (0051-0060)_main
\n", + "
" + ], + "text/plain": [ + "Empty DataFrame\n", + "Columns: [e3sm_v2 (0051-0060) DIFF (%), e3sm_v2 (0051-0060)_dev, e3sm_v2 (0051-0060)_main]\n", + "Index: []" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df_final" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Review variables and metrics above difference threshold.\n", + "\n", + "- Red cells are differences >= 2%\n", + "- `nan` cells are differences < 2% and **should be ignored**\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "* Related variables []\n", + "* Number of metrics above 2% max threshold: 0 / 720\n" + ] + } + ], + "source": [ + "df_final_adj = df_final.reset_index(names=[\"var_key\", \"region\", \"metric\"])\n", + "get_num_metrics_above_diff_thres(df_metrics_all, df_final_adj)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
 var_keyregionmetrice3sm_v2 (0051-0060) DIFF (%)e3sm_v2 (0051-0060)_deve3sm_v2 (0051-0060)_main
\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "highlight_large_diffs(df_final_adj, [\"e3sm_v2 (0051-0060) DIFF (%)\"])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Results\n", + "\n", + "- 792 / 792 metrics are within diff tolerance\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cdat_regression_test", + "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.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/auxiliary_tools/cdat_regression_testing/663-enso-diags/662-area-mean-time-series-rerun/regression-test-netcdf.ipynb b/auxiliary_tools/cdat_regression_testing/663-enso-diags/662-area-mean-time-series-rerun/regression-test-netcdf.ipynb new file mode 100644 index 000000000..11714aecf --- /dev/null +++ b/auxiliary_tools/cdat_regression_testing/663-enso-diags/662-area-mean-time-series-rerun/regression-test-netcdf.ipynb @@ -0,0 +1,712 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# CDAT Migration Regression Testing Notebook (`.nc` files)\n", + "\n", + "This notebook is used to perform regression testing between the development and\n", + "production versions of a diagnostic set.\n", + "\n", + "## How it works\n", + "\n", + "It compares the relative differences (%) between ref and test variables between\n", + "the dev and `main` branches.\n", + "\n", + "## How to use\n", + "\n", + "PREREQUISITE: The diagnostic set's netCDF stored in `.json` files in two directories\n", + "(dev and `main` branches).\n", + "\n", + "1. Make a copy of this notebook under `auxiliary_tools/cdat_regression_testing/`.\n", + "2. Run `mamba create -n cdat_regression_test -y -c conda-forge \"python<3.12\" xarray netcdf4 dask pandas matplotlib-base ipykernel`\n", + "3. Run `mamba activate cdat_regression_test`\n", + "4. Update `SET_DIR` and `SET_NAME` in the copy of your notebook.\n", + "5. Run all cells IN ORDER.\n", + "6. Review results for any outstanding differences (>=1e-5 relative tolerance).\n", + " - Debug these differences (e.g., bug in metrics functions, incorrect variable references, etc.)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup Code\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from collections import defaultdict\n", + "import glob\n", + "from typing import Tuple\n", + "\n", + "import numpy as np\n", + "import xarray as xr\n", + "from e3sm_diags.derivations.derivations import DERIVED_VARIABLES\n", + "\n", + "\n", + "# TODO: Update SET_NAME and SET_DIR\n", + "SET_NAME = \"area_mean_time_series\"\n", + "SET_DIR = \"663-area-mean-time-series-rerun\"\n", + "\n", + "DEV_PATH = f\"/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/{SET_DIR}/{SET_NAME}/**\"\n", + "DEV_GLOB = sorted(glob.glob(DEV_PATH + \"/*.nc\"))\n", + "DEV_NUM_FILES = len(DEV_GLOB)\n", + "\n", + "MAIN_PATH = f\"/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/{SET_NAME}/**\"\n", + "MAIN_GLOB = sorted(glob.glob(MAIN_PATH + \"/*.nc\"))\n", + "MAIN_NUM_FILES = len(MAIN_GLOB)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def _check_if_files_found():\n", + " if DEV_NUM_FILES == 0 or MAIN_NUM_FILES == 0:\n", + " raise IOError(\n", + " \"No files found at DEV_PATH and/or MAIN_PATH. \"\n", + " f\"Please check {DEV_PATH} and {MAIN_PATH}.\"\n", + " )\n", + "\n", + "\n", + "def _check_if_matching_filecount():\n", + " if DEV_NUM_FILES != MAIN_NUM_FILES:\n", + " raise IOError(\n", + " \"Number of files do not match at DEV_PATH and MAIN_PATH \"\n", + " f\"({DEV_NUM_FILES} vs. {MAIN_NUM_FILES}).\"\n", + " )\n", + "\n", + " print(f\"Matching file count ({DEV_NUM_FILES} and {MAIN_NUM_FILES}).\")\n", + "\n", + "\n", + "def _check_if_missing_files():\n", + " missing_count = 0\n", + "\n", + " for fp_main in MAIN_GLOB:\n", + " fp_dev = fp_main.replace(SET_DIR, \"main-area-mean-time-series\")\n", + "\n", + " if fp_dev not in MAIN_GLOB:\n", + " print(f\"No production file found to compare with {fp_dev}!\")\n", + " missing_count += 1\n", + "\n", + " for fp_dev in DEV_GLOB:\n", + " fp_main = fp_main.replace(\"main-area-mean-time-series\", SET_DIR)\n", + "\n", + " if fp_main not in DEV_GLOB:\n", + " print(f\"No development file found to compare with {fp_main}!\")\n", + " missing_count += 1\n", + "\n", + " print(f\"Number of files missing: {missing_count}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def _get_relative_diffs():\n", + " # We are mainly focusing on relative tolerance here (in percentage terms).\n", + " atol = 0\n", + " rtol = 1e-5\n", + "\n", + " for fp_main in MAIN_GLOB:\n", + " if \"test.nc\" in fp_main or \"ref.nc\" in fp_main:\n", + " fp_dev = fp_main.replace(\"main-area-mean-time-series\", SET_DIR)\n", + "\n", + " print(\"Comparing:\")\n", + " print(f\" * {fp_dev}\")\n", + " print(f\" * {fp_main}\")\n", + "\n", + " ds1 = xr.open_dataset(fp_dev)\n", + " ds2 = xr.open_dataset(fp_main)\n", + "\n", + " var_key = fp_main.split(\"-\")[-3]\n", + " # for 3d vars such as T-200\n", + " var_key.isdigit()\n", + " if var_key.isdigit():\n", + " var_key = fp_main.split(\"-\")[-4]\n", + "\n", + " print(f\" * var_key: {var_key}\")\n", + "\n", + " dev_data = _get_var_data(ds1, var_key)\n", + " main_data = _get_var_data(ds2, var_key)\n", + "\n", + " if dev_data is None or main_data is None:\n", + " print(\" * Could not find variable key in the dataset(s)\")\n", + " continue\n", + "\n", + " try:\n", + " np.testing.assert_allclose(\n", + " dev_data,\n", + " main_data,\n", + " atol=atol,\n", + " rtol=rtol,\n", + " )\n", + " except (KeyError, AssertionError) as e:\n", + " print(f\" {e}\")\n", + " else:\n", + " print(f\" * All close and within relative tolerance ({rtol})\")\n", + "\n", + "\n", + "def _get_var_data(ds: xr.Dataset, var_key: str) -> np.ndarray:\n", + " \"\"\"Get the variable data using a list of matching keys.\n", + "\n", + " The `main` branch saves the dataset using the original variable name,\n", + " while the dev branch saves the variable with the derived variable name.\n", + " The dev branch is performing the expected behavior here.\n", + "\n", + " Parameters\n", + " ----------\n", + " ds : xr.Dataset\n", + " _description_\n", + " var_key : str\n", + " _description_\n", + "\n", + " Returns\n", + " -------\n", + " np.ndarray\n", + " _description_\n", + " \"\"\"\n", + "\n", + " data = None\n", + "\n", + " var_keys = DERIVED_VARIABLES[var_key].keys()\n", + " var_keys = [var_key] + list(sum(var_keys, ()))\n", + "\n", + " for key in var_keys:\n", + " if key in ds.data_vars.keys():\n", + " data = ds[key].values\n", + " break\n", + "\n", + " return data" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Check for matching and equal number of files\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "_check_if_files_found()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Matching file count (72 and 72).\n" + ] + } + ], + "source": [ + "_check_if_matching_filecount()" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of files missing: 0\n" + ] + } + ], + "source": [ + "_check_if_missing_files()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2 Compare the netCDF files between branches\n", + "\n", + "- Compare \"ref\" and \"test\" files\n", + "- \"diff\" files are ignored because getting relative diffs for these does not make sense (relative diff will be above tolerance)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/FLUT/FLUT-20N50N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/FLUT/FLUT-20N50N_test.nc\n", + "var_key FLUT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/FLUT/FLUT-20S20N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/FLUT/FLUT-20S20N_test.nc\n", + "var_key FLUT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/FLUT/FLUT-50N90N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/FLUT/FLUT-50N90N_test.nc\n", + "var_key FLUT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/FLUT/FLUT-50S20S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/FLUT/FLUT-50S20S_test.nc\n", + "var_key FLUT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/FLUT/FLUT-90S50S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/FLUT/FLUT-90S50S_test.nc\n", + "var_key FLUT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/FLUT/FLUT-global_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/FLUT/FLUT-global_test.nc\n", + "var_key FLUT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/FLUT/FLUT-land_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/FLUT/FLUT-land_test.nc\n", + "var_key FLUT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/FLUT/FLUT-ocean_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/FLUT/FLUT-ocean_test.nc\n", + "var_key FLUT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/FSNTOA/FSNTOA-20N50N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/FSNTOA/FSNTOA-20N50N_test.nc\n", + "var_key FSNTOA\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/FSNTOA/FSNTOA-20S20N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/FSNTOA/FSNTOA-20S20N_test.nc\n", + "var_key FSNTOA\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/FSNTOA/FSNTOA-50N90N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/FSNTOA/FSNTOA-50N90N_test.nc\n", + "var_key FSNTOA\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/FSNTOA/FSNTOA-50S20S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/FSNTOA/FSNTOA-50S20S_test.nc\n", + "var_key FSNTOA\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/FSNTOA/FSNTOA-90S50S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/FSNTOA/FSNTOA-90S50S_test.nc\n", + "var_key FSNTOA\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/FSNTOA/FSNTOA-global_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/FSNTOA/FSNTOA-global_test.nc\n", + "var_key FSNTOA\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/FSNTOA/FSNTOA-land_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/FSNTOA/FSNTOA-land_test.nc\n", + "var_key FSNTOA\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/FSNTOA/FSNTOA-ocean_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/FSNTOA/FSNTOA-ocean_test.nc\n", + "var_key FSNTOA\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/LHFLX/LHFLX-20N50N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/LHFLX/LHFLX-20N50N_test.nc\n", + "var_key LHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/LHFLX/LHFLX-20S20N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/LHFLX/LHFLX-20S20N_test.nc\n", + "var_key LHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/LHFLX/LHFLX-50N90N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/LHFLX/LHFLX-50N90N_test.nc\n", + "var_key LHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/LHFLX/LHFLX-50S20S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/LHFLX/LHFLX-50S20S_test.nc\n", + "var_key LHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/LHFLX/LHFLX-90S50S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/LHFLX/LHFLX-90S50S_test.nc\n", + "var_key LHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/LHFLX/LHFLX-global_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/LHFLX/LHFLX-global_test.nc\n", + "var_key LHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/LHFLX/LHFLX-land_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/LHFLX/LHFLX-land_test.nc\n", + "var_key LHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/LHFLX/LHFLX-ocean_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/LHFLX/LHFLX-ocean_test.nc\n", + "var_key LHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/LWCF/LWCF-20N50N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/LWCF/LWCF-20N50N_test.nc\n", + "var_key LWCF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/LWCF/LWCF-20S20N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/LWCF/LWCF-20S20N_test.nc\n", + "var_key LWCF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/LWCF/LWCF-50N90N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/LWCF/LWCF-50N90N_test.nc\n", + "var_key LWCF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/LWCF/LWCF-50S20S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/LWCF/LWCF-50S20S_test.nc\n", + "var_key LWCF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/LWCF/LWCF-90S50S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/LWCF/LWCF-90S50S_test.nc\n", + "var_key LWCF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/LWCF/LWCF-global_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/LWCF/LWCF-global_test.nc\n", + "var_key LWCF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/LWCF/LWCF-land_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/LWCF/LWCF-land_test.nc\n", + "var_key LWCF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/LWCF/LWCF-ocean_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/LWCF/LWCF-ocean_test.nc\n", + "var_key LWCF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/PRECT/PRECT-20N50N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/PRECT/PRECT-20N50N_test.nc\n", + "var_key PRECT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/PRECT/PRECT-20S20N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/PRECT/PRECT-20S20N_test.nc\n", + "var_key PRECT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/PRECT/PRECT-50N90N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/PRECT/PRECT-50N90N_test.nc\n", + "var_key PRECT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/PRECT/PRECT-50S20S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/PRECT/PRECT-50S20S_test.nc\n", + "var_key PRECT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/PRECT/PRECT-90S50S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/PRECT/PRECT-90S50S_test.nc\n", + "var_key PRECT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/PRECT/PRECT-global_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/PRECT/PRECT-global_test.nc\n", + "var_key PRECT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/PRECT/PRECT-land_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/PRECT/PRECT-land_test.nc\n", + "var_key PRECT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/PRECT/PRECT-ocean_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/PRECT/PRECT-ocean_test.nc\n", + "var_key PRECT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/QFLX/QFLX-20N50N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/QFLX/QFLX-20N50N_test.nc\n", + "var_key QFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/QFLX/QFLX-20S20N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/QFLX/QFLX-20S20N_test.nc\n", + "var_key QFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/QFLX/QFLX-50N90N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/QFLX/QFLX-50N90N_test.nc\n", + "var_key QFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/QFLX/QFLX-50S20S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/QFLX/QFLX-50S20S_test.nc\n", + "var_key QFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/QFLX/QFLX-90S50S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/QFLX/QFLX-90S50S_test.nc\n", + "var_key QFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/QFLX/QFLX-global_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/QFLX/QFLX-global_test.nc\n", + "var_key QFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/QFLX/QFLX-land_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/QFLX/QFLX-land_test.nc\n", + "var_key QFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/QFLX/QFLX-ocean_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/QFLX/QFLX-ocean_test.nc\n", + "var_key QFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/SHFLX/SHFLX-20N50N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/SHFLX/SHFLX-20N50N_test.nc\n", + "var_key SHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/SHFLX/SHFLX-20S20N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/SHFLX/SHFLX-20S20N_test.nc\n", + "var_key SHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/SHFLX/SHFLX-50N90N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/SHFLX/SHFLX-50N90N_test.nc\n", + "var_key SHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/SHFLX/SHFLX-50S20S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/SHFLX/SHFLX-50S20S_test.nc\n", + "var_key SHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/SHFLX/SHFLX-90S50S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/SHFLX/SHFLX-90S50S_test.nc\n", + "var_key SHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/SHFLX/SHFLX-global_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/SHFLX/SHFLX-global_test.nc\n", + "var_key SHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/SHFLX/SHFLX-land_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/SHFLX/SHFLX-land_test.nc\n", + "var_key SHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/SHFLX/SHFLX-ocean_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/SHFLX/SHFLX-ocean_test.nc\n", + "var_key SHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/SWCF/SWCF-20N50N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/SWCF/SWCF-20N50N_test.nc\n", + "var_key SWCF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/SWCF/SWCF-20S20N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/SWCF/SWCF-20S20N_test.nc\n", + "var_key SWCF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/SWCF/SWCF-50N90N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/SWCF/SWCF-50N90N_test.nc\n", + "var_key SWCF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/SWCF/SWCF-50S20S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/SWCF/SWCF-50S20S_test.nc\n", + "var_key SWCF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/SWCF/SWCF-90S50S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/SWCF/SWCF-90S50S_test.nc\n", + "var_key SWCF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/SWCF/SWCF-global_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/SWCF/SWCF-global_test.nc\n", + "var_key SWCF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/SWCF/SWCF-land_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/SWCF/SWCF-land_test.nc\n", + "var_key SWCF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/SWCF/SWCF-ocean_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/SWCF/SWCF-ocean_test.nc\n", + "var_key SWCF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/TREFHT/TREFHT-20N50N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/TREFHT/TREFHT-20N50N_test.nc\n", + "var_key TREFHT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/TREFHT/TREFHT-20S20N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/TREFHT/TREFHT-20S20N_test.nc\n", + "var_key TREFHT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/TREFHT/TREFHT-50N90N_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/TREFHT/TREFHT-50N90N_test.nc\n", + "var_key TREFHT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/TREFHT/TREFHT-50S20S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/TREFHT/TREFHT-50S20S_test.nc\n", + "var_key TREFHT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/TREFHT/TREFHT-90S50S_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/TREFHT/TREFHT-90S50S_test.nc\n", + "var_key TREFHT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/TREFHT/TREFHT-global_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/TREFHT/TREFHT-global_test.nc\n", + "var_key TREFHT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/TREFHT/TREFHT-land_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/TREFHT/TREFHT-land_test.nc\n", + "var_key TREFHT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + "/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun/area_mean_time_series/TREFHT/TREFHT-ocean_test.nc \n", + " /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main-area-mean-time-series/area_mean_time_series/TREFHT/TREFHT-ocean_test.nc\n", + "var_key TREFHT\n", + " * All close and within relative tolerance (1e-05)\n" + ] + } + ], + "source": [ + "# We are mainly focusing on relative tolerance here (in percentage terms).\n", + "atol = 0\n", + "rtol = 1e-5\n", + "\n", + "count_mismatch = 0\n", + "\n", + "for filepath_dev in DEV_GLOB:\n", + " if \"test.nc\" in filepath_dev or \"ref.nc\" in filepath_dev:\n", + " filepath_main = filepath_dev.replace(SET_DIR, \"main-area-mean-time-series\")\n", + " print(\"Comparing:\")\n", + " print(filepath_dev, \"\\n\", filepath_main)\n", + " ds1 = xr.open_dataset(filepath_dev)\n", + " ds2 = xr.open_dataset(filepath_main)\n", + "\n", + " var_key = filepath_dev.split(\"/\")[-2]\n", + " # for 3d vars such as T-200\n", + " var_key.isdigit()\n", + " if var_key.isdigit():\n", + " var_key = filepath_dev.split(\"-\")[-4]\n", + "\n", + " print(\"var_key\", var_key)\n", + " try:\n", + " np.testing.assert_allclose(\n", + " ds1[var_key].values,\n", + " ds2[var_key].values,\n", + " atol=atol,\n", + " rtol=rtol,\n", + " )\n", + " except AssertionError as e:\n", + " print(e)\n", + " count_mismatch += 1\n", + " else:\n", + " print(f\" * All close and within relative tolerance ({rtol})\")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "0" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "count_mismatch" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Results\n", + "\n", + "All files are within rtol 1e-5, so the changes should be good to go.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cdat_regression_test", + "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.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/auxiliary_tools/cdat_regression_testing/663-enso-diags/662-area-mean-time-series-rerun/run.cfg b/auxiliary_tools/cdat_regression_testing/663-enso-diags/662-area-mean-time-series-rerun/run.cfg new file mode 100644 index 000000000..85b50434c --- /dev/null +++ b/auxiliary_tools/cdat_regression_testing/663-enso-diags/662-area-mean-time-series-rerun/run.cfg @@ -0,0 +1,140 @@ +[#] +sets = ["enso_diags"] +case_id = "PRECT-response" +variables = ["PRECT"] +ref_name = "GPCP_v2.3" +reference_name = "GPCP_v2.3" +regions = ["20S20N"] +seasons = ["ANN"] +reference_colormap = "diverging_bwr.rgb" +test_colormap = "diverging_bwr.rgb" +diff_colormap = "BrBG" +contour_levels = [-5,-3,-2,-1,-0.5,-0.2,0.2,0.5,1,2,3,5] +diff_levels = [-5,-3,-2,-1,-0.5,-0.2,0.2,0.5,1,2,3,5] + + +[#] +sets = ["enso_diags"] +case_id = "TAUX-response" +variables = ["TAUX"] +ref_name = "ERA-Interim" +reference_name = "ERA-Interim" +regions = ["20S20N"] +seasons = ["ANN"] +reference_colormap = "diverging_bwr.rgb" +test_colormap = "diverging_bwr.rgb" +diff_colormap = "BrBG" +contour_levels = [-0.1,-0.05,-0.02, -0.01,-0.005,-0.002, 0.002,0.005 ,0.01,0.02,0.05,0.1] +diff_levels = [-0.1,-0.05,-0.02, -0.01,-0.005,-0.002, 0.002,0.005, 0.01,0.02,0.05,0.1] + + +[#] +sets = ["enso_diags"] +case_id = "TAUY-response" +variables = ["TAUY"] +ref_name = "ERA-Interim" +reference_name = "ERA-Interim" +regions = ["20S20N"] +seasons = ["ANN"] +reference_colormap = "diverging_bwr.rgb" +test_colormap = "diverging_bwr.rgb" +diff_colormap = "BrBG" +contour_levels = [-0.05,-0.02, -0.01,-0.005,-0.002, 0.002,0.005 ,0.01,0.02,0.05] +diff_levels = [-0.05,-0.02, -0.01,-0.005,-0.002, 0.002,0.005, 0.01,0.02,0.05] + +[#] +sets = ["enso_diags"] +case_id = "LHFLX-response" +variables = ["LHFLX"] +ref_name = "ERA-Interim" +reference_name = "ERA-Interim" +regions = ["20S20N"] +seasons = ["ANN"] +reference_colormap = "diverging_bwr.rgb" +test_colormap = "diverging_bwr.rgb" +diff_colormap = "BrBG" +contour_levels = [-30, -25, -20,-15,-10,-5,-2.5,2.5, 5,10,15,20,25,30] +diff_levels = [-30, -25, -20,-15,-10,-5,-2.5,2.5, 5,10,15,20,25,30] + + +[#] +sets = ["enso_diags"] +case_id = "SHFLX-response" +variables = ["SHFLX"] +ref_name = "ERA-Interim" +reference_name = "ERA-Interim" +regions = ["20S20N"] +seasons = ["ANN"] +reference_colormap = "diverging_bwr.rgb" +test_colormap = "diverging_bwr.rgb" +diff_colormap = "BrBG" +contour_levels = [-15,-10,-5,-2.5,-1,-0.5,0.5,1,2.5, 5,10,15] +diff_levels = [-15,-10,-5,-2.5,-1,-0.5,0.5,1,2.5, 5,10,15] + +[#] +sets = ["enso_diags"] +case_id = "NET_FLUX_SRF-response" +variables = ["NET_FLUX_SRF"] +ref_name = "ERA-Interim" +reference_name = "ERA-Interim" +regions = ["20S20N"] +seasons = ["ANN"] +reference_colormap = "diverging_bwr.rgb" +test_colormap = "diverging_bwr.rgb" +diff_colormap = "BrBG" +contour_levels = [-30, -25, -20,-15,-10,-5,-2.5,2.5, 5,10,15,20,25,30] +diff_levels = [-30, -25, -20,-15,-10,-5,-2.5,2.5, 5,10,15,20,25,30] + +# [#] +# sets = ["enso_diags"] +# plot_type = "scatter" +# case_id = "TAUX-feedback" +# variables = ["TAUX"] +# seasons = ["ANN"] +# ref_name = "ERA-Interim" +# reference_name = "ERA-Interim" + +# [#] +# sets = ["enso_diags"] +# plot_type = "scatter" +# case_id = "LHFLX-feedback" +# variables = ["LHFLX"] +# seasons = ["ANN"] +# ref_name = "ERA-Interim" +# reference_name = "ERA-Interim" + +# [#] +# sets = ["enso_diags"] +# plot_type = "scatter" +# case_id = "SHFLX-feedback" +# variables = ["SHFLX"] +# seasons = ["ANN"] +# ref_name = "ERA-Interim" +# reference_name = "ERA-Interim" + +# [#] +# sets = ["enso_diags"] +# plot_type = "scatter" +# case_id = "FLNS-feedback" +# variables = ["FLNS"] +# seasons = ["ANN"] +# ref_name = "ERA-Interim" +# reference_name = "ERA-Interim" + +# [#] +# sets = ["enso_diags"] +# plot_type = "scatter" +# case_id = "FSNS-feedback" +# variables = ["FSNS"] +# seasons = ["ANN"] +# ref_name = "ERA-Interim" +# reference_name = "ERA-Interim" + +# [#] +# sets = ["enso_diags"] +# plot_type = "scatter" +# case_id = "NET_FLUX_SRF-feedback" +# variables = ["NET_FLUX_SRF"] +# seasons = ["ANN"] +# ref_name = "ERA-Interim" +# reference_name = "ERA-Interim" diff --git a/auxiliary_tools/cdat_regression_testing/663-enso-diags/662-area-mean-time-series-rerun/run_script.py b/auxiliary_tools/cdat_regression_testing/663-enso-diags/662-area-mean-time-series-rerun/run_script.py new file mode 100644 index 000000000..db7facf98 --- /dev/null +++ b/auxiliary_tools/cdat_regression_testing/663-enso-diags/662-area-mean-time-series-rerun/run_script.py @@ -0,0 +1,12 @@ +# %% +# python -m auxiliary_tools.cdat_regression_testing.663-enso-diags.662-area-mean-time-series-rerun.run_script +# chmod -R o=rx /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-area-mean-time-series-rerun +from auxiliary_tools.cdat_regression_testing.base_run_script import run_set + +SET_NAME = "area_mean_time_series" +SET_DIR = "663-area-mean-time-series-rerun" +CFG_PATH: str | None = None +# CFG_PATH = "/global/u2/v/vo13/E3SM-Project/e3sm_diags/auxiliary_tools/cdat_regression_testing/663-enso-diags/662-area-mean-time-series-rerun/run.cfg" +MULTIPROCESSING = True + +run_set(SET_NAME, SET_DIR, CFG_PATH, MULTIPROCESSING) diff --git a/auxiliary_tools/cdat_regression_testing/663-enso-diags/qa.py b/auxiliary_tools/cdat_regression_testing/663-enso-diags/qa.py new file mode 100644 index 000000000..8992a8ad2 --- /dev/null +++ b/auxiliary_tools/cdat_regression_testing/663-enso-diags/qa.py @@ -0,0 +1,64 @@ +def _subset_on_region(ds: xr.Dataset, region: str) -> xr.Dataset: + """Subset the dataset by the region 5S5N (latitude). + This function takes into account the CDAT subset flag, "ccb", which can + add new latitude coordinate points to the beginning and end. + Parameters + ---------- + ds : xr.Dataset + The dataset. + Returns + ------- + xr.Dataset + The dataset subsetted by the region. + """ + specs = REGION_SPECS[region] + lat_slice, lon_slice = specs.get("lat"), specs.get("lon") # type: ignore + + ds_new = ds.copy() + lat_dim_key = xc.get_dim_keys(ds, "Y") + lon_dim_key = xc.get_dim_keys(ds, "X") + + # 1. Subset on the region_slice + # slice = -5.0, 5.0 + slice_dict = {} + if lat_slice is not None: + slice_dict[lat_dim_key] = slice(*lat_slice) + if lon_slice is not None: + slice_dict[lon_dim_key] = slice(*lon_slice) + + ds_new = ds_new.sel(slice_dict) + + # 2. Add delta to first and last value + dim_bounds = ds_new.bounds.get_bounds(axis="Y") + + # delta = 1.0 / 2 = 0.5 + delta = (dim_bounds[0][1].item() - dim_bounds[0][0].item()) / 2 + delta_slice = (lat_slice[0] - delta, lat_slice[1] + delta) + + # 3. Check if latitude slice value exists in original latitude. + # If it exists already, then don't add the coordinate point. + # If it does not exist, add the coordinate point. + # delta = 0.5 + # delta slice = -5.5, 5.5 + ds_list = [ds_new] + + try: + ds.sel({lat_dim_key: delta_slice[0]}) + except KeyError: + ds_first_pt = ds_new.isel({lat_dim_key: 0}) + ds_first_pt[lat_dim_key] = ds_first_pt[lat_dim_key] - delta + + ds_list.append(ds_first_pt) + + try: + ds.sel({lat_dim_key: delta_slice[-1]}) + except KeyError: + ds_last_pt = ds_new.isel({lat_dim_key: -1}) + ds_last_pt[lat_dim_key] = ds_last_pt[lat_dim_key] + delta + + ds_list.append(ds_last_pt) + + ds_new = xr.concat(ds_list, dim=lat_dim_key, data_vars="minimal", coords="minimal") + ds_new = ds_new.sortby(lat_dim_key) + + return ds_new diff --git a/auxiliary_tools/cdat_regression_testing/663-enso-diags/regression_test.ipynb b/auxiliary_tools/cdat_regression_testing/663-enso-diags/regression_test.ipynb new file mode 100644 index 000000000..c74f5b2b3 --- /dev/null +++ b/auxiliary_tools/cdat_regression_testing/663-enso-diags/regression_test.ipynb @@ -0,0 +1,1593 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# CDAT Migration Regression Testing Notebook (`.nc` files)\n", + "\n", + "This notebook is used to perform regression testing between the development and\n", + "production versions of a diagnostic set.\n", + "\n", + "## How it works\n", + "\n", + "It compares the relative differences (%) between ref and test variables between\n", + "the dev and `main` branches.\n", + "\n", + "## How to use\n", + "\n", + "PREREQUISITE: The diagnostic set's netCDF stored in `.json` files in two directories\n", + "(dev and `main` branches).\n", + "\n", + "1. Make a copy of this notebook under `auxiliary_tools/cdat_regression_testing/`.\n", + "2. Run `mamba create -n cdat_regression_test -y -c conda-forge \"python<3.12\" xarray netcdf4 dask pandas matplotlib-base ipykernel`\n", + "3. Run `mamba activate cdat_regression_test`\n", + "4. Update `SET_DIR` and `SET_NAME` in the copy of your notebook.\n", + "5. Run all cells IN ORDER.\n", + "6. Review results for any outstanding differences (>=1e-5 relative tolerance).\n", + " - Debug these differences (e.g., bug in metrics functions, incorrect variable references, etc.)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup Code\n" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "metadata": {}, + "outputs": [], + "source": [ + "import glob\n", + "\n", + "import numpy as np\n", + "import xarray as xr\n", + "from e3sm_diags.derivations.derivations import DERIVED_VARIABLES\n", + "\n", + "SET_NAME = \"enso_diags\"\n", + "SET_DIR = \"663-enso-diags\"\n", + "\n", + "DEV_PATH = f\"/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/{SET_DIR}/{SET_NAME}/**\"\n", + "DEV_GLOB = sorted(glob.glob(DEV_PATH + \"/*.nc\"))\n", + "DEV_NUM_FILES = len(DEV_GLOB)\n", + "\n", + "MAIN_PATH = f\"/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/{SET_NAME}/**\"\n", + "MAIN_GLOB = sorted(glob.glob(MAIN_PATH + \"/*.nc\"))\n", + "MAIN_NUM_FILES = len(MAIN_GLOB)" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "metadata": {}, + "outputs": [], + "source": [ + "def _check_if_files_found():\n", + " if DEV_NUM_FILES == 0 or MAIN_NUM_FILES == 0:\n", + " raise IOError(\n", + " \"No files found at DEV_PATH and/or MAIN_PATH. \"\n", + " f\"Please check {DEV_PATH} and {MAIN_PATH}.\"\n", + " )\n", + "\n", + "\n", + "def _check_if_matching_filecount():\n", + " if DEV_NUM_FILES != MAIN_NUM_FILES:\n", + " raise IOError(\n", + " \"Number of files do not match at DEV_PATH and MAIN_PATH \"\n", + " f\"({DEV_NUM_FILES} vs. {MAIN_NUM_FILES}).\"\n", + " )\n", + "\n", + " print(f\"Matching file count ({DEV_NUM_FILES} and {MAIN_NUM_FILES}).\")\n", + "\n", + "\n", + "def _check_if_missing_files():\n", + " missing_count = 0\n", + "\n", + " for fp_main in MAIN_GLOB:\n", + " fp_dev = fp_main.replace(SET_DIR, \"enso-main\")\n", + "\n", + " if fp_dev not in MAIN_GLOB:\n", + " print(f\"No production file found to compare with {fp_dev}!\")\n", + " missing_count += 1\n", + "\n", + " for fp_dev in DEV_GLOB:\n", + " fp_main = fp_main.replace(\"enso-main\", SET_DIR)\n", + "\n", + " if fp_main not in DEV_GLOB:\n", + " print(f\"No development file found to compare with {fp_main}!\")\n", + " missing_count += 1\n", + "\n", + " print(f\"Number of files missing: {missing_count}\")" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "metadata": {}, + "outputs": [], + "source": [ + "def _get_relative_diffs() -> int:\n", + " # We are mainly focusing on relative tolerance here (in percentage terms).\n", + " atol = 0\n", + " rtol = 1e-5\n", + "\n", + " mismatches = []\n", + "\n", + " for fp_main in MAIN_GLOB:\n", + " if \"test.nc\" in fp_main or \"ref.nc\" in fp_main:\n", + " fp_dev = fp_main.replace(\"enso-main\", SET_DIR)\n", + "\n", + " print(\"Comparing:\")\n", + " print(f\" * {fp_dev}\")\n", + " print(f\" * {fp_main}\")\n", + "\n", + " ds1 = xr.open_dataset(fp_dev)\n", + " ds2 = xr.open_dataset(fp_main)\n", + "\n", + " if \"regression\" in fp_main:\n", + " var_key = fp_main.split(\"/\")[-1].split(\"-\")[2].upper()\n", + " var_key_cdat = f\"{var_key}-regression-over-nino\"\n", + " elif \"feedback\" in fp_main:\n", + " var_key = fp_main.split(\"/\")[-1].split(\"-\")[1].upper()\n", + " var_key_cdat = f\"{var_key}-feedback\"\n", + "\n", + " print(f\" * var_key: {var_key}\")\n", + "\n", + " dev_data = ds1[var_key].values\n", + " # main_data = ds2[var_key_cdat].values[1:-1]\n", + " main_data = ds2[var_key_cdat]\n", + "\n", + " if dev_data is None or main_data is None:\n", + " print(\" * Could not find variable key in the dataset(s)\")\n", + " continue\n", + "\n", + " try:\n", + " np.testing.assert_allclose(\n", + " dev_data,\n", + " main_data,\n", + " atol=atol,\n", + " rtol=rtol,\n", + " )\n", + " except (KeyError, AssertionError) as e:\n", + " if \"mismatch\" in str(e):\n", + " np.testing.assert_allclose(\n", + " dev_data,\n", + " main_data[1:-1],\n", + " atol=atol,\n", + " rtol=rtol,\n", + " )\n", + " else:\n", + " print(f\" {e}\")\n", + " mismatches.append((fp_dev, fp_main))\n", + " else:\n", + " print(f\" * All close and within relative tolerance ({rtol})\")\n", + "\n", + " return mismatches" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Check for matching and equal number of files\n" + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "metadata": {}, + "outputs": [], + "source": [ + "_check_if_files_found()" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FLNS-feedback/feedback-FLNS-NINO3-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FLNS-feedback/feedback-FLNS-NINO3-TS-NINO3_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FSNS-feedback/feedback-FSNS-NINO3-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FSNS-feedback/feedback-FSNS-NINO3-TS-NINO3_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-feedback/feedback-LHFLX-NINO3-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-feedback/feedback-LHFLX-NINO3-TS-NINO3_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-response/regression-coefficient-lhflx-over-nino34_diff.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-response/regression-coefficient-lhflx-over-nino34_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-response/regression-coefficient-lhflx-over-nino34_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-feedback/feedback-NET_FLUX_SRF-NINO3-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-feedback/feedback-NET_FLUX_SRF-NINO3-TS-NINO3_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-response/regression-coefficient-net_flux_srf-over-nino34_diff.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-response/regression-coefficient-net_flux_srf-over-nino34_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-response/regression-coefficient-net_flux_srf-over-nino34_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/PRECT-response/regression-coefficient-prect-over-nino34_diff.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/PRECT-response/regression-coefficient-prect-over-nino34_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/PRECT-response/regression-coefficient-prect-over-nino34_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-feedback/feedback-SHFLX-NINO3-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-feedback/feedback-SHFLX-NINO3-TS-NINO3_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-response/regression-coefficient-shflx-over-nino34_diff.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-response/regression-coefficient-shflx-over-nino34_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-response/regression-coefficient-shflx-over-nino34_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-feedback/feedback-TAUX-NINO4-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-feedback/feedback-TAUX-NINO4-TS-NINO3_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-response/regression-coefficient-taux-over-nino34_diff.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-response/regression-coefficient-taux-over-nino34_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-response/regression-coefficient-taux-over-nino34_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUY-response/regression-coefficient-tauy-over-nino34_diff.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUY-response/regression-coefficient-tauy-over-nino34_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUY-response/regression-coefficient-tauy-over-nino34_test.nc']" + ] + }, + "execution_count": 81, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DEV_GLOB" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/FLNS-feedback/feedback-FLNS-NINO3-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/FLNS-feedback/feedback-FLNS-NINO3-TS-NINO3_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/FSNS-feedback/feedback-FSNS-NINO3-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/FSNS-feedback/feedback-FSNS-NINO3-TS-NINO3_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/LHFLX-feedback/feedback-LHFLX-NINO3-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/LHFLX-feedback/feedback-LHFLX-NINO3-TS-NINO3_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/LHFLX-response/regression-coefficient-lhflx-over-nino34_diff.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/LHFLX-response/regression-coefficient-lhflx-over-nino34_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/LHFLX-response/regression-coefficient-lhflx-over-nino34_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/NET_FLUX_SRF-feedback/feedback-NET_FLUX_SRF-NINO3-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/NET_FLUX_SRF-feedback/feedback-NET_FLUX_SRF-NINO3-TS-NINO3_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/NET_FLUX_SRF-response/regression-coefficient-net_flux_srf-over-nino34_diff.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/NET_FLUX_SRF-response/regression-coefficient-net_flux_srf-over-nino34_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/NET_FLUX_SRF-response/regression-coefficient-net_flux_srf-over-nino34_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/PRECT-response/regression-coefficient-prect-over-nino34_diff.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/PRECT-response/regression-coefficient-prect-over-nino34_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/PRECT-response/regression-coefficient-prect-over-nino34_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/SHFLX-feedback/feedback-SHFLX-NINO3-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/SHFLX-feedback/feedback-SHFLX-NINO3-TS-NINO3_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/SHFLX-response/regression-coefficient-shflx-over-nino34_diff.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/SHFLX-response/regression-coefficient-shflx-over-nino34_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/SHFLX-response/regression-coefficient-shflx-over-nino34_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/TAUX-feedback/feedback-TAUX-NINO4-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/TAUX-feedback/feedback-TAUX-NINO4-TS-NINO3_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/TAUX-response/regression-coefficient-taux-over-nino34_diff.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/TAUX-response/regression-coefficient-taux-over-nino34_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/TAUX-response/regression-coefficient-taux-over-nino34_test.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/TAUY-response/regression-coefficient-tauy-over-nino34_diff.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/TAUY-response/regression-coefficient-tauy-over-nino34_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/TAUY-response/regression-coefficient-tauy-over-nino34_test.nc']" + ] + }, + "execution_count": 82, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "MAIN_GLOB" + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of files missing: 0\n" + ] + } + ], + "source": [ + "_check_if_missing_files()" + ] + }, + { + "cell_type": "code", + "execution_count": 84, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Matching file count (30 and 30).\n" + ] + } + ], + "source": [ + "_check_if_matching_filecount()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2 Compare the netCDF files between branches\n", + "\n", + "- Compare \"ref\" and \"test\" files\n", + "- \"diff\" files are ignored because getting relative diffs for these does not make sense (relative diff will be above tolerance)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FLNS-feedback/feedback-FLNS-NINO3-TS-NINO3_ref.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/FLNS-feedback/feedback-FLNS-NINO3-TS-NINO3_ref.nc\n", + " * var_key: FLNS\n", + " \n", + "Not equal to tolerance rtol=1e-05, atol=0\n", + "\n", + "Mismatched elements: 120 / 120 (100%)\n", + "Max absolute difference: 0.08201782\n", + "Max relative difference: 0.64689754\n", + " x: array([ 4.58544 , 2.485706, 3.111711, 0.767667, 1.648402, 3.223995,\n", + " 1.59968 , 0.373345, 1.550645, -0.038671, -1.088026, 0.199513,\n", + " -1.694707, 0.574215, -1.211548, -0.088548, 1.481813, 0.685491,...\n", + " y: array([ 4.576222, 2.494163, 3.120666, 0.74798 , 1.660498, 3.238505,\n", + " 1.615224, 0.385147, 1.570752, -0.061149, -1.084575, 0.19586 ,\n", + " -1.688319, 0.608236, -1.16629 , -0.060196, 1.414754, 0.636474,...\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FLNS-feedback/feedback-FLNS-NINO3-TS-NINO3_test.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/FLNS-feedback/feedback-FLNS-NINO3-TS-NINO3_test.nc\n", + " * var_key: FLNS\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FSNS-feedback/feedback-FSNS-NINO3-TS-NINO3_ref.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/FSNS-feedback/feedback-FSNS-NINO3-TS-NINO3_ref.nc\n", + " * var_key: FSNS\n", + " \n", + "Not equal to tolerance rtol=1e-05, atol=0\n", + "\n", + "Mismatched elements: 120 / 120 (100%)\n", + "Max absolute difference: 0.43258972\n", + "Max relative difference: 2.16216355\n", + " x: array([ 1.213787e+01, 7.135695e+00, 3.147739e+00, 3.137359e+00,\n", + " 4.684971e+00, 6.356290e+00, 2.267159e+00, 3.013787e+00,\n", + " 3.547312e+00, 2.070424e-02, 3.573419e+00, 5.998464e+00,...\n", + " y: array([ 1.203760e+01, 7.208760e+00, 3.153898e+00, 3.050703e+00,\n", + " 4.708108e+00, 6.299110e+00, 2.339174e+00, 3.113597e+00,\n", + " 3.582495e+00, -1.781526e-02, 3.572107e+00, 6.033232e+00,...\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FSNS-feedback/feedback-FSNS-NINO3-TS-NINO3_test.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/FSNS-feedback/feedback-FSNS-NINO3-TS-NINO3_test.nc\n", + " * var_key: FSNS\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-feedback/feedback-LHFLX-NINO3-TS-NINO3_ref.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/LHFLX-feedback/feedback-LHFLX-NINO3-TS-NINO3_ref.nc\n", + " * var_key: LHFLX\n", + " \n", + "Not equal to tolerance rtol=1e-05, atol=0\n", + "\n", + "Mismatched elements: 120 / 120 (100%)\n", + "Max absolute difference: 0.31026925\n", + "Max relative difference: 1.4436187\n", + " x: array([-16.846834, -11.460476, -0.260557, -7.857147, -7.481644,\n", + " 2.495975, -2.459956, -6.000213, -7.531353, -4.563122,\n", + " -14.512969, -17.648658, -9.918952, -6.918592, -0.572116,...\n", + " y: array([-16.849978, -11.233628, -0.106627, -7.62422 , -7.419526,\n", + " 2.54498 , -2.387893, -6.147381, -7.456442, -4.568174,\n", + " -14.5197 , -17.660721, -10.14628 , -7.124798, -0.717113,...\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-feedback/feedback-LHFLX-NINO3-TS-NINO3_test.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/LHFLX-feedback/feedback-LHFLX-NINO3-TS-NINO3_test.nc\n", + " * var_key: LHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-response/regression-coefficient-lhflx-over-nino34_ref.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/LHFLX-response/regression-coefficient-lhflx-over-nino34_ref.nc\n", + " * var_key: LHFLX\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-response/regression-coefficient-lhflx-over-nino34_test.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/LHFLX-response/regression-coefficient-lhflx-over-nino34_test.nc\n", + " * var_key: LHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-feedback/feedback-NET_FLUX_SRF-NINO3-TS-NINO3_ref.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/NET_FLUX_SRF-feedback/feedback-NET_FLUX_SRF-NINO3-TS-NINO3_ref.nc\n", + " * var_key: NET_FLUX_SRF\n", + " \n", + "Not equal to tolerance rtol=1e-05, atol=0\n", + "\n", + "Mismatched elements: 120 / 120 (100%)\n", + "Max absolute difference: 0.57487474\n", + "Max relative difference: 0.44062548\n", + " x: array([ 24.986253, 16.954633, -0.294606, 11.19022 , 11.682311,\n", + " 1.17291 , 3.197588, 8.8063 , 9.345975, 4.506336,\n", + " 19.777237, 24.261501, 15.081716, 15.104376, -2.231025,...\n", + " y: array([ 24.890231, 16.794664, -0.473196, 10.855193, 11.617329,\n", + " 1.032505, 3.180212, 9.061564, 9.283075, 4.487035,\n", + " 19.787209, 24.335711, 15.280794, 15.390211, -1.65615 ,...\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-feedback/feedback-NET_FLUX_SRF-NINO3-TS-NINO3_test.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/NET_FLUX_SRF-feedback/feedback-NET_FLUX_SRF-NINO3-TS-NINO3_test.nc\n", + " * var_key: NET_FLUX_SRF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-response/regression-coefficient-net_flux_srf-over-nino34_ref.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/NET_FLUX_SRF-response/regression-coefficient-net_flux_srf-over-nino34_ref.nc\n", + " * var_key: NET_FLUX_SRF\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-response/regression-coefficient-net_flux_srf-over-nino34_test.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/NET_FLUX_SRF-response/regression-coefficient-net_flux_srf-over-nino34_test.nc\n", + " * var_key: NET_FLUX_SRF\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/PRECT-response/regression-coefficient-prect-over-nino34_ref.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/PRECT-response/regression-coefficient-prect-over-nino34_ref.nc\n", + " * var_key: PRECT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/PRECT-response/regression-coefficient-prect-over-nino34_test.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/PRECT-response/regression-coefficient-prect-over-nino34_test.nc\n", + " * var_key: PRECT\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-feedback/feedback-SHFLX-NINO3-TS-NINO3_ref.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/SHFLX-feedback/feedback-SHFLX-NINO3-TS-NINO3_ref.nc\n", + " * var_key: SHFLX\n", + " \n", + "Not equal to tolerance rtol=1e-05, atol=0\n", + "\n", + "Mismatched elements: 120 / 120 (100%)\n", + "Max absolute difference: 0.04254491\n", + "Max relative difference: 1.57097275\n", + " x: array([-0.586986, -0.844167, 0.591191, -0.96338 , -1.164099, -0.536591,\n", + " -0.070152, -0.165644, 0.182045, 0.116161, -0.602823, -0.813892,\n", + " -0.385786, -0.996138, -0.915772, -1.011553, -0.57045 , 0.179156,...\n", + " y: array([-0.57887 , -0.846439, 0.613055, -0.928249, -1.150193, -0.516881,\n", + " -0.068369, -0.185733, 0.18511 , 0.124473, -0.610827, -0.83762 ,\n", + " -0.412688, -1.004824, -0.958317, -1.046433, -0.579117, 0.146234,...\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-feedback/feedback-SHFLX-NINO3-TS-NINO3_test.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/SHFLX-feedback/feedback-SHFLX-NINO3-TS-NINO3_test.nc\n", + " * var_key: SHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-response/regression-coefficient-shflx-over-nino34_ref.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/SHFLX-response/regression-coefficient-shflx-over-nino34_ref.nc\n", + " * var_key: SHFLX\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-response/regression-coefficient-shflx-over-nino34_test.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/SHFLX-response/regression-coefficient-shflx-over-nino34_test.nc\n", + " * var_key: SHFLX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-feedback/feedback-TAUX-NINO4-TS-NINO3_ref.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/TAUX-feedback/feedback-TAUX-NINO4-TS-NINO3_ref.nc\n", + " * var_key: TAUX\n", + " \n", + "Not equal to tolerance rtol=1e-05, atol=0\n", + "\n", + "Mismatched elements: 119 / 120 (99.2%)\n", + "Max absolute difference: 0.41271604\n", + "Max relative difference: 0.66807887\n", + " x: array([ 18.272091, 11.067615, 7.282271, 2.0656 , -2.899694,\n", + " -5.542193, -7.454822, 1.728814, -8.768283, -0.205142,\n", + " -1.855827, -25.474358, -7.966977, -12.302557, -1.793213,...\n", + " y: array([ 18.509528, 11.142698, 7.307616, 2.121824, -2.963343,\n", + " -5.693791, -7.595321, 1.652784, -8.808141, -0.218935,\n", + " -1.951945, -25.624018, -8.032325, -12.401156, -1.821695,...\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-feedback/feedback-TAUX-NINO4-TS-NINO3_test.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/TAUX-feedback/feedback-TAUX-NINO4-TS-NINO3_test.nc\n", + " * var_key: TAUX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-response/regression-coefficient-taux-over-nino34_ref.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/TAUX-response/regression-coefficient-taux-over-nino34_ref.nc\n", + " * var_key: TAUX\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-response/regression-coefficient-taux-over-nino34_test.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/TAUX-response/regression-coefficient-taux-over-nino34_test.nc\n", + " * var_key: TAUX\n", + " * All close and within relative tolerance (1e-05)\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUY-response/regression-coefficient-tauy-over-nino34_ref.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/TAUY-response/regression-coefficient-tauy-over-nino34_ref.nc\n", + " * var_key: TAUY\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUY-response/regression-coefficient-tauy-over-nino34_test.nc\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/TAUY-response/regression-coefficient-tauy-over-nino34_test.nc\n", + " * var_key: TAUY\n", + " * All close and within relative tolerance (1e-05)\n" + ] + } + ], + "source": [ + "mismatches = _get_relative_diffs()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Results\n", + "\n", + "- For the CDAT regression netCDF files, we remove the extra start and end lat coordinates that are added via the `\"ccb\"` slice flag. This ensures arrays and results are aligned.\n", + "- For the feedback netCDF files, the two extra points results in what seems like a large differences in the anomalies.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[('/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FLNS-feedback/feedback-FLNS-NINO3-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/FLNS-feedback/feedback-FLNS-NINO3-TS-NINO3_ref.nc'),\n", + " ('/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FSNS-feedback/feedback-FSNS-NINO3-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/FSNS-feedback/feedback-FSNS-NINO3-TS-NINO3_ref.nc'),\n", + " ('/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-feedback/feedback-LHFLX-NINO3-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/LHFLX-feedback/feedback-LHFLX-NINO3-TS-NINO3_ref.nc'),\n", + " ('/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-feedback/feedback-NET_FLUX_SRF-NINO3-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/NET_FLUX_SRF-feedback/feedback-NET_FLUX_SRF-NINO3-TS-NINO3_ref.nc'),\n", + " ('/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-feedback/feedback-SHFLX-NINO3-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/SHFLX-feedback/feedback-SHFLX-NINO3-TS-NINO3_ref.nc'),\n", + " ('/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-feedback/feedback-TAUX-NINO4-TS-NINO3_ref.nc',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/enso-main/enso_diags/TAUX-feedback/feedback-TAUX-NINO4-TS-NINO3_ref.nc')]" + ] + }, + "execution_count": 86, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "mismatches" + ] + }, + { + "cell_type": "code", + "execution_count": 65, + "metadata": {}, + "outputs": [], + "source": [ + "ds1 = xr.open_dataset(mismatches[0][0])" + ] + }, + { + "cell_type": "code", + "execution_count": 66, + "metadata": {}, + "outputs": [], + "source": [ + "ds2 = xr.open_dataset(mismatches[0][1])" + ] + }, + { + "cell_type": "code", + "execution_count": 67, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 2kB\n",
+       "Dimensions:  (time: 120)\n",
+       "Coordinates:\n",
+       "  * time     (time) datetime64[ns] 960B 2001-01-16T12:00:00 ... 2010-12-16T12...\n",
+       "Data variables:\n",
+       "    FLNS     (time) float64 960B ...
" + ], + "text/plain": [ + " Size: 2kB\n", + "Dimensions: (time: 120)\n", + "Coordinates:\n", + " * time (time) datetime64[ns] 960B 2001-01-16T12:00:00 ... 2010-12-16T12...\n", + "Data variables:\n", + " FLNS (time) float64 960B ..." + ] + }, + "execution_count": 67, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds1" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "
<xarray.Dataset> Size: 4kB\n",
+       "Dimensions:        (time: 120, bound: 2)\n",
+       "Coordinates:\n",
+       "  * time           (time) datetime64[ns] 960B 2001-01-16T12:00:00 ... 2010-12...\n",
+       "Dimensions without coordinates: bound\n",
+       "Data variables:\n",
+       "    bounds_time    (time, bound) datetime64[ns] 2kB ...\n",
+       "    FLNS-feedback  (time) float64 960B 4.576 2.494 3.121 ... -5.735 -2.117 -3.35\n",
+       "Attributes:\n",
+       "    Conventions:  CF-1.0
" + ], + "text/plain": [ + " Size: 4kB\n", + "Dimensions: (time: 120, bound: 2)\n", + "Coordinates:\n", + " * time (time) datetime64[ns] 960B 2001-01-16T12:00:00 ... 2010-12...\n", + "Dimensions without coordinates: bound\n", + "Data variables:\n", + " bounds_time (time, bound) datetime64[ns] 2kB ...\n", + " FLNS-feedback (time) float64 960B 4.576 2.494 3.121 ... -5.735 -2.117 -3.35\n", + "Attributes:\n", + " Conventions: CF-1.0" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "ds2" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "-0.2028542676624383 -0.202113868730045\n", + "-0.0016904522305203193 -0.0016842822394170416\n", + "-8.156214274999115 -8.103394765085596\n", + "4.585439916659517 4.5762217718118094\n", + "2.281253170375751 2.2717773078329726\n" + ] + } + ], + "source": [ + "print(ds1[\"FLNS\"].sum().item(), ds2[\"FLNS-feedback\"].sum().item())\n", + "print(ds1[\"FLNS\"].mean().item(), ds2[\"FLNS-feedback\"].mean().item())\n", + "print(ds1[\"FLNS\"].min().item(), ds2[\"FLNS-feedback\"].min().item())\n", + "print(ds1[\"FLNS\"].max().item(), ds2[\"FLNS-feedback\"].max().item())\n", + "print(ds1[\"FLNS\"].std().item(), ds2[\"FLNS-feedback\"].std().item())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cdat_regression_test", + "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.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/auxiliary_tools/cdat_regression_testing/663-enso-diags/regression_test_png.ipynb b/auxiliary_tools/cdat_regression_testing/663-enso-diags/regression_test_png.ipynb new file mode 100644 index 000000000..0e8635e5d --- /dev/null +++ b/auxiliary_tools/cdat_regression_testing/663-enso-diags/regression_test_png.ipynb @@ -0,0 +1,339 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# CDAT Migration Regression Testing Notebook (`.png` files)\n", + "\n", + "This notebook is used to perform regression testing between the development and\n", + "production versions of a diagnostic set.\n", + "\n", + "## How to use\n", + "\n", + "PREREQUISITE: The diagnostic set's netCDF stored in `.json` files in two directories\n", + "(dev and `main` branches).\n", + "\n", + "1. Make a copy of this notebook under `auxiliary_tools/cdat_regression_testing/`.\n", + "2. Run `mamba create -n cdat_regression_test -y -c conda-forge \"python<3.12\" xarray netcdf4 dask pandas matplotlib-base ipykernel`\n", + "3. Run `mamba activate cdat_regression_test`\n", + "4. Update `SET_DIR` and `SET_NAME` in the copy of your notebook.\n", + "5. Run all cells IN ORDER.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup Code\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import glob\n", + "\n", + "from auxiliary_tools.cdat_regression_testing.utils import get_image_diffs\n", + "\n", + "SET_NAME = \"enso_diags\"\n", + "SET_DIR = \"663-enso-diags\"\n", + "\n", + "DEV_PATH = f\"/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/{SET_DIR}/{SET_NAME}/**\"\n", + "DEV_GLOB = sorted(glob.glob(DEV_PATH + \"/*.png\"))\n", + "DEV_NUM_FILES = len(DEV_GLOB)\n", + "\n", + "MAIN_PATH = f\"/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/{SET_NAME}/**\"\n", + "MAIN_GLOB = sorted(glob.glob(MAIN_PATH + \"/*.png\"))\n", + "MAIN_NUM_FILES = len(MAIN_GLOB)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "def _check_if_files_found():\n", + " if DEV_NUM_FILES == 0 or MAIN_NUM_FILES == 0:\n", + " raise IOError(\n", + " \"No files found at DEV_PATH and/or MAIN_PATH. \"\n", + " f\"Please check {DEV_PATH} and {MAIN_PATH}.\"\n", + " )\n", + "\n", + "\n", + "def _check_if_matching_filecount():\n", + " if DEV_NUM_FILES != MAIN_NUM_FILES:\n", + " raise IOError(\n", + " \"Number of files do not match at DEV_PATH and MAIN_PATH \"\n", + " f\"({DEV_NUM_FILES} vs. {MAIN_NUM_FILES}).\"\n", + " )\n", + "\n", + " print(f\"Matching file count ({DEV_NUM_FILES} and {MAIN_NUM_FILES}).\")\n", + "\n", + "\n", + "def _check_if_missing_files():\n", + " missing_count = 0\n", + "\n", + " for fp_main in MAIN_GLOB:\n", + " fp_dev = fp_main.replace(SET_DIR, \"main\")\n", + "\n", + " if fp_dev not in MAIN_GLOB:\n", + " print(f\"No production file found to compare with {fp_dev}!\")\n", + " missing_count += 1\n", + "\n", + " for fp_dev in DEV_GLOB:\n", + " fp_main = fp_main.replace(\"main\", SET_DIR)\n", + "\n", + " if fp_main not in DEV_GLOB:\n", + " print(f\"No development file found to compare with {fp_main}!\")\n", + " missing_count += 1\n", + "\n", + " print(f\"Number of files missing: {missing_count}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Check for matching and equal number of files\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "_check_if_files_found()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Number of files missing: 0\n" + ] + } + ], + "source": [ + "_check_if_missing_files()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "ename": "OSError", + "evalue": "Number of files do not match at DEV_PATH and MAIN_PATH (24 vs. 12).", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mOSError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[5], line 1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[43m_check_if_matching_filecount\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[2], line 11\u001b[0m, in \u001b[0;36m_check_if_matching_filecount\u001b[0;34m()\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m_check_if_matching_filecount\u001b[39m():\n\u001b[1;32m 10\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m DEV_NUM_FILES \u001b[38;5;241m!=\u001b[39m MAIN_NUM_FILES:\n\u001b[0;32m---> 11\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mIOError\u001b[39;00m(\n\u001b[1;32m 12\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mNumber of files do not match at DEV_PATH and MAIN_PATH \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 13\u001b[0m \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m(\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mDEV_NUM_FILES\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m vs. \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mMAIN_NUM_FILES\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m).\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 14\u001b[0m )\n\u001b[1;32m 16\u001b[0m \u001b[38;5;28mprint\u001b[39m(\u001b[38;5;124mf\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mMatching file count (\u001b[39m\u001b[38;5;132;01m{\u001b[39;00mDEV_NUM_FILES\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m and \u001b[39m\u001b[38;5;132;01m{\u001b[39;00mMAIN_NUM_FILES\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m).\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n", + "\u001b[0;31mOSError\u001b[0m: Number of files do not match at DEV_PATH and MAIN_PATH (24 vs. 12)." + ] + } + ], + "source": [ + "_check_if_matching_filecount()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2 Compare the plots between branches\n", + "\n", + "- Compare \"ref\" and \"test\" files\n", + "- \"diff\" files are ignored because getting relative diffs for these does not make sense (relative diff will be above tolerance)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/FLNS-feedback/feedback-FLNS-NINO3-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/FSNS-feedback/feedback-FSNS-NINO3-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/LHFLX-feedback/feedback-LHFLX-NINO3-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/LHFLX-response/regression-coefficient-lhflx-over-nino34.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/NET_FLUX_SRF-feedback/feedback-NET_FLUX_SRF-NINO3-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/NET_FLUX_SRF-response/regression-coefficient-net_flux_srf-over-nino34.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/PRECT-response/regression-coefficient-prect-over-nino34.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/SHFLX-feedback/feedback-SHFLX-NINO3-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/SHFLX-response/regression-coefficient-shflx-over-nino34.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/TAUX-feedback/feedback-TAUX-NINO4-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/TAUX-response/regression-coefficient-taux-over-nino34.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/TAUY-response/regression-coefficient-tauy-over-nino34.png']" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "MAIN_GLOB" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FLNS-feedback/feedback-FLNS-NINO3-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FLNS-feedback_diff/feedback-FLNS-NINO3-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FSNS-feedback/feedback-FSNS-NINO3-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FSNS-feedback_diff/feedback-FSNS-NINO3-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-feedback/feedback-LHFLX-NINO3-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-feedback_diff/feedback-LHFLX-NINO3-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-response/regression-coefficient-lhflx-over-nino34.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-response_diff/regression-coefficient-lhflx-over-nino34.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-feedback/feedback-NET_FLUX_SRF-NINO3-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-feedback_diff/feedback-NET_FLUX_SRF-NINO3-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-response/regression-coefficient-net_flux_srf-over-nino34.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-response_diff/regression-coefficient-net_flux_srf-over-nino34.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/PRECT-response/regression-coefficient-prect-over-nino34.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/PRECT-response_diff/regression-coefficient-prect-over-nino34.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-feedback/feedback-SHFLX-NINO3-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-feedback_diff/feedback-SHFLX-NINO3-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-response/regression-coefficient-shflx-over-nino34.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-response_diff/regression-coefficient-shflx-over-nino34.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-feedback/feedback-TAUX-NINO4-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-feedback_diff/feedback-TAUX-NINO4-TS-NINO3.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-response/regression-coefficient-taux-over-nino34.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-response_diff/regression-coefficient-taux-over-nino34.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUY-response/regression-coefficient-tauy-over-nino34.png',\n", + " '/global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUY-response_diff/regression-coefficient-tauy-over-nino34.png']" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "DEV_GLOB" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/FLNS-feedback/feedback-FLNS-NINO3-TS-NINO3.png\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FLNS-feedback/feedback-FLNS-NINO3-TS-NINO3.png\n", + " * Difference path /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FLNS-feedback_diff/feedback-FLNS-NINO3-TS-NINO3.png\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/FSNS-feedback/feedback-FSNS-NINO3-TS-NINO3.png\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FSNS-feedback/feedback-FSNS-NINO3-TS-NINO3.png\n", + " * Difference path /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/FSNS-feedback_diff/feedback-FSNS-NINO3-TS-NINO3.png\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/LHFLX-feedback/feedback-LHFLX-NINO3-TS-NINO3.png\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-feedback/feedback-LHFLX-NINO3-TS-NINO3.png\n", + " * Difference path /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-feedback_diff/feedback-LHFLX-NINO3-TS-NINO3.png\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/LHFLX-response/regression-coefficient-lhflx-over-nino34.png\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-response/regression-coefficient-lhflx-over-nino34.png\n", + " * Difference path /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/LHFLX-response_diff/regression-coefficient-lhflx-over-nino34.png\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/NET_FLUX_SRF-feedback/feedback-NET_FLUX_SRF-NINO3-TS-NINO3.png\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-feedback/feedback-NET_FLUX_SRF-NINO3-TS-NINO3.png\n", + " * Difference path /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-feedback_diff/feedback-NET_FLUX_SRF-NINO3-TS-NINO3.png\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/NET_FLUX_SRF-response/regression-coefficient-net_flux_srf-over-nino34.png\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-response/regression-coefficient-net_flux_srf-over-nino34.png\n", + " * Difference path /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/NET_FLUX_SRF-response_diff/regression-coefficient-net_flux_srf-over-nino34.png\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/PRECT-response/regression-coefficient-prect-over-nino34.png\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/PRECT-response/regression-coefficient-prect-over-nino34.png\n", + " * Difference path /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/PRECT-response_diff/regression-coefficient-prect-over-nino34.png\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/SHFLX-feedback/feedback-SHFLX-NINO3-TS-NINO3.png\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-feedback/feedback-SHFLX-NINO3-TS-NINO3.png\n", + " * Difference path /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-feedback_diff/feedback-SHFLX-NINO3-TS-NINO3.png\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/SHFLX-response/regression-coefficient-shflx-over-nino34.png\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-response/regression-coefficient-shflx-over-nino34.png\n", + " * Difference path /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/SHFLX-response_diff/regression-coefficient-shflx-over-nino34.png\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/TAUX-feedback/feedback-TAUX-NINO4-TS-NINO3.png\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-feedback/feedback-TAUX-NINO4-TS-NINO3.png\n", + " * Difference path /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-feedback_diff/feedback-TAUX-NINO4-TS-NINO3.png\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/TAUX-response/regression-coefficient-taux-over-nino34.png\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-response/regression-coefficient-taux-over-nino34.png\n", + " * Difference path /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUX-response_diff/regression-coefficient-taux-over-nino34.png\n", + "Comparing:\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/main/enso_diags/TAUY-response/regression-coefficient-tauy-over-nino34.png\n", + " * /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUY-response/regression-coefficient-tauy-over-nino34.png\n", + " * Difference path /global/cfs/cdirs/e3sm/www/cdat-migration-fy24/663-enso-diags/enso_diags/TAUY-response_diff/regression-coefficient-tauy-over-nino34.png\n" + ] + } + ], + "source": [ + "dev_glob = [file for file in DEV_GLOB if \"diff\" not in file]\n", + "for main_path, dev_path in zip(MAIN_GLOB, dev_glob):\n", + " print(\"Comparing:\")\n", + " print(f\" * {main_path}\")\n", + " print(f\" * {dev_path}\")\n", + "\n", + " get_image_diffs(dev_path, main_path)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Results\n", + "\n", + "- All plots are really close. The two extra latitude points for `\"ccb\"` in the CDAT code\n", + " influence the diffs. Specifically, the regression-coefficient plots for xCDAT show a missing\n", + " line at the bottom which is most likely due to the two extra latitude points not being included.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "cdat_regression_test", + "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.14" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/auxiliary_tools/cdat_regression_testing/663-enso-diags/run.cfg b/auxiliary_tools/cdat_regression_testing/663-enso-diags/run.cfg new file mode 100644 index 000000000..a34870b91 --- /dev/null +++ b/auxiliary_tools/cdat_regression_testing/663-enso-diags/run.cfg @@ -0,0 +1,140 @@ +[#] +sets = ["enso_diags"] +case_id = "PRECT-response" +variables = ["PRECT"] +ref_name = "GPCP_v2.3" +reference_name = "GPCP_v2.3" +regions = ["20S20N"] +seasons = ["ANN"] +reference_colormap = "diverging_bwr.rgb" +test_colormap = "diverging_bwr.rgb" +diff_colormap = "BrBG" +contour_levels = [-5,-3,-2,-1,-0.5,-0.2,0.2,0.5,1,2,3,5] +diff_levels = [-5,-3,-2,-1,-0.5,-0.2,0.2,0.5,1,2,3,5] + + +[#] +sets = ["enso_diags"] +case_id = "TAUX-response" +variables = ["TAUX"] +ref_name = "ERA-Interim" +reference_name = "ERA-Interim" +regions = ["20S20N"] +seasons = ["ANN"] +reference_colormap = "diverging_bwr.rgb" +test_colormap = "diverging_bwr.rgb" +diff_colormap = "BrBG" +contour_levels = [-0.1,-0.05,-0.02, -0.01,-0.005,-0.002, 0.002,0.005 ,0.01,0.02,0.05,0.1] +diff_levels = [-0.1,-0.05,-0.02, -0.01,-0.005,-0.002, 0.002,0.005, 0.01,0.02,0.05,0.1] + + +[#] +sets = ["enso_diags"] +case_id = "TAUY-response" +variables = ["TAUY"] +ref_name = "ERA-Interim" +reference_name = "ERA-Interim" +regions = ["20S20N"] +seasons = ["ANN"] +reference_colormap = "diverging_bwr.rgb" +test_colormap = "diverging_bwr.rgb" +diff_colormap = "BrBG" +contour_levels = [-0.05,-0.02, -0.01,-0.005,-0.002, 0.002,0.005 ,0.01,0.02,0.05] +diff_levels = [-0.05,-0.02, -0.01,-0.005,-0.002, 0.002,0.005, 0.01,0.02,0.05] + +[#] +sets = ["enso_diags"] +case_id = "LHFLX-response" +variables = ["LHFLX"] +ref_name = "ERA-Interim" +reference_name = "ERA-Interim" +regions = ["20S20N"] +seasons = ["ANN"] +reference_colormap = "diverging_bwr.rgb" +test_colormap = "diverging_bwr.rgb" +diff_colormap = "BrBG" +contour_levels = [-30, -25, -20,-15,-10,-5,-2.5,2.5, 5,10,15,20,25,30] +diff_levels = [-30, -25, -20,-15,-10,-5,-2.5,2.5, 5,10,15,20,25,30] + + +[#] +sets = ["enso_diags"] +case_id = "SHFLX-response" +variables = ["SHFLX"] +ref_name = "ERA-Interim" +reference_name = "ERA-Interim" +regions = ["20S20N"] +seasons = ["ANN"] +reference_colormap = "diverging_bwr.rgb" +test_colormap = "diverging_bwr.rgb" +diff_colormap = "BrBG" +contour_levels = [-15,-10,-5,-2.5,-1,-0.5,0.5,1,2.5, 5,10,15] +diff_levels = [-15,-10,-5,-2.5,-1,-0.5,0.5,1,2.5, 5,10,15] + +[#] +sets = ["enso_diags"] +case_id = "NET_FLUX_SRF-response" +variables = ["NET_FLUX_SRF"] +ref_name = "ERA-Interim" +reference_name = "ERA-Interim" +regions = ["20S20N"] +seasons = ["ANN"] +reference_colormap = "diverging_bwr.rgb" +test_colormap = "diverging_bwr.rgb" +diff_colormap = "BrBG" +contour_levels = [-30, -25, -20,-15,-10,-5,-2.5,2.5, 5,10,15,20,25,30] +diff_levels = [-30, -25, -20,-15,-10,-5,-2.5,2.5, 5,10,15,20,25,30] + +[#] +sets = ["enso_diags"] +plot_type = "scatter" +case_id = "TAUX-feedback" +variables = ["TAUX"] +seasons = ["ANN"] +ref_name = "ERA-Interim" +reference_name = "ERA-Interim" + +[#] +sets = ["enso_diags"] +plot_type = "scatter" +case_id = "LHFLX-feedback" +variables = ["LHFLX"] +seasons = ["ANN"] +ref_name = "ERA-Interim" +reference_name = "ERA-Interim" + +[#] +sets = ["enso_diags"] +plot_type = "scatter" +case_id = "SHFLX-feedback" +variables = ["SHFLX"] +seasons = ["ANN"] +ref_name = "ERA-Interim" +reference_name = "ERA-Interim" + +[#] +sets = ["enso_diags"] +plot_type = "scatter" +case_id = "FLNS-feedback" +variables = ["FLNS"] +seasons = ["ANN"] +ref_name = "ERA-Interim" +reference_name = "ERA-Interim" + +[#] +sets = ["enso_diags"] +plot_type = "scatter" +case_id = "FSNS-feedback" +variables = ["FSNS"] +seasons = ["ANN"] +ref_name = "ERA-Interim" +reference_name = "ERA-Interim" + +[#] +sets = ["enso_diags"] +plot_type = "scatter" +case_id = "NET_FLUX_SRF-feedback" +variables = ["NET_FLUX_SRF"] +seasons = ["ANN"] +ref_name = "ERA-Interim" +reference_name = "ERA-Interim" diff --git a/auxiliary_tools/cdat_regression_testing/663-enso-diags/run_script.py b/auxiliary_tools/cdat_regression_testing/663-enso-diags/run_script.py new file mode 100644 index 000000000..dfdd2072a --- /dev/null +++ b/auxiliary_tools/cdat_regression_testing/663-enso-diags/run_script.py @@ -0,0 +1,14 @@ +# %% +# python -m auxiliary_tools.cdat_regression_testing.663-enso-diags.run_script +from auxiliary_tools.cdat_regression_testing.base_run_script import run_set + +SET_NAME = "enso_diags" +SET_DIR = "663-enso-diags" +CFG_PATH: str | None = None +# CFG_PATH: str | None = "/global/u2/v/vo13/E3SM-Project/e3sm_diags/auxiliary_tools/cdat_regression_testing/663-enso-diags/run.cfg" +MULTIPROCESSING = True + +# %% +run_set(SET_NAME, SET_DIR, CFG_PATH, MULTIPROCESSING) + +# %% diff --git a/e3sm_diags/driver/enso_diags_driver.py b/e3sm_diags/driver/enso_diags_driver.py index ff060110a..bf56c3431 100644 --- a/e3sm_diags/driver/enso_diags_driver.py +++ b/e3sm_diags/driver/enso_diags_driver.py @@ -1,21 +1,24 @@ from __future__ import annotations -import json import math import os -from typing import TYPE_CHECKING - -import cdms2 -import cdutil -import numpy -import scipy.stats - -import e3sm_diags -from e3sm_diags.derivations import default_regions -from e3sm_diags.driver import utils +from typing import TYPE_CHECKING, Dict, List, Literal, Tuple, TypedDict, Union + +import numpy as np +import xarray as xr +import xcdat as xc +import xskillscore as xs + +from e3sm_diags import INSTALL_PATH +from e3sm_diags.driver.utils.dataset_xr import Dataset +from e3sm_diags.driver.utils.io import ( + _save_data_metrics_and_plots, + _write_vars_to_netcdf, +) +from e3sm_diags.driver.utils.regrid import _subset_on_region, align_grids_to_lower_res from e3sm_diags.logger import custom_logger -from e3sm_diags.metrics import corr, max_cdms, mean, min_cdms, rmse, std -from e3sm_diags.plot.cartopy.enso_diags_plot import plot_map, plot_scatter +from e3sm_diags.metrics.metrics import spatial_avg, std +from e3sm_diags.plot.enso_diags_plot import plot_map, plot_scatter if TYPE_CHECKING: from e3sm_diags.parameter.enso_diags_parameter import EnsoDiagsParameter @@ -24,170 +27,36 @@ logger = custom_logger(__name__) -def calculate_nino_index(nino_region_str, parameter, test=False, ref=False): - """ - Use the built-in HadISST nino index time series from http://www.esrl.noaa.gov/psd/gcos_wgsp/Timeseries/ - for observation datasets and when model output is not available. - - Relevant files are e3sm_diags/driver/default_diags/enso_NINO{3, 34, 4}.long.data - """ - data_file = "".join(["enso_", nino_region_str, ".long.data"]) - nino_index_path = os.path.join(e3sm_diags.INSTALL_PATH, "enso_diags", data_file) - sst_orig = numpy.loadtxt( - nino_index_path, skiprows=1, max_rows=149 - ) # load data up to year 2018 from 1870 - if test: - start = int(parameter.test_start_yr) - end = int(parameter.test_end_yr) - elif ref: - start = int(parameter.ref_start_yr) - end = int(parameter.ref_end_yr) - else: - start = int(parameter.start_yr) - end = int(parameter.end_yr) - sst_years = sst_orig[:, 0].astype(int) - try: - start_ind = numpy.where(sst_years == start)[0][0] - end_ind = numpy.where(sst_years == end)[0][0] - except Exception: - msg = "Requested years are outside of available sst obs records." - raise RuntimeError(msg) - - sst = sst_orig[start_ind : end_ind + 1, 1:] - # Get anomaly from annual cycle climatology - annual_cycle = numpy.mean(sst, axis=0) - sst_anomaly = numpy.empty_like(sst) - num_years = end - start + 1 - for iyr in range(num_years): - sst_anomaly[iyr, :] = sst[iyr, :] - annual_cycle - nino_index = numpy.reshape(sst_anomaly, (num_years * 12)) +class MetricsDictScatter(TypedDict): + """A TypedDict class representing metrics for the scatter driver""" - if parameter.print_statements: - logger.info(f"nino_index_obs {nino_index}") - return nino_index + var: str + units: str + region: str + test: xr.DataArray + ref: xr.DataArray -def calculate_nino_index_model(data, nino_region_str, parameter): - """ - Calculate nino index based on model output SST or TS. If neither of these is available, - then use the built-in HadISST nino index. - """ +class MetricsSubDict(TypedDict): + """A TypedDict class representing the metrics sub-dictionary.""" - try: - try: - # Try sea surface temperature first. - sst = data.get_timeseries_variable("SST") - except RuntimeError as e1: - if str(e1).startswith("Neither does SST nor the variables in"): - logger.info( - "Handling the following exception by looking for surface " - f"temperature: {e1}", - ) - # Try surface temperature. - sst = data.get_timeseries_variable("TS") - logger.info( - "Simulated sea surface temperature not found, using surface temperature instead." - ) - else: - raise e1 - nino_region = default_regions.regions_specs[nino_region_str]["domain"] # type: ignore - sst_nino = sst(nino_region) - # Domain average - sst_avg = cdutil.averager(sst_nino, axis="xy") - # Get anomaly from annual cycle climatology - sst_avg_anomaly = cdutil.ANNUALCYCLE.departures(sst_avg) - nino_index = sst_avg_anomaly - except RuntimeError as e2: - logger.info( - "Handling the following exception by trying built-in HadISST nino index " - f"time series: {e2}" - ) - test = data.test - ref = data.ref - nino_index = calculate_nino_index( - nino_region_str, parameter, test=test, ref=ref - ) - logger.info( - "Simulated surface temperature not found, using built-in HadISST nino index time series instead." - ) + min: float + max: float + mean: List[float] + std: List[float] - if parameter.print_statements: - logger.info(f"nino_index_model {nino_index}") - return nino_index +# A type annotation representing the metrics dictionary. +MetricsDictMap = Dict[str, Union[MetricsSubDict, str]] -def perform_regression(data, parameter, var, region, land_frac, ocean_frac, nino_index): - ts_var = data.get_timeseries_variable(var) - domain = utils.general.select_region( - region, ts_var, land_frac, ocean_frac, parameter - ) - # Average over selected region, and average - # over months to get the yearly mean. - cdutil.setTimeBoundsMonthly(domain) - # Get anomaly from annual cycle climatology - if parameter.print_statements: - logger.info("domain.shape: {}".format(domain.shape)) - anomaly = cdutil.ANNUALCYCLE.departures(domain) - nlat = len(anomaly.getLatitude()) - nlon = len(anomaly.getLongitude()) - reg_coe = anomaly[0, :, :](squeeze=1) - confidence_levels = cdutil.ANNUALCYCLE.departures(domain)[0, :, :](squeeze=1) - # Neither of the following methods work, so we just set values in confidence_levels - # to be explicitly 0 or 1. - # confidence_levels = anomaly[0, :, :](squeeze=1).fill(0) - # confidence_levels = numpy.zeros_like(reg_coe) - for ilat in range(nlat): - if parameter.print_statements: - logger.info("ilat: {}".format(ilat)) - for ilon in range(nlon): - dependent_var = anomaly[:, ilat, ilon] - independent_var = nino_index - # Uncomment the following line to use CDAT/genutil instead - # (You'll also need to set pvalue) - # slope, intercept = genutil.statistics.linearregression(dependent_var, x=independent_var) - slope, _, _, pvalue, _ = scipy.stats.linregress( - independent_var, dependent_var - ) - reg_coe[ilat, ilon] = slope - # Set confidence level to 1 if significant and 0 if not - if pvalue < 0.05: - # p-value < 5% - # This implies significance at 95% confidence level - confidence_levels[ilat, ilon] = 1 - else: - confidence_levels[ilat, ilon] = 0 - if parameter.print_statements: - logger.info(f"confidence in fn: {confidence_levels.shape}") - sst_units = "degC" - reg_coe.units = "{}/{}".format(ts_var.units, sst_units) - if parameter.print_statements: - logger.info("reg_coe.shape: {}".format(reg_coe.shape)) - return domain, reg_coe, confidence_levels - - -def create_single_metrics_dict(values): - d = { - "min": float(min_cdms(values)), - "max": float(max_cdms(values)), - "mean": float(mean(values)), - "std": float(std(values)), - } - return d - - -def create_metrics(ref, test, ref_regrid, test_regrid, diff): - """Creates the relevant metrics in a dictionary""" - metrics_dict = {} - metrics_dict["ref"] = create_single_metrics_dict(ref) - metrics_dict["ref_regrid"] = create_single_metrics_dict(ref_regrid) - metrics_dict["test"] = create_single_metrics_dict(test) - metrics_dict["test_regrid"] = create_single_metrics_dict(test_regrid) - metrics_dict["diff"] = create_single_metrics_dict(diff) - d = metrics_dict["diff"] - d["rmse"] = float(rmse(test_regrid, ref_regrid)) - d["corr"] = float(corr(test_regrid, ref_regrid)) - return metrics_dict +def run_diag(parameter: EnsoDiagsParameter) -> EnsoDiagsParameter: + if parameter.plot_type == "map": + return run_diag_map(parameter) + elif parameter.plot_type == "scatter": + return run_diag_scatter(parameter) + else: + raise Exception("Invalid plot_type={}".format(parameter.plot_type)) def run_diag_map(parameter: EnsoDiagsParameter) -> EnsoDiagsParameter: @@ -197,255 +66,542 @@ def run_diag_map(parameter: EnsoDiagsParameter) -> EnsoDiagsParameter: nino_region_str = parameter.nino_region run_type = parameter.run_type - if parameter.print_statements: - logger.info("run_type: {}".format(run_type)) - test_data = utils.dataset.Dataset(parameter, test=True) - ref_data = utils.dataset.Dataset(parameter, ref=True) + logger.info("run_type: {}".format(run_type)) + + test_ds = Dataset(parameter, data_type="test") + ref_ds = Dataset(parameter, data_type="ref") + if run_type == "model_vs_model": - test_nino_index = calculate_nino_index_model( - test_data, nino_region_str, parameter - ) - ref_nino_index = calculate_nino_index_model( - ref_data, nino_region_str, parameter - ) + da_test_nino = calculate_nino_index_model(test_ds, parameter, nino_region_str) + da_ref_nino = calculate_nino_index_model(ref_ds, parameter, nino_region_str) elif run_type == "model_vs_obs": - test_nino_index = calculate_nino_index_model( - test_data, nino_region_str, parameter + da_test_nino = calculate_nino_index_model(test_ds, parameter, nino_region_str) + da_ref_nino = calculate_nino_index_obs( + parameter, nino_region_str, data_type="ref" ) - ref_nino_index = calculate_nino_index(nino_region_str, parameter, ref=True) else: raise Exception("Invalid run_type={}".format(run_type)) for season in seasons: - if parameter.print_statements: - logger.info("Season: {}".format(season)) - # Get the name of the data, appended with the years averaged. - parameter.test_name_yrs = utils.general.get_name_and_yrs( - parameter, test_data, season - ) - parameter.ref_name_yrs = utils.general.get_name_and_yrs( - parameter, ref_data, season - ) + logger.info("Season: {}".format(season)) + parameter._set_name_yrs_attrs(test_ds, ref_ds, season) - # Get land/ocean fraction for masking. - try: - land_frac = test_data.get_climo_variable("LANDFRAC", season) - ocean_frac = test_data.get_climo_variable("OCNFRAC", season) - except Exception: - mask_path = os.path.join( - e3sm_diags.INSTALL_PATH, "acme_ne30_ocean_land_mask.nc" - ) - with cdms2.open(mask_path) as f: - land_frac = f("LANDFRAC") - ocean_frac = f("OCNFRAC") + for var_key in variables: + logger.info("Variable: {}".format(var_key)) + parameter.var_id = "{}-regression-over-nino".format(var_key) - for var in variables: - if parameter.print_statements: - logger.info("Variable: {}".format(var)) - parameter.var_id = "{}-regression-over-nino".format(var) + ds_test = test_ds.get_time_series_dataset(var_key) + ds_ref = ref_ds.get_time_series_dataset(var_key) for region in regions: - if parameter.print_statements: - logger.info("Selected region: {}".format(region)) - - # This will be the title of the plot. - parameter.main_title = ( - "Regression coefficient, {} anomaly to {}".format( - var, nino_region_str - ) - ) - parameter.viewer_descr[var] = ", ".join([parameter.main_title, region]) + logger.info("Selected region: {}".format(region)) - # Test - ( - test_domain, - test_reg_coe, - test_confidence_levels, - ) = perform_regression( - test_data, - parameter, - var, - region, - land_frac, - ocean_frac, - test_nino_index, + ds_test_reg_coe, da_test_conf_lvls = calc_linear_regression( + ds_test, da_test_nino, var_key, region ) - - # Reference - ( - ref_domain, - ref_reg_coe, - ref_confidence_levels, - ) = perform_regression( - ref_data, - parameter, - var, - region, - land_frac, - ocean_frac, - ref_nino_index, + ds_ref_reg_coe, da_ref_conf_lvls = calc_linear_regression( + ds_ref, da_ref_nino, var_key, region ) - # Difference - # Regrid towards the lower resolution of the two variables for calculating the difference. ( - test_reg_coe_regrid, - ref_reg_coe_regrid, - ) = utils.general.regrid_to_lower_res( - test_reg_coe, - ref_reg_coe, + ds_test_reg_coe_regrid, + ds_ref_reg_coe_regrid, + ) = align_grids_to_lower_res( + ds_test_reg_coe, + ds_ref_reg_coe, + var_key, parameter.regrid_tool, parameter.regrid_method, ) - diff = test_reg_coe_regrid - ref_reg_coe_regrid - - # Metrics - metrics_dict = create_metrics( - ref_reg_coe, - test_reg_coe, - ref_reg_coe_regrid, - test_reg_coe_regrid, - diff, + + ds_diff_reg_coe = ds_test_reg_coe_regrid.copy() + ds_diff_reg_coe[var_key] = ( + ds_diff_reg_coe[var_key] - ds_ref_reg_coe_regrid[var_key] + ) + + metrics_dict = _create_metrics_dict( + ds_test_reg_coe, + ds_test_reg_coe_regrid, + ds_ref_reg_coe, + ds_ref_reg_coe_regrid, + ds_diff_reg_coe, + var_key, ) - # If not defined, determine contour_levels + if not parameter.contour_levels or not parameter.diff_levels: - # We want contour levels for the plot, - # which uses original (non-regridded) test and ref, - # so we use those min and max values. - min_contour_level = math.floor( - min( - metrics_dict["ref"]["min"], - metrics_dict["test"]["min"], - ) - ) - max_contour_level = math.ceil( - max( - metrics_dict["ref"]["max"], - metrics_dict["test"]["max"], - ) - ) - CENTER_ON_ZERO = True - if CENTER_ON_ZERO: - bound = max(abs(max_contour_level), abs(min_contour_level)) - lower_bound = -bound - upper_bound = bound - contour_level_range = 2 * bound - else: - lower_bound = min_contour_level - upper_bound = max_contour_level - contour_level_range = max_contour_level - min_contour_level - step_size = contour_level_range / 10 - if step_size > 1: - step_size = int(step_size) - elif step_size == 0: - step_size = 1 / 10 - contour_levels = list( - numpy.arange(lower_bound, upper_bound + 1, step_size) - ) - if not parameter.contour_levels: - parameter.contour_levels = contour_levels - if not parameter.diff_levels: - parameter.diff_levels = contour_levels - parameter.output_file = "regression-coefficient-{}-over-{}".format( - var.lower(), nino_region_str.lower() - ) + contour_levels = _get_contour_levels(metrics_dict) + parameter.contour_levels = parameter.diff_levels = contour_levels - # Saving the metrics as a json. - metrics_dict["unit"] = test_reg_coe_regrid.units - metrics_output_file_name = os.path.join( - utils.general.get_output_dir(parameter.current_set, parameter), - parameter.output_file + ".json", + parameter.main_title = ( + f"Regression coefficient, {var_key} anomaly to {nino_region_str}" ) - with open(metrics_output_file_name, "w") as outfile: - json.dump(metrics_dict, outfile) - # Get the file name that the user has passed in and display that. - metrics_output_file_name = os.path.join( - utils.general.get_output_dir(parameter.current_set, parameter), - parameter.output_file + ".json", + parameter.output_file = ( + f"regression-coefficient-{var_key.lower()}-over-" + f"{nino_region_str.lower()}" ) - logger.info("Metrics saved in: {}".format(metrics_output_file_name)) - - # Plot parameter.var_region = region - # Plot original ref and test, not regridded versions. - plot_map( - ref_reg_coe, - test_reg_coe, - diff, - metrics_dict, - ref_confidence_levels, - test_confidence_levels, - parameter, - ) - utils.general.save_ncfiles( - parameter.current_set, - test_reg_coe, - ref_reg_coe, - diff, + + _save_data_metrics_and_plots( parameter, + plot_map, + var_key, + ds_test_reg_coe, + ds_ref_reg_coe, + ds_diff_reg_coe, + metrics_dict, # type: ignore + plot_kwargs={ + "da_test_conf_lvls": da_test_conf_lvls, + "da_ref_conf_lvls": da_ref_conf_lvls, + }, + viewer_descr=", ".join([parameter.main_title, region]), ) + return parameter def run_diag_scatter(parameter: EnsoDiagsParameter) -> EnsoDiagsParameter: variables = parameter.variables run_type = parameter.run_type - # We will always use the same regions, so we don't do the following: - # x['region'] = parameter.nino_region - # y['region'] = parameter.regions[0] - x = {"var": "TS", "units": "degC", "region": "NINO3"} - test_data = utils.dataset.Dataset(parameter, test=True) - ref_data = utils.dataset.Dataset(parameter, ref=True) - if parameter.print_statements: - logger.info("run_type: {}".format(run_type)) + + logger.info("run_type: {}".format(run_type)) + + metrics_dict: MetricsDictScatter = { + "var": "TS", + "units": "degC", + "region": "NINO3", + "test": xr.DataArray(), + "ref": xr.DataArray(), + } + + test_ds = Dataset(parameter, data_type="test") + ref_ds = Dataset(parameter, data_type="ref") + + parameter._set_name_yrs_attrs(test_ds, ref_ds, None) + if run_type == "model_vs_model": - x["test"] = calculate_nino_index_model(test_data, x["region"], parameter) - x["ref"] = calculate_nino_index_model(ref_data, x["region"], parameter) + metrics_dict["test"] = calculate_nino_index_model( + test_ds, parameter, metrics_dict["region"] + ) + metrics_dict["ref"] = calculate_nino_index_model( + ref_ds, parameter, metrics_dict["region"] + ) elif run_type == "model_vs_obs": - x["test"] = calculate_nino_index_model(test_data, x["region"], parameter) - x["ref"] = calculate_nino_index(x["region"], parameter, ref=True) + metrics_dict["test"] = calculate_nino_index_model( + test_ds, parameter, metrics_dict["region"] + ) + metrics_dict["ref"] = calculate_nino_index_obs( + parameter, metrics_dict["region"], "ref" + ) else: raise Exception("Invalid run_type={}".format(run_type)) - parameter.test_name_yrs = utils.general.get_name_and_yrs(parameter, test_data) - parameter.ref_name_yrs = utils.general.get_name_and_yrs(parameter, ref_data) - for y_var in variables: - if y_var == "TAUX": + + for var_key in variables: + logger.info("Variable: {}".format(var_key)) + if var_key == "TAUX": regions = ["NINO4"] else: regions = ["NINO3"] + for region in regions: - y = {"var": y_var, "region": region} - test_data_ts = test_data.get_timeseries_variable(y_var) - ref_data_ts = ref_data.get_timeseries_variable(y_var) - y_region = default_regions.regions_specs[region]["domain"] # type: ignore - test_data_ts_regional = test_data_ts(y_region) - ref_data_ts_regional = ref_data_ts(y_region) + y: MetricsDictScatter = { + "var": var_key, + "region": region, + "units": "", + "test": xr.DataArray(), + "ref": xr.DataArray(), + } + + ds_test = test_ds.get_time_series_dataset(var_key) + ds_ref = ref_ds.get_time_series_dataset(var_key) + + ds_test_region = _subset_on_region(ds_test, var_key, region) + ds_ref_region = _subset_on_region(ds_ref, var_key, region) + # Domain average - test_avg = cdutil.averager(test_data_ts_regional, axis="xy") - ref_avg = cdutil.averager(ref_data_ts_regional, axis="xy") + ds_test_avg = ds_test_region.spatial.average(var_key) + ds_ref_avg = ds_ref_region.spatial.average(var_key) + # Get anomaly from annual cycle climatology - y["test"] = cdutil.ANNUALCYCLE.departures(test_avg) - y["ref"] = cdutil.ANNUALCYCLE.departures(ref_avg) - y["units"] = test_avg.units - if y_var == "TAUX": + y["test"] = ds_test_avg.temporal.departures(var_key, freq="month")[var_key] + y["ref"] = ds_ref_avg.temporal.departures(var_key, freq="month")[var_key] + y["units"] = y["test"].attrs["units"] + + if var_key == "TAUX": y["test"] *= 1000 y["ref"] *= 1000 y["units"] = "10^3 {}".format(y["units"]) - parameter.var_id = "{}-feedback".format(y["var"]) - title_tuple = (y["var"], y["region"], x["var"], x["region"]) + + parameter.var_id = f"{y['var']}-feedback" + + title_tuple = ( + y["var"], + y["region"], + metrics_dict["var"], + metrics_dict["region"], + ) parameter.main_title = "{} anomaly ({}) vs. {} anomaly ({})".format( *title_tuple ) parameter.viewer_descr[y["var"]] = parameter.main_title parameter.output_file = "feedback-{}-{}-{}-{}".format(*title_tuple) - plot_scatter(x, y, parameter) + + plot_scatter(parameter, metrics_dict, y) + + if parameter.save_netcdf: + _write_vars_to_netcdf( + parameter, + var_key, + y["test"].to_dataset(), + y["ref"].to_dataset(), + None, + ) + return parameter -def run_diag(parameter: EnsoDiagsParameter) -> EnsoDiagsParameter: - if parameter.plot_type == "map": - return run_diag_map(parameter) - elif parameter.plot_type == "scatter": - return run_diag_scatter(parameter) - else: - raise Exception("Invalid plot_type={}".format(parameter.plot_type)) +def calculate_nino_index_model( + ds_obj: Dataset, + parameter: EnsoDiagsParameter, + nino_region_str: str, +) -> xr.DataArray: + """Calculate nino index based on model output SST or TS. + + If neither of these is available, then use the built-in HadISST nino index. + + Parameters + ---------- + ds_obj : Dataset + The dataset object. + parameter : EnsoDiagsParameter + The parameter object. + nino_region_str : str + The nino region. + + Returns + ------- + xr.DataArray + The nino index based on the model output. + + Raises + ------ + RunTimeError + If the "SST" or "TS" variables are not found. + """ + try: + try: + # Try sea surface temperature first. + sst = ds_obj.get_time_series_dataset("SST") + nino_var_key = "SST" + except IOError as e1: + if str(e1).startswith("No time series `.nc` file was found for 'SST' in"): + logger.info( + "Handling the following exception by looking for surface " + f"temperature: {e1}", + ) + # Try surface temperature. + sst = ds_obj.get_time_series_dataset("TS") + nino_var_key = "TS" + + logger.info( + "Simulated sea surface temperature not found, using surface " + "temperature instead." + ) + else: + raise e1 + + sst_nino = _subset_on_region(sst, nino_var_key, nino_region_str) + + # Domain average + sst_avg = sst_nino.spatial.average(nino_var_key, axis=["X", "Y"]) + + # Get anomaly from annual cycle climatology + sst_avg_anomaly = sst_avg.temporal.departures(nino_var_key, freq="month") + da_nino = sst_avg_anomaly[nino_var_key] + + except IOError as e2: + logger.info( + "Handling the following exception by trying built-in HadISST nino index " + f"time series: {e2}" + ) + da_nino = calculate_nino_index_obs(parameter, nino_region_str, ds_obj.data_type) + logger.info( + "Simulated surface temperature not found, using built-in HadISST nino " + "index time series instead." + ) + + return da_nino + + +def calculate_nino_index_obs( + parameter: EnsoDiagsParameter, + nino_region_str: str, + data_type: Literal["test", "ref"], +) -> xr.DataArray: + """Calculate the nino index using default observational datasets. + + This function uses the default HadISST nino index time series from + http://www.esrl.noaa.gov/psd/gcos_wgsp/Timeseries/ for observation datasets + and when model output is not available. + + Relevant files are e3sm_diags/driver/default_diags/enso_NINO{3, 34, 4}.long.data + + Parameters + ---------- + parameter : EnsoDiagsParameter + The parameter object. + nino_region_str : str + The nino region. + data_type : {"test", "ref"} + The data type, either "test" or "ref". + + Returns + ------- + xr.DataArray + The nino index. + + Raises + ------ + RuntimeError + If the requested years are outside the SST observational records. + """ + data_file = "".join(["enso_", nino_region_str, ".long.data"]) + nino_index_path = os.path.join(INSTALL_PATH, "enso_diags", data_file) + + # Load data up to year 2018 from 1870 + sst_orig = np.loadtxt(nino_index_path, skiprows=1, max_rows=149) + + if data_type == "test": + start = int(parameter.test_start_yr) + end = int(parameter.test_end_yr) + elif data_type == "ref": + start = int(parameter.ref_start_yr) + end = int(parameter.ref_end_yr) + + sst_years = sst_orig[:, 0].astype(int) + + try: + start_ind = np.where(sst_years == start)[0][0] + end_ind = np.where(sst_years == end)[0][0] + except Exception: + msg = "Requested years are outside of available sst obs records." + raise RuntimeError(msg) + + sst = sst_orig[start_ind : end_ind + 1, 1:] + + # Get anomaly from annual cycle climatology + annual_cycle = np.mean(sst, axis=0) + sst_anomaly = np.empty_like(sst) + num_years = end - start + 1 + + for iyr in range(num_years): + sst_anomaly[iyr, :] = sst[iyr, :] - annual_cycle + + len_months = num_years * 12 + nino_index = np.reshape(sst_anomaly, len_months) + + da_nino_index = xr.DataArray( + name="SST", data=nino_index, dims="time", coords={"time": np.arange(len_months)} + ) + + return da_nino_index + + +def calc_linear_regression( + ds: xr.Dataset, + da_nino: xr.DataArray, + var_key: str, + region: str, +) -> Tuple[xr.Dataset, xr.DataArray]: + """Calculate the linear regression between the variable and the nino index. + + Parameters + ---------- + ds : xr.Dataset + The dataset containing the variable. + da_nino : xr.DataArray + The nino index. + var_key : str + The key of the variable. + region : str + The nino region. + + Returns + ------- + Tuple[xr.Dataset, xr.DataArray] + A tuple containing the regression coefficient dataset and the + confidence levels dataarray. + """ + # Average over selected region, and average over months to get the yearly mean. + domain = _subset_on_region(ds, var_key, region) + + # Get anomaly from annual cycle climatology + anomaly = domain.temporal.departures(var_key, freq="month") + anomaly_var = anomaly[var_key].copy() + + # Align the time coordinates to enable Xarray alignment before calculating + # linear slope. This is necessary when the reference nino index is + # from observation data. + independent_var = da_nino.copy() + independent_var = _align_time_coords(anomaly_var, independent_var) + + reg_coe = xs.linslope(independent_var, anomaly_var, keep_attrs=True) + + # Set confidence level to 1 if significant and 0 if not. + # p-value < 5%, implies significance at 95% confidence level. + conf_lvls = xs.pearson_r_p_value(independent_var, anomaly_var, keep_attrs=True) + conf_lvls_masked = xr.where(conf_lvls < 0.05, 1, 0, keep_attrs=True) + + sst_units = "degC" + reg_coe.attrs["units"] = "{}/{}".format(ds[var_key].units, sst_units) + + reg_coe.name = var_key + conf_lvls_masked.name = var_key + + ds_reg_coe = reg_coe.to_dataset() + ds_reg_coe = ds_reg_coe.bounds.add_missing_bounds(axes=["X", "Y", "T"]) + + return ds_reg_coe, conf_lvls_masked + + +def _align_time_coords(da: xr.DataArray, da_nino_index: xr.DataArray) -> xr.DataArray: + """Align the nino index time coordinates to the variable. + + The reference data is calculated using a nino index from observation data. + The time coordinates of the nino index is updated to align with the + reference data for downstream operations using Xarray broadcasting + and alignment. + + Parameters + ---------- + da_test : xr.DataArray + The variable. + da_nino_index : xr.DataArray + The nino index variable. + + Returns + ------- + xr.DataArray + The nino index variable with time coordinates aligned to the variable. + """ + time_coords = xc.get_dim_coords(da, axis="T") + + da_ref_new = da_nino_index.assign_coords({"time": time_coords}) + + return da_ref_new + + +def _create_metrics_dict( + ds_test: xr.Dataset, + ds_test_regrid: xr.Dataset, + ds_ref: xr.Dataset, + ds_ref_regrid: xr.Dataset, + ds_diff: xr.Dataset, + var_key: str, +) -> MetricsDictMap: + """Calculate metrics using the variable in the datasets. + + Metrics include min value, max value, spatial average (mean), and standard + deviation. + + Parameters + ---------- + var_key : str + The variable key. + ds_test : xr.Dataset + The test dataset. + ds_test_regrid : xr.Dataset + The regridded test dataset. + ds_ref : xr.Dataset + The reference dataset. + ds_ref_regrid : xr.Dataset + The regridded reference dataset. + ds_diff : xr.Dataset | None + The difference between ``ds_test_regrid`` and ``ds_ref_regrid``. + + Returns + ------- + MetricsDict + A dictionary with the key being a string and the value being a + sub-dictionary (key is metric and value is float). + """ + metrics_dict: MetricsDictMap = {} + + metrics_dict["ref"] = get_metrics_subdict(ds_ref, var_key) + metrics_dict["ref_regrid"] = get_metrics_subdict(ds_ref_regrid, var_key) + metrics_dict["test"] = get_metrics_subdict(ds_test, var_key) + metrics_dict["test_regrid"] = get_metrics_subdict(ds_test_regrid, var_key) + metrics_dict["diff"] = get_metrics_subdict(ds_diff, var_key) + + metrics_dict["unit"] = ds_test[var_key].units + + return metrics_dict + + +def get_metrics_subdict(ds: xr.Dataset, var_key: str) -> MetricsSubDict: + """Get the metrics sub-dictionary. + + Parameters + ---------- + ds : xr.Dataset + The dataset object. + var_key : str + The key of the variable to calculate metrics for. + + Returns + ------- + MetricsSubDict + A dictionary of metrics. + """ + metrics_subdict: MetricsSubDict = { + "min": ds[var_key].min().item(), + "max": ds[var_key].max().item(), + "mean": spatial_avg(ds, var_key, axis=["X", "Y"], as_list=True), # type: ignore + "std": std(ds, var_key), + } + return metrics_subdict + + +def _get_contour_levels(metrics_dict: MetricsDictMap) -> List[float]: + """Get the contour levels for the map plot. + + The non-regridded data ("test" and "ref") are used for their min and max + values. + + Parameters + ---------- + metrics_dict : MetricsDictMap + The metrics dictionary. + + Returns + ------- + List[float] + A list of floats representing contour levels. + """ + min_contour_level = math.floor( + min( + metrics_dict["ref"]["min"], # type: ignore + metrics_dict["test"]["min"], # type: ignore + ) + ) + max_contour_level = math.ceil( + max( + metrics_dict["ref"]["max"], # type: ignore + metrics_dict["test"]["max"], # type: ignore + ) + ) + + # Center on zero. + bound = max(abs(max_contour_level), abs(min_contour_level)) + lower_bound = -bound + upper_bound = bound + contour_level_range = 2 * bound + + step_size = contour_level_range / 10 + if step_size > 1: + step_size = int(step_size) + elif step_size == 0: + step_size = 1 / 10 + + contour_levels = list(np.arange(lower_bound, upper_bound + 1, step_size)) + + return contour_levels diff --git a/e3sm_diags/driver/qbo_driver.py b/e3sm_diags/driver/qbo_driver.py index d05952691..e8ec32f49 100644 --- a/e3sm_diags/driver/qbo_driver.py +++ b/e3sm_diags/driver/qbo_driver.py @@ -109,11 +109,7 @@ def run_diag(parameter: QboParameter) -> QboParameter: # Write the metrics to .json files. test_dict["name"] = test_ds._get_test_name() - - try: - ref_dict["name"] = ref_ds._get_ref_name() - except AttributeError: - ref_dict["name"] = parameter.ref_name + ref_dict["name"] = ref_ds._get_ref_name() _save_metrics_to_json(parameter, test_dict, "test") # type: ignore _save_metrics_to_json(parameter, ref_dict, "ref") # type: ignore diff --git a/e3sm_diags/driver/utils/dataset_xr.py b/e3sm_diags/driver/utils/dataset_xr.py index 86a30c0a1..7645d0c60 100644 --- a/e3sm_diags/driver/utils/dataset_xr.py +++ b/e3sm_diags/driver/utils/dataset_xr.py @@ -16,7 +16,7 @@ import glob import os import re -from datetime import datetime +from datetime import datetime, timedelta from typing import TYPE_CHECKING, Callable, Dict, Literal, Tuple import pandas as pd @@ -37,7 +37,6 @@ if TYPE_CHECKING: from e3sm_diags.parameter.core_parameter import CoreParameter - logger = custom_logger(__name__) # A constant variable that defines the pattern for time series filenames. @@ -239,11 +238,13 @@ def _get_test_name(self) -> str: return self.parameter.short_test_name elif self.parameter.test_name != "": return self.parameter.test_name + else: + # NOTE: This else statement is preserved from the previous CDAT + # codebase to maintain the same behavior. + if self.parameter.test_name == "": + logger.warning("No test name was set, using empty string as test name.") - raise AttributeError( - "Either `parameter.short_test_name` or `parameter.test_name attributes` " - "must be set to get the name and years attribute for test datasets." - ) + return self.parameter.test_name def _get_ref_name(self) -> str: """Get the diagnostic reference name. @@ -261,14 +262,15 @@ def _get_ref_name(self) -> str: return self.parameter.short_ref_name elif self.parameter.reference_name != "": return self.parameter.reference_name - elif self.parameter.ref_name != "": - return self.parameter.ref_name + else: + # NOTE: This else statement is preserved from the previous CDAT + # codebase to maintain the same behavior. + if self.parameter.ref_name == "": + logger.warning( + "No reference name was set, using empty string as reference name." + ) - raise AttributeError( - "Either `parameter.short_ref_name`, `parameter.reference_name`, or " - "`parameter.ref_name` must be set to get the name and years attribute for " - "reference datasets." - ) + return self.parameter.ref_name def _get_global_attr_from_climo_dataset( self, attr: str, season: ClimoFreq @@ -1238,66 +1240,118 @@ def _get_time_slice(self, ds: xr.Dataset, filename: str) -> slice: start_time = f"{start_yr_str}-01-01" end_time = f"{str(int(end_yr_str) + 1)}-01-01" else: - start_time = f"{start_yr_str}-01-15" - end_time = self._get_end_time_with_bounds(ds, end_yr_str) + start_time = self._get_slice_with_bounds(ds, start_yr_str, "start") + end_time = self._get_slice_with_bounds(ds, end_yr_str, "end") return slice(start_time, end_time) - def _get_end_time_with_bounds(self, ds: xr.Dataset, old_end_time_str: str) -> str: - """Get the end time for non-submonthly data using bounds. + def _get_slice_with_bounds( + self, ds: xr.Dataset, year_str: str, slice_type: Literal["start", "end"] + ) -> str: + """Get the time slice for non-submonthly data using bounds if needed. For example, let's say we have non-submonthly time coordinates with a - start time of "2011-01-01" and end time of "2014-01-01". We pre-define + start time and end time of ("2011-01-01", "2014-01-01"). We pre-define a time slice of ("2011-01-15", "2013-12-15"). However slicing with an end time of "2013-12-15" will exclude the last coordinate - "2014-01-01". To rectify this situation, we use time bounds to extend - the coordinates like so: + "2014-01-01". To rectify this situation, we use the time delta between + time bounds to extend the end time slice to "2014-01-15". 1. Get the time delta between bound values ["2013-12-15", "2014-01-15"], which is one month. 2. Add the time delta to the old end time of "2013-12-15", which - results in a new end time of "2014-01-15". + results in a new end time of "2014-01-15" + - Time delta is subtracted if start time slice needs to be + adjusted. 3. Now slice the time coordinates using ("2011-01-15", "2014-01-15"). - This new time slice will correctly subset to include the last - coordinate value of "2014-01-01". + Xarray will now correctly correctly subset to include the last + coordinate value of "2014-01-01" using this time slice. Parameters ---------- ds : xr.Dataset The dataset. - old_end_time_str : str - The old end time string. + year_str : str + The year string for the slice. + slice_type : Literal["start", "end"] + The slice type, start or end. Returns ------- str - The new end time string. + The new time slice type. Notes ----- This function replicates the cdms2/cdutil "ccb" slice flag used for subsetting. "ccb" only allows the right side to be closed. It will get - the difference between bounds values and add it to the last coordinate - point to get a new stopping point to slice on. + the difference between bounds values and adjust the subsetted start + and end time coordinates depending on whether they fall within the + slice or not. """ + time_bounds = ds.bounds.get_bounds(axis="T") + time_delta = self._get_time_bounds_delta(time_bounds) + time_coords = xc.get_dim_coords(ds, axis="T") - time_dim = time_coords.name - time_bnds_key = time_coords.attrs["bounds"] - - # Extract the sub-dataset for all data at the last time coordinate and - # get the delta between time coordinates using the difference between - # bounds values. - ds_last_time = ds.isel({time_dim: -1}) - time_bnds = ds_last_time[time_bnds_key] - time_delta = time_bnds[-1] - time_bnds[0] + actual_day = time_coords[0].dt.day.item() + actual_month = time_coords[0].dt.month.item() + + if slice_type == "start": + stop = f"{year_str}-01-15" + + if actual_day >= 15 or actual_month > 1: + return stop + + stop_dt = datetime.strptime(stop, "%Y-%m-%d") + new_stop = stop_dt - time_delta + elif slice_type == "end": + stop = f"{year_str}-12-15" + + if actual_day <= 15 and actual_month == 1: + return stop + + stop_dt = datetime.strptime(stop, "%Y-%m-%d") + new_stop = stop_dt + time_delta + + new_stop_str = self._convert_new_stop_pt_to_iso_format(new_stop) + + return new_stop_str + + def _get_time_bounds_delta(self, time_bnds: xr.DataArray) -> timedelta: + """Get the time delta between bounds values. + + Parameters + ---------- + time_bnds : xr.DataArray + The time bounds. + + Returns + ------- + timedelta + The time delta. + """ + time_delta = time_bnds[0][-1] - time_bnds[0][0] time_delta_py = pd.to_timedelta(time_delta.values).to_pytimedelta() - # Add the time delta to the old stop point to get a new stop point. - old_stop = datetime.strptime(f"{old_end_time_str}-12-15", "%Y-%m-%d") - new_stop = old_stop + time_delta_py + return time_delta_py + + def _convert_new_stop_pt_to_iso_format(self, new_stop: datetime) -> str: + """ + Convert the new stop point from datetime to an ISO-8061 formatted + string. + + For example, "2012-12-15" and "0051-12-01". + + Parameters + ---------- + new_stop : datetime + The new stop point. - # Convert the new stopping point from datetime to an ISO-8061 formatted - # string (e.g., "2012-01-01", "0051-12-01"). + Returns + ------- + str + The new stop point as an ISO-8061 formatted string. + """ year_str = self._get_year_str(new_stop.year) month_day_str = self._get_month_day_str(new_stop.month, new_stop.day) new_stop_str = f"{year_str}-{month_day_str}" diff --git a/e3sm_diags/driver/utils/io.py b/e3sm_diags/driver/utils/io.py index df9bed067..13bf5dc16 100644 --- a/e3sm_diags/driver/utils/io.py +++ b/e3sm_diags/driver/utils/io.py @@ -3,7 +3,7 @@ import errno import json import os -from typing import Callable, Literal +from typing import Any, Callable, Dict, Literal import xarray as xr @@ -22,6 +22,8 @@ def _save_data_metrics_and_plots( ds_ref: xr.Dataset | None, ds_diff: xr.Dataset | None, metrics_dict: MetricsDict | None, + plot_kwargs: Dict[str, Any] | None = None, + viewer_descr: str | None = None, ): """Save data (optional), metrics, and plots. @@ -45,6 +47,15 @@ def _save_data_metrics_and_plots( The optional dictionary containing metrics for the variable. Some sets such as cosp_histogram only calculate spatial average and do not use ``metrics_dict``. + plot_kwargs : Dict[str, Any] | None + An optional dictionary containing extra keyword arguments used by a + plotter, by default None. For example, the enso_diags plotter has extra + kwargs for confidence levels called `da_test_conf_lvls` and + `da_ref_conf_lvls`. + viewer_descr : str | None + An optional viewer description, by default None. For example, + the enso_diags driver has a custom viewer description that is not + the "long_name" variable attribute. """ if parameter.save_netcdf: _write_vars_to_netcdf( @@ -65,20 +76,29 @@ def _save_data_metrics_and_plots( logger.info(f"Metrics saved in {filepath}") - # Set the viewer description to the "long_name" attr of the variable. - parameter.viewer_descr[var_key] = ds_test[var_key].attrs.get("long_name", var_key) + # Set the viewer description to the "long_name" attr of the variable if not + # manually set. + if viewer_descr is not None: + parameter.viewer_descr[var_key] = viewer_descr + else: + parameter.viewer_descr[var_key] = ds_test[var_key].attrs.get( + "long_name", var_key + ) # Get the function arguments and pass to the set's plotting function. - args = [ - parameter, - ds_test[var_key], - ds_ref[var_key] if ds_ref is not None else None, - ds_diff[var_key] if ds_diff is not None else None, - ] + args = { + "parameter": parameter, + "da_test": ds_test[var_key], + "da_ref": ds_ref[var_key] if ds_ref is not None else None, + "da_diff": ds_diff[var_key] if ds_diff is not None else None, + } if metrics_dict is not None: - args = args + [metrics_dict] + args["metrics_dict"] = metrics_dict + + if plot_kwargs is not None: + args = {**args, **plot_kwargs} - plot_func(*args) + plot_func(**args) def _write_vars_to_netcdf( diff --git a/e3sm_diags/plot/cartopy/enso_diags_plot.py b/e3sm_diags/plot/cartopy/enso_diags_plot.py deleted file mode 100644 index 457c8a1dc..000000000 --- a/e3sm_diags/plot/cartopy/enso_diags_plot.py +++ /dev/null @@ -1,463 +0,0 @@ -from __future__ import print_function - -import os - -import cartopy.crs as ccrs -import cdutil -import matplotlib -import numpy as np -import numpy.ma as ma -from cartopy.mpl.ticker import LatitudeFormatter, LongitudeFormatter -from numpy.polynomial.polynomial import polyfit - -from e3sm_diags.derivations.default_regions import regions_specs -from e3sm_diags.driver.utils.general import get_output_dir -from e3sm_diags.logger import custom_logger -from e3sm_diags.plot import get_colormap - -matplotlib.use("Agg") -import matplotlib.colors as colors # isort:skip # noqa: E402 -import matplotlib.pyplot as plt # isort:skip # noqa: E402 - -logger = custom_logger(__name__) - -plotTitle = {"fontsize": 11.5} -plotSideTitle = {"fontsize": 9.5} - -# Position and sizes of subplot axes in page coordinates (0 to 1) -panel = [ - (0.1691, 0.6810, 0.6465, 0.2258), - (0.1691, 0.3961, 0.6465, 0.2258), - (0.1691, 0.1112, 0.6465, 0.2258), -] - -# Border padding relative to subplot axes for saving individual panels -# (left, bottom, right, top) in page coordinates -border = (-0.06, -0.03, 0.13, 0.03) - - -def add_cyclic(var): - lon = var.getLongitude() - return var(longitude=(lon[0], lon[0] + 360.0, "coe")) - - -def get_ax_size(fig, ax): - bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted()) - width, height = bbox.width, bbox.height - width *= fig.dpi - height *= fig.dpi - return width, height - - -def determine_tick_step(degrees_covered): - if degrees_covered > 180: - return 60 - if degrees_covered > 60: - return 30 - elif degrees_covered > 20: - return 10 - else: - return 1 - - -def plot_panel_map( - n, fig, proj, var, clevels, cmap, title, parameter, conf=None, stats={} -): - var = add_cyclic(var) - lon = var.getLongitude() - lat = var.getLatitude() - var = ma.squeeze(var.asma()) - - # Contour levels - levels = None - norm = None - if len(clevels) > 0: - levels = [-1.0e8] + clevels + [1.0e8] - norm = colors.BoundaryNorm(boundaries=levels, ncolors=256) - - # Contour plot - ax = fig.add_axes(panel[n], projection=proj) - region_str = parameter.regions[0] - region = regions_specs[region_str] - if "domain" in region.keys(): # type: ignore - # Get domain to plot - domain = region["domain"] # type: ignore - else: - # Assume global domain - domain = cdutil.region.domain(latitude=(-90.0, 90, "ccb")) - kargs = domain.components()[0].kargs - lon_west, lon_east, lat_south, lat_north = (0, 360, -90, 90) - if "longitude" in kargs: - lon_west, lon_east, _ = kargs["longitude"] - if "latitude" in kargs: - lat_south, lat_north, _ = kargs["latitude"] - lon_covered = lon_east - lon_west - lon_step = determine_tick_step(lon_covered) - xticks = np.arange(lon_west, lon_east, lon_step) - # Subtract 0.50 to get 0 W to show up on the right side of the plot. - # If less than 0.50 is subtracted, then 0 W will overlap 0 E on the left side of the plot. - # If a number is added, then the value won't show up at all. - xticks = np.append(xticks, lon_east - 0.50) - lat_covered = lat_north - lat_south - lat_step = determine_tick_step(lat_covered) - yticks = np.arange(lat_south, lat_north, lat_step) - yticks = np.append(yticks, lat_north) - ax.set_extent([lon_west, lon_east, lat_south, lat_north], crs=proj) - cmap = get_colormap(cmap, parameter) - contours = ax.contourf( - lon, - lat, - var, - transform=ccrs.PlateCarree(), - norm=norm, - levels=levels, - cmap=cmap, - extend="both", - ) - - if conf is not None: - conf = add_cyclic(conf) - conf = ma.squeeze(conf.asma()) - # Values in conf will be either 0 or 1. Thus, there are only two levels - - # represented by the no-hatching and hatching levels. - ax.contourf( - lon, - lat, - conf, - 2, - transform=ccrs.PlateCarree(), - norm=norm, - colors="none", - extend="both", - hatches=[None, "//"], - ) - # Full world would be aspect 360/(2*180) = 1 - ax.set_aspect((lon_east - lon_west) / (2 * (lat_north - lat_south))) - ax.coastlines(lw=0.3) - if title[0] is not None: - ax.set_title(title[0], loc="left", fontdict=plotSideTitle) - if title[1] is not None: - ax.set_title(title[1], fontdict=plotTitle) - if title[2] is not None: - ax.set_title(title[2], loc="right", fontdict=plotSideTitle) - ax.set_xticks(xticks, crs=ccrs.PlateCarree()) - ax.set_yticks(yticks, crs=ccrs.PlateCarree()) - lon_formatter = LongitudeFormatter(zero_direction_label=True, number_format=".0f") - lat_formatter = LatitudeFormatter() - ax.xaxis.set_major_formatter(lon_formatter) - ax.yaxis.set_major_formatter(lat_formatter) - ax.tick_params(labelsize=8.0, direction="out", width=1) - ax.xaxis.set_ticks_position("bottom") - ax.yaxis.set_ticks_position("left") - # Place a vertical line in the middle of the plot - i.e. 180 degrees - ax.axvline(x=0.5, color="k", linewidth=0.5) - - # Color bar - cbax = fig.add_axes((panel[n][0] + 0.6635, panel[n][1] + 0.0115, 0.0326, 0.1792)) - cbar = fig.colorbar(contours, cax=cbax) - w, h = get_ax_size(fig, cbax) - - if levels is None: - cbar.ax.tick_params(labelsize=9.0, length=0) - - else: - maxval = np.amax(np.absolute(levels[1:-1])) - if maxval < 1.0: - fmt = "%5.3f" - pad = 30 - elif maxval < 10.0: - fmt = "%5.2f" - pad = 25 - elif maxval < 100.0: - fmt = "%5.1f" - pad = 25 - else: - fmt = "%6.1f" - pad = 30 - cbar.set_ticks(levels[1:-1]) - labels = [fmt % level for level in levels[1:-1]] - cbar.ax.set_yticklabels(labels, ha="right") - cbar.ax.tick_params(labelsize=9.0, pad=pad, length=0) - - # Display stats - if stats: - top_stats = (stats["max"], stats["min"], stats["mean"], stats["std"]) - top_text = "Max\nMin\nMean\nSTD" - fig.text( - panel[n][0] + 0.6635, - panel[n][1] + 0.2107, - top_text, - ha="left", - fontdict=plotSideTitle, - ) - fig.text( - panel[n][0] + 0.7635, - panel[n][1] + 0.2107, - "%.2f\n%.2f\n%.2f\n%.2f" % top_stats, - ha="right", - fontdict=plotSideTitle, - ) - - if "rmse" in stats.keys(): - bottom_stats = (stats["rmse"], stats["corr"]) - bottom_text = "RMSE\nCORR" - fig.text( - panel[n][0] + 0.6635, - panel[n][1] - 0.0205, - bottom_text, - ha="left", - fontdict=plotSideTitle, - ) - fig.text( - panel[n][0] + 0.7635, - panel[n][1] - 0.0205, - "%.2f\n%.2f" % bottom_stats, - ha="right", - fontdict=plotSideTitle, - ) - - # Hatch text - if conf is not None: - hatch_text = "Hatched when pvalue < 0.05" - fig.text( - panel[n][0] + 0.25, - panel[n][1] - 0.0355, - hatch_text, - ha="right", - fontdict=plotSideTitle, - ) - - -def plot_map( - reference, - test, - diff, - metrics_dict, - ref_confidence_levels, - test_confidence_levels, - parameter, -): - if parameter.backend not in ["cartopy", "mpl", "matplotlib"]: - return - - # Create figure, projection - fig = plt.figure(figsize=parameter.figsize, dpi=parameter.dpi) - # Use 179.99 as central longitude due to https://github.com/SciTools/cartopy/issues/946 - # proj = ccrs.PlateCarree(central_longitude=180) - proj = ccrs.PlateCarree(central_longitude=179.99) - - # Use non-regridded test and ref for stats, - # so we have the original stats displayed - - # First panel - plot_panel_map( - 0, - fig, - proj, - test, - parameter.contour_levels, - parameter.test_colormap, - (parameter.test_name_yrs, parameter.test_title, test.units), - parameter, - conf=test_confidence_levels, - stats=metrics_dict["test"], - ) - - # Second panel - plot_panel_map( - 1, - fig, - proj, - reference, - parameter.contour_levels, - parameter.reference_colormap, - (parameter.ref_name_yrs, parameter.reference_title, reference.units), - parameter, - conf=ref_confidence_levels, - stats=metrics_dict["ref"], - ) - - # Third panel - plot_panel_map( - 2, - fig, - proj, - diff, - parameter.diff_levels, - parameter.diff_colormap, - (None, parameter.diff_title, test.units), - parameter, - stats=metrics_dict["diff"], - ) - - # Figure title - fig.suptitle(parameter.main_title, x=0.5, y=0.97, fontsize=15) - - # Prepare to save figure - # get_output_dir => {parameter.results_dir}/{set_name}/{parameter.case_id} - # => {parameter.results_dir}/enso_diags/{parameter.case_id} - output_dir = get_output_dir(parameter.current_set, parameter) - if parameter.print_statements: - logger.info("Output dir: {}".format(output_dir)) - # get_output_dir => {parameter.orig_results_dir}/{set_name}/{parameter.case_id} - # => {parameter.orig_results_dir}/enso_diags/{parameter.case_id} - original_output_dir = get_output_dir(parameter.current_set, parameter) - if parameter.print_statements: - logger.info("Original output dir: {}".format(original_output_dir)) - # parameter.output_file is defined in e3sm_diags/driver/enso_diags_driver.py - # {parameter.results_dir}/enso_diags/{parameter.case_id}/{parameter.output_file} - file_path = os.path.join(output_dir, parameter.output_file) - # {parameter.orig_results_dir}/enso_diags/{parameter.case_id}/{parameter.output_file} - original_file_path = os.path.join(original_output_dir, parameter.output_file) - - # Save figure - for f in parameter.output_format: - f = f.lower().split(".")[-1] - plot_suffix = "." + f - plot_file_path = file_path + plot_suffix - plt.savefig(plot_file_path) - # Get the filename that the user has passed in and display that. - original_plot_file_path = original_file_path + plot_suffix - logger.info(f"Plot saved in: {original_plot_file_path}") - - # Save individual subplots - for f in parameter.output_format_subplot: - page = fig.get_size_inches() - i = 0 - for p in panel: - # Extent of subplot - subpage = np.array(p).reshape(2, 2) - subpage[1, :] = subpage[0, :] + subpage[1, :] - subpage = subpage + np.array(border).reshape(2, 2) - subpage = list(((subpage) * page).flatten()) # type: ignore - extent = matplotlib.transforms.Bbox.from_extents(*subpage) - # Save subplot - subplot_suffix = ".%i." % (i) + f - subplot_file_path = file_path + subplot_suffix - plt.savefig(subplot_file_path, bbox_inches=extent) - # Get the filename that the user has passed in and display that. - original_subplot_file_path = original_file_path + subplot_suffix - logger.info(f"Sub-plot saved in: {original_subplot_file_path}") - i += 1 - - plt.close() - - -def plot_scatter(x, y, parameter): - fig = plt.figure(figsize=parameter.figsize, dpi=parameter.dpi) - test_color = "black" - ref_color = "red" - test_title = "Test" if parameter.test_title == "" else parameter.test_title - if parameter.test_name_yrs: - test_title += " : {}".format(parameter.test_name_yrs) - ref_title = ( - "Reference" if parameter.reference_title == "" else parameter.reference_title - ) - if parameter.ref_name_yrs: - ref_title += " : {}".format(parameter.ref_name_yrs) - # https://stackoverflow.com/questions/14827650/pyplot-scatter-plot-marker-size - plt.scatter( - x["test"], - y["test"], - label=test_title, - color=test_color, - marker="s", - s=8, - ) - plt.scatter(x["ref"], y["ref"], label=ref_title, color=ref_color, marker="o", s=8) - for value_type in ["test", "ref"]: - if value_type == "test": - type_str = "Test" - type_color = test_color - x_range = (min(x["test"]), max(x["test"])) - else: - type_str = "Reference" - type_color = ref_color - x_range = (min(x["ref"]), max(x["ref"])) - # https://stackoverflow.com/questions/35091879/merge-2-arrays-vertical-to-tuple-numpy - # Two parallel lists of values - values = np.array((x[value_type], y[value_type])) - # Zip the values together (i.e., list of (x,y) instead of (list of x, list of y) - values = values.T - if y["var"] == "TAUX": - value_strs = [""] - else: - value_strs = ["positive ", "negative "] - for value_str in value_strs: - # https://stackoverflow.com/questions/24219841/numpy-where-on-a-2d-matrix - if value_str == "positive ": - # For all rows (x,y), choose the rows where x > 0 - rows = np.where(values[:, 0] > 0) - smaller_x_range = (0, x_range[1]) - linestyle = "-" - elif value_str == "negative ": - # For all rows (x,y), choose the rows where x < 0 - rows = np.where(values[:, 0] < 0) - smaller_x_range = (x_range[0], 0) - linestyle = "--" - elif value_str == "": - rows = None - smaller_x_range = x_range - linestyle = "-" - if rows: - # Get the filtered zip (list of (x,y) where x > 0 or x < 0) - filtered_values = values[rows] - else: - filtered_values = values - # Get the filtered parallel lists (i.e., (list of x, list of y)) - filtered_values = filtered_values.T - # https://stackoverflow.com/questions/19068862/how-to-overplot-a-line-on-a-scatter-plot-in-python - b, m = polyfit(filtered_values[0], filtered_values[1], 1) - label = "Linear fit for %sTS anomalies: %s (slope = %.2f)" % ( - value_str, - type_str, - m, - ) - ys = [b + m * x for x in smaller_x_range] - plt.plot( - smaller_x_range, - ys, - label=label, - color=type_color, - linestyle=linestyle, - ) - max_test = max(abs(min(y["test"])), abs(max(y["test"]))) - max_ref = max(abs(min(y["ref"])), abs(max(y["ref"]))) - max_value = max(max_test, max_ref) + 1 - plt.ylim(-max_value, max_value) - plt.xlabel("{} anomaly ({})".format(x["var"], x["units"])) - plt.ylabel("{} anomaly ({})".format(y["var"], y["units"])) - plt.legend() - - # Prepare to save figure - # get_output_dir => {parameter.results_dir}/{set_name}/{parameter.case_id} - # => {parameter.results_dir}/enso_diags/{parameter.case_id} - output_dir = get_output_dir(parameter.current_set, parameter) - if parameter.print_statements: - logger.info("Output dir: {}".format(output_dir)) - # get_output_dir => {parameter.orig_results_dir}/{set_name}/{parameter.case_id} - # => {parameter.orig_results_dir}/enso_diags/{parameter.case_id} - original_output_dir = get_output_dir(parameter.current_set, parameter) - if parameter.print_statements: - logger.info("Original output dir: {}".format(original_output_dir)) - # parameter.output_file is defined in e3sm_diags/driver/enso_diags_driver.py - # {parameter.results_dir}/enso_diags/{parameter.case_id}/{parameter.output_file} - file_path = os.path.join(output_dir, parameter.output_file) - # {parameter.orig_results_dir}/enso_diags/{parameter.case_id}/{parameter.output_file} - original_file_path = os.path.join(original_output_dir, parameter.output_file) - - # Figure title - fig.suptitle(parameter.main_title, x=0.5, y=0.93, fontsize=15) - - # Save figure - for f in parameter.output_format: - f = f.lower().split(".")[-1] - plot_suffix = "." + f - plot_file_path = file_path + plot_suffix - plt.savefig(plot_file_path) - # Get the filename that the user has passed in and display that. - - original_plot_file_path = original_file_path + plot_suffix - logger.info(f"Plot saved in: {original_plot_file_path}") - - plt.close() diff --git a/e3sm_diags/plot/enso_diags_plot.py b/e3sm_diags/plot/enso_diags_plot.py new file mode 100644 index 000000000..caca77906 --- /dev/null +++ b/e3sm_diags/plot/enso_diags_plot.py @@ -0,0 +1,379 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, List, Tuple + +import cartopy.crs as ccrs +import matplotlib +import numpy as np +import xarray as xr +import xcdat as xc +from numpy.polynomial.polynomial import polyfit + +from e3sm_diags.derivations.default_regions_xr import REGION_SPECS +from e3sm_diags.logger import custom_logger +from e3sm_diags.parameter.enso_diags_parameter import EnsoDiagsParameter +from e3sm_diags.plot.utils import ( + DEFAULT_PANEL_CFG, + SECONDARY_TITLE_FONTSIZE, + _add_colorbar, + _add_contour_plot, + _configure_titles, + _configure_x_and_y_axes, + _get_c_levels_and_norm, + _get_x_ticks, + _get_y_ticks, + _make_lon_cyclic, + _save_plot, +) + +matplotlib.use("Agg") +import matplotlib.pyplot as plt # isort:skip # noqa: E402 + +if TYPE_CHECKING: + from e3sm_diags.driver.enso_diags_driver import ( + MetricsDictMap, + MetricsDictScatter, + MetricsSubDict, + ) + +logger = custom_logger(__name__) + +# Use 179.99 as central longitude due to https://github.com/SciTools/cartopy/issues/946 +PROJECTION = ccrs.PlateCarree(central_longitude=179.99) + + +def plot_scatter( + parameter: EnsoDiagsParameter, x: MetricsDictScatter, y: MetricsDictScatter +): + fig = plt.figure(figsize=parameter.figsize, dpi=parameter.dpi) + fig.suptitle(parameter.main_title, x=0.5, y=0.93, fontsize=15) + + test_color = "black" + ref_color = "red" + test_title = "Test" if parameter.test_title == "" else parameter.test_title + + if parameter.test_name_yrs: + test_title += " : {}".format(parameter.test_name_yrs) + + ref_title = ( + "Reference" if parameter.reference_title == "" else parameter.reference_title + ) + + if parameter.ref_name_yrs: + ref_title += " : {}".format(parameter.ref_name_yrs) + + plt.scatter( + x["test"], + y["test"], + label=test_title, + color=test_color, + marker="s", + s=8, + ) + plt.scatter(x["ref"], y["ref"], label=ref_title, color=ref_color, marker="o", s=8) + + for value_type in ["test", "ref"]: + if value_type == "test": + type_str = "Test" + type_color = test_color + x_range = (min(x["test"]), max(x["test"])) + else: + type_str = "Reference" + type_color = ref_color + x_range = (min(x["ref"]), max(x["ref"])) + + values = np.array((x[value_type], y[value_type])) # type: ignore + values = values.T + + if y["var"] == "TAUX": + value_strs = [""] + else: + value_strs = ["positive ", "negative "] + + for value_str in value_strs: + if value_str == "positive ": + # For all rows (x,y), choose the rows where x > 0 + rows = np.where(values[:, 0] > 0) + smaller_x_range = (0, x_range[1]) + linestyle = "-" + elif value_str == "negative ": + # For all rows (x,y), choose the rows where x < 0 + rows = np.where(values[:, 0] < 0) + smaller_x_range = (x_range[0], 0) + linestyle = "--" + elif value_str == "": + rows = None + smaller_x_range = x_range + linestyle = "-" + + if rows: + filtered_values = values[rows] + else: + filtered_values = values + + filtered_values = filtered_values.T + + b, m = polyfit(filtered_values[0], filtered_values[1], 1) + label = "Linear fit for %sTS anomalies: %s (slope = %.2f)" % ( + value_str, + type_str, + m, + ) + ys = [b + m * x for x in smaller_x_range] + + plt.plot( + smaller_x_range, + ys, + label=label, + color=type_color, + linestyle=linestyle, + ) + + max_test = max(abs(min(y["test"])), abs(max(y["test"]))) + max_ref = max(abs(min(y["ref"])), abs(max(y["ref"]))) + max_value = max(max_test, max_ref) + 1 + + plt.ylim(-max_value, max_value) + plt.xlabel("{} anomaly ({})".format(x["var"], x["units"])) + plt.ylabel("{} anomaly ({})".format(y["var"], y["units"])) + plt.legend() + + _save_plot(fig, parameter) + + plt.close() + + +def plot_map( + parameter: EnsoDiagsParameter, + da_test: xr.DataArray, + da_ref: xr.DataArray, + da_diff: xr.DataArray, + metrics_dict: MetricsDictMap, + da_test_conf_lvls: xr.DataArray, + da_ref_conf_lvls: xr.DataArray, +): + # Create figure + fig = plt.figure(figsize=parameter.figsize, dpi=parameter.dpi) + fig.suptitle(parameter.main_title, x=0.5, y=0.97, fontsize=15) + + # Use non-regridded test and ref for stats, so we have the original stats + # displayed + _add_colormap( + 0, + da_test, + fig, + parameter, + parameter.test_colormap, + parameter.contour_levels, + (parameter.test_name_yrs, parameter.test_title, da_test.units), + metrics_dict["test"], # type: ignore + conf=da_test_conf_lvls, + ) + _add_colormap( + 1, + da_ref, + fig, + parameter, + parameter.reference_colormap, + parameter.contour_levels, + (parameter.ref_name_yrs, parameter.reference_title, da_ref.units), + metrics_dict["ref"], # type: ignore + conf=da_ref_conf_lvls, + ) + _add_colormap( + 2, + da_diff, + fig, + parameter, + parameter.diff_colormap, + parameter.diff_levels, + (None, parameter.diff_title, da_test.units), + metrics_dict["diff"], # type: ignore + ) + + _save_plot(fig, parameter) + + plt.close() + + +def _add_colormap( + subplot_num: int, + var: xr.DataArray, + fig: plt.Figure, + parameter: EnsoDiagsParameter, + color_map: str, + contour_levels: List[float], + title: Tuple[str | None, str, str], + metrics: MetricsSubDict, + conf: xr.DataArray | None = None, +): + var = _make_lon_cyclic(var) + lat = xc.get_dim_coords(var, axis="Y") + lon = xc.get_dim_coords(var, axis="X") + + var = var.squeeze() + + # Configure contour levels and boundary norm. + # -------------------------------------------------------------------------- + c_levels, norm = _get_c_levels_and_norm(contour_levels) + + # Get region info and X and Y plot ticks. + # -------------------------------------------------------------------------- + region_key = parameter.regions[0] + region_specs = REGION_SPECS[region_key] + + # Get the region's domain slices for latitude and longitude if set, or + # use the default value. If both are not set, then the region type is + # considered "global". + lat_slice = region_specs.get("lat", (-90, 90)) # type: ignore + lon_slice = region_specs.get("lon", (0, 360)) # type: ignore + + # Boolean flags for configuring plots. + is_global_domain = lat_slice == (-90, 90) and lon_slice == (0, 360) + is_lon_full = lon_slice == (0, 360) + + # Determine X and Y ticks using longitude and latitude domains respectively. + lon_west, lon_east = lon_slice + x_ticks = _get_x_ticks( + lon_west, + lon_east, + is_global_domain, + is_lon_full, + tick_step_func=_determine_tick_step, + ) + + lat_south, lat_north = lat_slice + y_ticks = _get_y_ticks(lat_south, lat_north, tick_step_func=_determine_tick_step) + + # Get the figure Axes object using the projection above. + # -------------------------------------------------------------------------- + ax = fig.add_axes(DEFAULT_PANEL_CFG[subplot_num], projection=PROJECTION) + ax.set_extent([lon_west, lon_east, lat_south, lat_north], crs=PROJECTION) + contour_plot = _add_contour_plot( + ax, parameter, var, lon, lat, color_map, ccrs.PlateCarree(), norm, c_levels + ) + + if conf is not None: + conf = _make_lon_cyclic(conf) + conf = conf.squeeze() # type: ignore + + # Values in conf will be either 0 or 1. Thus, there are only two levels - + # represented by the no-hatching and hatching levels. + ax.contourf( + lon, + lat, + conf, + 2, + transform=ccrs.PlateCarree(), + norm=norm, + colors="none", + extend="both", + hatches=[None, "//"], + ) + + # Full world would be aspect 360/(2*180) = 1 + ax.set_aspect((lon_east - lon_west) / (2 * (lat_north - lat_south))) + ax.coastlines(lw=0.3) + + # Place a vertical line in the middle of the plot - i.e. 180 degrees + ax.axvline(x=0.5, color="k", linewidth=0.5) + + # Configure the titles, x and y axes, and colorbar. + # -------------------------------------------------------------------------- + _configure_titles(ax, title) + _configure_x_and_y_axes( + ax, x_ticks, y_ticks, ccrs.PlateCarree(), parameter.current_set + ) + _add_colorbar( + fig, + subplot_num, + DEFAULT_PANEL_CFG, + contour_plot, + c_levels=c_levels, + rect=None, + c_label_fmt_and_pad_func=_get_contour_label_format_and_pad, + ) + + # Add metrics text to the figure. + # -------------------------------------------------------------------------- + metrics_values = tuple(metrics.values()) + top_text = "Max\nMin\nMean\nSTD" + fig.text( + DEFAULT_PANEL_CFG[subplot_num][0] + 0.6635, + DEFAULT_PANEL_CFG[subplot_num][1] + 0.2107, + top_text, + ha="left", + fontdict={"fontsize": SECONDARY_TITLE_FONTSIZE}, + ) + fig.text( + DEFAULT_PANEL_CFG[subplot_num][0] + 0.7635, + DEFAULT_PANEL_CFG[subplot_num][1] + 0.2107, + "%.2f\n%.2f\n%.2f\n%.2f" % metrics_values, # type: ignore + ha="right", + fontdict={"fontsize": SECONDARY_TITLE_FONTSIZE}, + ) + + # Hatch text + if conf is not None: + hatch_text = "Hatched when pvalue < 0.05" + fig.text( + DEFAULT_PANEL_CFG[subplot_num][0] + 0.25, + DEFAULT_PANEL_CFG[subplot_num][1] - 0.0355, + hatch_text, + ha="right", + fontdict={"fontsize": SECONDARY_TITLE_FONTSIZE}, + ) + + +def _determine_tick_step(degrees_covered: float) -> int: + """Determine the number of tick steps based on the degrees covered by the axis. + + Parameters + ---------- + degrees_covered : float + The degrees covered by the axis. + + Returns + ------- + int + The number of tick steps. + """ + if degrees_covered > 180: + return 60 + if degrees_covered > 60: + return 30 + elif degrees_covered > 20: + return 10 + else: + return 1 + + +def _get_contour_label_format_and_pad(c_levels: List[float]) -> Tuple[str, int]: + """Get the label format and padding for each contour level. + + Parameters + ---------- + c_levels : List[float] + The contour levels. + + Returns + ------- + Tuple[str, int] + A tuple for the label format and padding. + """ + maxval = np.amax(np.absolute(c_levels[1:-1])) + + if maxval < 1.0: + fmt = "%5.3f" + pad = 30 + elif maxval < 10.0: + fmt = "%5.2f" + pad = 25 + elif maxval < 100.0: + fmt = "%5.1f" + pad = 25 + else: + fmt = "%6.1f" + pad = 30 + + return fmt, pad diff --git a/e3sm_diags/plot/utils.py b/e3sm_diags/plot/utils.py index 5ffa65486..37481d69b 100644 --- a/e3sm_diags/plot/utils.py +++ b/e3sm_diags/plot/utils.py @@ -261,7 +261,7 @@ def _determine_tick_step(degrees_covered: float) -> int: """ if degrees_covered > 180: return 60 - if degrees_covered > 60: + elif degrees_covered > 60: return 30 elif degrees_covered > 30: return 10 @@ -449,6 +449,43 @@ def _configure_x_and_y_axes( ax.yaxis.set_major_formatter(lat_formatter) +def _get_contour_label_format_and_pad(c_levels: List[float]) -> Tuple[str, int]: + """Get the label format and padding for each contour level. + + Parameters + ---------- + c_levels : List[float] + The contour levels. + + Returns + ------- + Tuple[str, int] + A tuple for the label format and padding. + """ + maxval = np.amax(np.absolute(c_levels[1:-1])) + + if maxval < 0.01: + fmt = "%.1e" + pad = 35 + elif maxval < 0.2: + fmt = "%5.3f" + pad = 28 + elif maxval < 10.0: + fmt = "%5.2f" + pad = 25 + elif maxval < 100.0: + fmt = "%5.1f" + pad = 25 + elif maxval > 9999.0: + fmt = "%.0f" + pad = 40 + else: + fmt = "%6.1f" + pad = 30 + + return fmt, pad + + def _add_colorbar( fig: plt.Figure, subplot_num: int, @@ -456,6 +493,7 @@ def _add_colorbar( contour_plot: mcontour.QuadContourSet, c_levels: List[float] | None, rect: Rect | None = None, + c_label_fmt_and_pad_func: Callable = _get_contour_label_format_and_pad, ): """Configure the colorbar on a colormap. @@ -476,6 +514,9 @@ def _add_colorbar( An optional adjustment to the dimensions (left, bottom, width, height) of the new `~.axes.Axes`. All quantities are in fractions of figure width and height. + c_label_fmt_and_pad_func : Callable + An optional function for configuring the contour level label format + and padding. """ cbax_rect = _get_rect(subplot_num, panel_configs, rect) cbax = fig.add_axes(cbax_rect) @@ -486,8 +527,9 @@ def _add_colorbar( else: cbar.set_ticks(c_levels[1:-1]) - label_format, pad = _get_contour_label_format_and_pad(c_levels) + label_format, pad = c_label_fmt_and_pad_func(c_levels) labels = [label_format % level for level in c_levels[1:-1]] + cbar.ax.set_yticklabels(labels, ha="right") cbar.ax.tick_params(labelsize=9.0, pad=pad, length=0) @@ -527,43 +569,6 @@ def _get_rect( ) -def _get_contour_label_format_and_pad(c_levels: List[float]) -> Tuple[str, int]: - """Get the label format and padding for each contour level. - - Parameters - ---------- - c_levels : List[float] - The contour levels. - - Returns - ------- - Tuple[str, int] - A tuple for the label format and padding. - """ - maxval = np.amax(np.absolute(c_levels[1:-1])) - - if maxval < 0.01: - fmt = "%.1e" - pad = 35 - elif maxval < 0.2: - fmt = "%5.3f" - pad = 28 - elif maxval < 10.0: - fmt = "%5.2f" - pad = 25 - elif maxval < 100.0: - fmt = "%5.1f" - pad = 25 - elif maxval > 9999.0: - fmt = "%.0f" - pad = 40 - else: - fmt = "%6.1f" - pad = 30 - - return fmt, pad - - def _add_min_mean_max_text( fig: plt.Figure, subplot_num: int, diff --git a/tests/e3sm_diags/driver/utils/test_dataset_xr.py b/tests/e3sm_diags/driver/utils/test_dataset_xr.py index a329fef80..79bf48770 100644 --- a/tests/e3sm_diags/driver/utils/test_dataset_xr.py +++ b/tests/e3sm_diags/driver/utils/test_dataset_xr.py @@ -941,11 +941,11 @@ def test_returns_climo_dataset_using_climo_of_time_series_files(self): ds = Dataset(parameter, data_type="ref") result = ds.get_climo_dataset("ts", "ANN") - # Since the data is not sub-monthly, the first time coord (2001-01-01) - # is dropped when subsetting with the middle of the month (2000-01-15). - expected = self.ds_ts.isel(time=slice(1, 4)) + + expected = self.ds_ts.copy() expected["time"].data[:] = np.array( [ + cftime.DatetimeGregorian(2000, 1, 16, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2000, 2, 15, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2000, 3, 16, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2001, 1, 16, 12, 0, 0, 0, has_year_zero=False), @@ -1190,11 +1190,10 @@ def test_returns_time_series_dataset_using_file(self): result = ds.get_time_series_dataset("ts") - # Since the data is not sub-monthly, the first time coord (2001-01-01) - # is dropped when subsetting with the middle of the month (2000-01-15). - expected = self.ds_ts.isel(time=slice(1, 4)) + expected = self.ds_ts.copy() expected["time"].data[:] = np.array( [ + cftime.DatetimeGregorian(2000, 1, 16, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2000, 2, 15, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2000, 3, 16, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2001, 1, 16, 12, 0, 0, 0, has_year_zero=False), @@ -1248,9 +1247,10 @@ def test_returns_time_series_dataset_using_derived_var(self): ds = Dataset(parameter, data_type="ref") result = ds.get_time_series_dataset("PRECT") - expected = ds_pr.sel(time=slice("2000-02-01", "2001-01-01")) + expected = ds_pr.copy() expected["time"].data[:] = np.array( [ + cftime.DatetimeGregorian(2000, 1, 16, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2000, 2, 15, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2000, 3, 16, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2001, 1, 16, 12, 0, 0, 0, has_year_zero=False), @@ -1287,10 +1287,11 @@ def test_returns_time_series_dataset_using_derived_var_directly_from_dataset(sel result = ds.get_time_series_dataset("PRECST") expected = ds_precst.copy() - expected = ds_precst.sel(time=slice("2000-02-01", "2001-01-01")) + expected = ds_precst.copy() expected["PRECST"].attrs["units"] = "mm/s" expected["time"].data[:] = np.array( [ + cftime.DatetimeGregorian(2000, 1, 16, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2000, 2, 15, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2000, 3, 16, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2001, 1, 16, 12, 0, 0, 0, has_year_zero=False), @@ -1341,9 +1342,10 @@ def test_returns_time_series_dataset_with_centered_time_if_non_sub_monthly_data( ds.is_sub_monthly = False result = ds.get_time_series_dataset("ts") - expected = self.ds_ts.isel(time=slice(1, 4)) + expected = self.ds_ts.copy() expected["time"].data[:] = np.array( [ + cftime.DatetimeGregorian(2000, 1, 16, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2000, 2, 15, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2000, 3, 16, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2001, 1, 16, 12, 0, 0, 0, has_year_zero=False), @@ -1367,11 +1369,11 @@ def test_returns_time_series_dataset_using_file_with_ref_name_prepended(self): ds = Dataset(parameter, data_type="ref") result = ds.get_time_series_dataset("ts") - # Since the data is not sub-monthly, the first time coord (2001-01-01) - # is dropped when subsetting with the middle of the month (2000-01-15). - expected = ds_ts.isel(time=slice(1, 4)) + + expected = ds_ts.copy() expected["time"].data[:] = np.array( [ + cftime.DatetimeGregorian(2000, 1, 16, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2000, 2, 15, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2000, 3, 16, 12, 0, 0, 0, has_year_zero=False), cftime.DatetimeGregorian(2001, 1, 16, 12, 0, 0, 0, has_year_zero=False), @@ -1548,16 +1550,6 @@ def setup(self, tmp_path): self.ds_ts = xr.Dataset() self.ds_ts.to_netcdf(self.ts_path) - def test_raises_error_if_test_name_attrs_not_set_for_test_dataset(self): - param1 = _create_parameter_object( - "test", "climo", self.data_path, "2000", "2002" - ) - - ds1 = Dataset(param1, data_type="test") - - with pytest.raises(AttributeError): - ds1.get_name_yrs_attr("ANN") - def test_raises_error_if_season_arg_is_not_passed_for_climo_dataset(self): param1 = _create_parameter_object( "test", "climo", self.data_path, "2000", "2002" @@ -1569,16 +1561,6 @@ def test_raises_error_if_season_arg_is_not_passed_for_climo_dataset(self): with pytest.raises(ValueError): ds1.get_name_yrs_attr() - def test_raises_error_if_ref_name_attrs_not_set_ref_dataset(self): - param1 = _create_parameter_object( - "ref", "climo", self.data_path, "2000", "2002" - ) - - ds1 = Dataset(param1, data_type="ref") - - with pytest.raises(AttributeError): - ds1.get_name_yrs_attr("ANN") - def test_returns_test_name_and_yrs_averaged_attr_with_climo_dataset_using_short_test_name( self, ): @@ -1637,6 +1619,25 @@ def test_returns_only_test_name_attr_if_yrs_averaged_attr_not_found_with_climo_d assert result == expected + def test_returns_only_yrs_averaged_attr_if_test_name_not_set_with_climo_dataset( + self, + ): + param1 = _create_parameter_object( + "test", "climo", self.data_path, "2000", "2002" + ) + param1.test_name = "" + param1.test_file = self.test_file + + # Write the climatology dataset out before function call. + ds_climo = self.ds_climo.copy() + ds_climo.to_netcdf(f"{self.data_path}/{param1.test_file}") + + ds1 = Dataset(param1, data_type="test") + result = ds1.get_name_yrs_attr("ANN") + expected = " (2000-2002)" + + assert result == expected + def test_returns_ref_name_and_yrs_averaged_attr_with_climo_dataset_using_short_ref_name( self, ): @@ -1688,6 +1689,22 @@ def test_returns_ref_name_and_yrs_averaged_attr_with_climo_dataset_using_ref_nam assert result == expected + def test_returns_only_yrs_averaged_attr_if_ref_name_is_not_set_with_climo_dataset( + self, + ): + param = _create_parameter_object("ref", "climo", self.data_path, "2000", "2002") + param.ref_name = "" + param.ref_file = self.ref_file + + # Write the climatology dataset out before function call. + self.ds_climo.to_netcdf(f"{self.data_path}/{param.ref_file}") + + ds3 = Dataset(param, data_type="ref") + result = ds3.get_name_yrs_attr("ANN") + expected = " (2000-2002)" + + assert result == expected + def test_returns_test_name_and_years_averaged_as_single_string_with_timeseries_dataset( self, ):