Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

get_warming_level #210

Merged
merged 16 commits into from Jun 5, 2023
7 changes: 4 additions & 3 deletions .github/PULL_REQUEST_TEMPLATE.md
Expand Up @@ -3,10 +3,11 @@
### Pull Request Checklist:
- [ ] This PR addresses an already opened issue (for bug fixes / features)
- This PR fixes #xyz
- [ ] (If applicable) Documentation has been added / updated (for bug fixes / features)
- [ ] (If applicable) Documentation has been added / updated (for bug fixes / features).
- [ ] (If applicable) Tests have been added.
- [ ] This PR does not seem to break the templates.
- [ ] HISTORY.rst has been updated (with summary of main changes)
- [ ] Link to issue (:issue:`number`) and pull request (:pull:`number`) has been added
- [ ] HISTORY.rst has been updated (with summary of main changes).
- [ ] Link to issue (:issue:`number`) and pull request (:pull:`number`) has been added.

### What kind of change does this PR introduce?

Expand Down
1 change: 1 addition & 0 deletions HISTORY.rst
Expand Up @@ -13,6 +13,7 @@ Announcements
New features and enhancements
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* `xscen` now tracks code coverage using `coveralls <https://coveralls.io/>`_. (:pull:`187`).
* New function `get_warming_level` to search within the IPCC CMIP global temperatures CSV without requiring data. (:issue:`208`, :pull:`210`).

Breaking changes
^^^^^^^^^^^^^^^^
Expand Down
61 changes: 56 additions & 5 deletions docs/notebooks/5_warminglevels.ipynb
Expand Up @@ -6,15 +6,68 @@
"metadata": {},
"source": [
"# Warming levels\n",
" This notebook will demonstrate a typical workflow for showing indicators by warming levels."
"\n",
"``xs.get_warming_level`` can be used to know when a given model reaches a given warming level.\n",
"\n",
"The arguments of ``xs.get_warming_level`` are:\n",
"\n",
"- `realization`: Dataset, string, or list of strings. Strings should follow the format 'mip-era_source_experiment_member'\n",
"- `wl`: warming level.\n",
"- `window`: Number of years in the centered window during which the warming level is reached. Note that in the case of an even number, the IPCC standard is used (-n/2+1, +n/2).\n",
"- `tas_baseline_period`: The period over which the warming level is calculated, equivalent to \"+0°C\". Defaults to 1850-1900.\n",
"- `ignore_member`: The default `warming_level_csv` only contains data for 1 member. If you want a result regardless of the realization number, set this to True. This is only used when `models` is a Dataset.\n",
"- `return_horizon`: Whether to return the start/end of the horizon or to return the middle year.\n",
" \n",
"If `realization` is a list, the function returns a dictionary. Otherwise, it will return either a string or ['start_yr', 'end_yr'], depending on `return_horizon`. For entries that it fails to find in the csv, or for instances where a given warming level is not reached, the function returns None."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "fa38e4c5-b693-42ea-bf9e-08862742c729",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"import xscen as xs\n",
"\n",
"# Multiple entries, returns a dictionary\n",
"print(\n",
" xs.get_warming_level(\n",
" [\n",
" \"CMIP6_CanESM5_ssp126_r1i1p1f1\",\n",
" \"CMIP6_CanESM5_ssp245_r1i1p1f1\",\n",
" \"CMIP6_CanESM5_ssp370_r1i1p1f1\",\n",
" \"CMIP6_CanESM5_ssp585_r1i1p1f1\",\n",
" ],\n",
" wl=2,\n",
" window=20,\n",
" return_horizon=False,\n",
" )\n",
")\n",
"# Returns a list\n",
"print(\n",
" xs.get_warming_level(\n",
" \"CMIP6_CanESM5_ssp585_r1i1p1f1\", wl=2, window=20, return_horizon=True\n",
" )\n",
")\n",
"# Only the middle year is requested, returns a string\n",
"print(\n",
" xs.get_warming_level(\n",
" \"CMIP6_CanESM5_ssp585_r1i1p1f1\", wl=2, window=20, return_horizon=False\n",
" )\n",
")\n",
"# +10°C is never reached, returns None\n",
"print(xs.get_warming_level(\"CMIP6_CanESM5_ssp585_r1i1p1f1\", wl=10, window=20))"
]
},
{
"cell_type": "markdown",
"id": "b9b427d5",
"metadata": {},
"source": [
"First, initialize your project catalog."
"This rest of this notebook will demonstrate a typical workflow for showing indicators by warming levels. First, initialize your project catalog."
]
},
{
Expand All @@ -31,8 +84,6 @@
"import xesmf as xe\n",
"from matplotlib import pyplot as plt\n",
"\n",
"import xscen as xs\n",
"\n",
"output_folder = Path().absolute() / \"_data\"\n",
"\n",
"project = {\n",
Expand Down Expand Up @@ -110,7 +161,7 @@
"\n",
"Warming levels are computed individually in order to be able to calculate the ensemble weights properly (see [subsection below](#Ensemble-statistics)).\n",
"\n",
"The arguments of ``xs.subset_warming_level`` are:\n",
"The function calls `get_warming_level`, so the arguments are essentially the same.:\n",
"\n",
"- `ds`: input dataset.\n",
"- `wl`: warming level.\n",
Expand Down
153 changes: 153 additions & 0 deletions tests/test_extract.py
@@ -0,0 +1,153 @@
from copy import deepcopy

import numpy as np
import pytest
from xclim.testing.helpers import test_timeseries as timeseries

import xscen as xs


class TestGetWarmingLevel:
def test_list(self):
out = xs.get_warming_level(
["CMIP6_CanESM5_ssp126_r1i1p1f1", "CMIP6_CanESM5_ssp245_r1i1p1f1"],
wl=2,
window=20,
return_horizon=False,
)
assert isinstance(out, dict)
assert out["CMIP6_CanESM5_ssp126_r1i1p1f1"] == "2026"

def test_string_with_horizon(self):
out = xs.get_warming_level(
"CMIP6_CanESM5_ssp585_r1i1p1f1", wl=2, window=20, return_horizon=True
)
assert isinstance(out, list)
assert out == ["2013", "2032"]

@pytest.mark.parametrize("attrs", ["global", "regional", "regional_w_institution"])
def test_ds(self, attrs):
ds = timeseries(
np.tile(np.arange(1, 2), 30),
variable="tas",
start="2001-01-01",
freq="AS-JAN",
as_dataset=True,
)
attributes = {
"global": {
"cat:mip_era": "CMIP6",
"cat:source": "CanESM5",
"cat:experiment": "ssp126",
},
"regional": {
"cat:mip_era": "CMIP6",
"cat:driving_model": "CanESM5",
"cat:experiment": "ssp126",
},
"regional_w_institution": {
"cat:mip_era": "CMIP6",
"cat:driving_institution": "CCCma",
"cat:driving_model": "CCCma-CanESM5",
"cat:experiment": "ssp126",
},
}

ds.attrs = attributes[deepcopy(attrs)]
assert (
xs.get_warming_level(
ds, wl=2, window=20, ignore_member=True, return_horizon=False
)
== "2026"
)

def test_multiple_matches(self):
# 55 instances of CanESM2-rcp85 in the CSV, but it should still return a single value
assert (
xs.get_warming_level(
"CMIP5_CanESM2_rcp85_.*", wl=3.5, window=30, return_horizon=False
)
== "2059"
)

def test_odd_window(self):
assert xs.get_warming_level(
"CMIP6_CanESM5_ssp126_r1i1p1f1", wl=2, window=21, return_horizon=True
) == ["2016", "2036"]

def test_none(self):
assert (
xs.get_warming_level(
"CMIP6_CanESM5_ssp585_r1i1p1f1", wl=20, window=20, return_horizon=False
)
is None
)
assert (
xs.get_warming_level(
"CMIP6_notreal_ssp585_r1i1p1f1", wl=20, window=20, return_horizon=False
)
is None
)

def test_wrong_types(self):
with pytest.raises(ValueError):
xs.get_warming_level(
{"this": "is not valid."}, wl=2, window=20, return_horizon=True
)
with pytest.raises(ValueError):
xs.get_warming_level(
"CMIP6_CanESM5_ssp585_r1i1p1f1_toomany_underscores",
wl=2,
window=20,
return_horizon=True,
)
with pytest.raises(ValueError):
xs.get_warming_level(
"CMIP6_CanESM5_ssp585_r1i1p1f1", wl=2, window=3.85, return_horizon=True
)


class TestSubsetWarmingLevel:
ds = timeseries(
np.tile(np.arange(1, 2), 50),
variable="tas",
start="2000-01-01",
freq="AS-JAN",
as_dataset=True,
)
ds.attrs = {
"cat:mip_era": "CMIP6",
"cat:source": "CanESM5",
"cat:experiment": "ssp585",
"cat:member": "r1i1p1f1",
}

def test_default(self):
ds_sub = xs.subset_warming_level(TestSubsetWarmingLevel.ds, wl=2)

np.testing.assert_array_equal(ds_sub.time.dt.year[0], 2013)
np.testing.assert_array_equal(ds_sub.time.dt.year[-1], 2032)
np.testing.assert_array_equal(ds_sub.warminglevel[0], "+2Cvs1850-1900")
assert ds_sub.warminglevel.attrs["baseline"] == "1850-1900"
assert ds_sub.attrs["cat:processing_level"] == "warminglevel-2vs1850-1900"

def test_kwargs(self):
ds_sub = xs.subset_warming_level(
TestSubsetWarmingLevel.ds,
wl=1,
window=25,
tas_baseline_period=["1981", "2010"],
to_level="tests",
)

np.testing.assert_array_equal(ds_sub.time.dt.year[0], 2009)
np.testing.assert_array_equal(ds_sub.time.dt.year[-1], 2033)
np.testing.assert_array_equal(ds_sub.warminglevel[0], "+1Cvs1981-2010")
assert ds_sub.warminglevel.attrs["baseline"] == "1981-2010"
assert ds_sub.attrs["cat:processing_level"] == "tests"

def test_outofrange(self):
assert xs.subset_warming_level(TestSubsetWarmingLevel.ds, wl=5) is None

def test_none(self):
assert xs.subset_warming_level(TestSubsetWarmingLevel.ds, wl=20) is None
7 changes: 6 additions & 1 deletion xscen/__init__.py
Expand Up @@ -26,7 +26,12 @@
from .config import CONFIG, load_config # noqa
from .diagnostics import properties_and_measures
from .ensembles import *
from .extract import extract_dataset, search_data_catalogs, subset_warming_level # noqa
from .extract import ( # noqa
extract_dataset,
get_warming_level,
search_data_catalogs,
subset_warming_level,
)
from .indicators import compute_indicators # noqa
from .io import save_to_netcdf, save_to_zarr # noqa
from .reduce import build_reduction_data, reduce_ensemble
Expand Down