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

Climatological op #290

Merged
merged 73 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
9318cf1
filter indicator list for variables available in a dataset
vindelico Oct 12, 2023
bd37066
climatological_op 1st version; enhanced docs only
vindelico Oct 12, 2023
379decc
filter indicator list for variables available in a dataset
vindelico Oct 12, 2023
77564af
update xscen version from remote
vindelico Oct 13, 2023
ffdcb08
test for select_inds_for_avail_vars, first draft
vindelico Oct 16, 2023
5931257
snapshot; climatological_op working for linregress, doing window(ing)…
vindelico Nov 6, 2023
fcff131
snapshot; rmoved xscen/aggregate_old.py intermediate file
vindelico Nov 6, 2023
7de6829
climatological_op, working v1
vindelico Nov 9, 2023
f21aa05
debug test_indicators
vindelico Nov 9, 2023
539c9d5
all tests for climatological_op passing
vindelico Nov 13, 2023
364c680
change regressors in linregress to periods years, updated test, all t…
vindelico Nov 13, 2023
5fb2fef
Updated HISTORY; climatological_mean calls climatological_op, TestCli…
vindelico Nov 15, 2023
33c3ad0
Draft output option periods_as_dim for climatological_op + test
vindelico Nov 15, 2023
675fcad
periods_as_dim final and updated to xarray=2023.9; added unstacked ti…
vindelico Nov 17, 2023
677be08
Merge branch 'main' of github.com:Ouranosinc/xscen into select_indica…
vindelico Nov 17, 2023
6ad3176
merged with climatological_op
vindelico Nov 17, 2023
9c7e916
removed select_inds_for_avail_vars + test for PR
vindelico Nov 17, 2023
a6e5440
Update HISTORY.rst
vindelico Nov 17, 2023
9e1497e
Updated docstring, pre-commit passed
vindelico Nov 19, 2023
51ff13a
Changed Getting Started notebook to use climatological_op
vindelico Nov 19, 2023
757be8e
Merged climatological_op with origin/main
vindelico Nov 21, 2023
2a03ba6
Comments from PR addressed, round 1.
vindelico Nov 22, 2023
d98f082
Changed call of climatological_mean within compute_horizon to calling…
vindelico Nov 22, 2023
5276bb3
Fixed text in Getting Started docs.
vindelico Nov 22, 2023
851de09
Update aggregate.py
vindelico Nov 22, 2023
4d56597
Merged with updated climatological_op
vindelico Nov 22, 2023
75babcf
Update select_ind_for_avail_vars and test.
vindelico Nov 22, 2023
d5fa354
Test select_inds_for_avail_vars; draft
vindelico Nov 23, 2023
0d4ca1f
Test select_inds_for_avail_vars passes.
vindelico Nov 23, 2023
ffabb74
Updated HISTORY.rst
vindelico Nov 23, 2023
3f5028b
Updated to xarray's new way of handling pandas.MultiIndex objects
vindelico Nov 27, 2023
3fcea88
Merge branch 'climatological_op' into select_indicators_for_available…
vindelico Nov 27, 2023
4cc6928
Merge branch 'main' into climatological_op
aulemahal Nov 28, 2023
5a0abdb
Merge branch 'climatological_op' into select_indicators_for_available…
RondeauG Nov 29, 2023
1518814
Update aggregate.py
vindelico Dec 1, 2023
2ec9855
Apply suggestions from code review
vindelico Dec 1, 2023
e8ef834
Update HISTORY.rst
vindelico Dec 1, 2023
9b629e6
Apply suggestions from code review
vindelico Dec 1, 2023
96bca11
Merge branch 'main' of github.com:Ouranosinc/xscen into select_indica…
vindelico Dec 1, 2023
d419b79
Merge branch 'select_indicators_for_available_vars' of github.com:Our…
vindelico Dec 1, 2023
a7a727e
Merge branch 'climatological_op' of github.com:Ouranosinc/xscen into …
vindelico Dec 1, 2023
40a9afc
Merge remote-tracking branch 'origin/main' into climatological_op
vindelico Dec 1, 2023
d74adcc
Review comments integrated, test for empty module returned added.
vindelico Dec 1, 2023
e2277dd
Updated tests from code review; try/except to adapt to FutureWarning'…
vindelico Dec 2, 2023
ac206a3
Updated notebooks 2_getting_started and 6_config with associated temp…
vindelico Dec 2, 2023
af032a8
Fixed some warnings in test_aggregate
vindelico Dec 2, 2023
c81817c
Merge branch 'climatological_op' into select_indicators_for_available…
vindelico Dec 2, 2023
0fa8382
Correct kernel in notebook 6_config
vindelico Dec 2, 2023
1ae4981
Fix bug in notebook 6_config
vindelico Dec 3, 2023
0a2f292
Merge branch 'climatological_op' into select_indicators_for_available…
vindelico Dec 3, 2023
256a333
Correct docstring select_inds_for_avail_vars
vindelico Dec 5, 2023
53f08d5
Select the set of indicators for available variables (#291)
vindelico Dec 5, 2023
2b9e299
Integrations from review; removed redundant tests for climatological_…
vindelico Dec 5, 2023
76250a7
Merge branch 'climatological_op' of github.com:Ouranosinc/xscen into …
vindelico Dec 5, 2023
ab1c226
Minor edits
vindelico Dec 5, 2023
71f374b
Update CHANGES.rst
RondeauG Dec 5, 2023
49a33a5
Fix CHANGES.rst
vindelico Dec 5, 2023
8940f68
Merge branch 'climatological_op' of github.com:Ouranosinc/xscen into …
vindelico Dec 5, 2023
b5644ac
produce_horizon uses climatological_op with option horizons_as_dim=Tr…
vindelico Dec 7, 2023
19b2675
horizons_as_dim changed to singular: horizon_as_dim
vindelico Dec 7, 2023
58de8bb
Fixed test_aggregate
vindelico Dec 8, 2023
a2919b5
Notebook 2_getting_started adjusted
vindelico Dec 8, 2023
fd4c350
Remove time coord from output from produce_horizon
vindelico Dec 8, 2023
49f493a
Do remove with call to climatological_op in produce_horizon
vindelico Dec 8, 2023
dacff59
Place removal of time coord after the call to climatological_op withi…
vindelico Dec 8, 2023
f831b38
Implementation of horizons_as_dim (previously periods_as_dim) using x…
vindelico Dec 9, 2023
2774148
Implementation of horizons_as_dim (previously periods_as_dim) cleaned
vindelico Dec 9, 2023
9c44e9a
Merge branch 'climatological_op' of github.com:Ouranosinc/xscen into …
vindelico Dec 9, 2023
ef03413
Merge branch 'main' into climatological_op
vindelico Dec 9, 2023
1a444da
Correct order of rename and set_coords in aggregate.py, remove void c…
vindelico Dec 18, 2023
faeb686
Merge branch 'climatological_op' of github.com:Ouranosinc/xscen into …
vindelico Dec 18, 2023
76959d1
Corrections notebook 2, aggregate.py
vindelico Dec 19, 2023
199a796
Merge branch 'main' into climatological_op
vindelico Dec 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion HISTORY.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ History

v0.8.0 (unreleased)
-------------------
Contributors to this version: Gabriel Rondeau-Genesse (:user:`RondeauG`), Pascal Bourgault (:user:`aulemahal`), Juliette Lavoie (:user:`juliettelavoie`), Sarah-Claude Bourdeau-Goulet (:user:`sarahclaude`).
Contributors to this version: Gabriel Rondeau-Genesse (:user:`RondeauG`), Pascal Bourgault (:user:`aulemahal`), Juliette Lavoie (:user:`juliettelavoie`), Sarah-Claude Bourdeau-Goulet (:user:`sarahclaude`), Marco Braun (:user:`vindelico`).

Announcements
^^^^^^^^^^^^^
* N/A

New features and enhancements
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* Replaced aggregation function ``climatological_mean()`` with ``climatological_op()`` offering more types of operations to aggregate over climate periods. (:pull:`290`)
vindelico marked this conversation as resolved.
Show resolved Hide resolved
* Added the ability to search for simulations that reach a given warming level. (:pull:`251`).
* ``xs.spatial_mean`` now accepts the ``region="global"`` keyword to perform a global average (:issue:`94`, :pull:`260`).
* ``xs.spatial_mean`` with ``method='xESMF'`` will also automatically segmentize polygons (down to a 1° resolution) to ensure a correct average (:pull:`260`).
Expand Down
218 changes: 212 additions & 6 deletions tests/test_aggregate.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@


class TestClimatologicalMean:
def _format(self, s):
RondeauG marked this conversation as resolved.
Show resolved Hide resolved
import xclim
op_format = (dict.fromkeys(("mean", "std", "var", "sum"), "adj") |
dict.fromkeys(("max", "min"), "noun"))
return xclim.core.formatting.default_formatter.format_field(s, op_format[s])

def test_daily(self):
ds = timeseries(
np.tile(np.arange(1, 13), 3),
Expand Down Expand Up @@ -42,10 +48,12 @@ def test_all_default(self, xrfreq):
# Test metadata
assert (
out.tas.attrs["description"]
== f"30-year mean of {ds.tas.attrs['description']}"
# == f"30-year mean of {ds.tas.attrs['description']}" # Changed to reflect output from climatological_op
vindelico marked this conversation as resolved.
Show resolved Hide resolved
== f"Climatological 30-year average of {ds.tas.attrs['description']}"
)
assert (
"30-year rolling average (non-centered) with a minimum of 30 years of data"
# "30-year rolling average (non-centered) with a minimum of 30 years of data"
"Climatological 30-year average (non-centered) with a minimum of 30 years of data"
in out.tas.attrs["history"]
)
assert out.attrs["cat:processing_level"] == "climatology"
Expand Down Expand Up @@ -76,10 +84,12 @@ def test_options(self, xrfreq):
# Test metadata
assert (
out.tas.attrs["description"]
== f"15-year mean of {ds.tas.attrs['description']}"
# == f"15-year mean of {ds.tas.attrs['description']}" # Changed to reflect output from climatological_op
== f"Climatological 15-year average of {ds.tas.attrs['description']}"
)
assert (
"15-year rolling average (non-centered) with a minimum of 15 years of data"
# "15-year rolling average (non-centered) with a minimum of 15 years of data"
"Climatological 15-year average (non-centered) with a minimum of 15 years of data"
in out.tas.attrs["history"]
)
assert out.attrs["cat:processing_level"] == "for_testing"
Expand Down Expand Up @@ -356,12 +366,15 @@ def test_options(self, periods, to_level):
assert out.attrs["cat:xrfreq"] == "fx"
assert all(v in out for v in ["tg_min", "growing_degree_days"])
assert (
f"{30 if periods is None else int(periods[0][1]) - int(periods[0][0]) + 1}-year mean of"
# f"{30 if periods is None else int(periods[0][1]) - int(periods[0][0]) + 1}-year mean of"
f"Climatological {30 if periods is None else int(periods[0][1]) - int(periods[0][0]) + 1}-year average of"
in out.tg_min.attrs["description"]
)
assert (
out.tg_min.attrs["description"].split(
f"{30 if periods is None else int(periods[0][1]) - int(periods[0][0]) + 1}-year mean of "
# f"{30 if periods is None else int(periods[0][1]) - int(periods[0][0]) + 1}-year mean of "
f"Climatological {30 if periods is None else int(periods[0][1]) - int(periods[0][0]) + 1}"
f"-year average of "
)[1]
!= self.ds.tas.attrs["description"]
)
Expand Down Expand Up @@ -548,3 +561,196 @@ def test_global(self, lonstart, method, exp):

avg = xs.aggregate.spatial_mean(ds, method=method, region="global")
np.testing.assert_allclose(avg.tas, exp)


class TestClimatologicalOp:
def _format(self, s):
vindelico marked this conversation as resolved.
Show resolved Hide resolved
import xclim
op_format = (dict.fromkeys(("mean", "std", "var", "sum"), "adj") |
dict.fromkeys(("max", "min"), "noun"))
return xclim.core.formatting.default_formatter.format_field(s, op_format[s])

def test_daily(self):
ds = timeseries(
np.tile(np.arange(1, 13), 3),
variable="tas",
start="2001-01-01",
freq="D",
as_dataset=True,
)
with pytest.raises(NotImplementedError):
xs.climatological_op(ds, op='mean')

@pytest.mark.parametrize("xrfreq", ["MS", "AS-JAN"])
@pytest.mark.parametrize("op", ["max", "mean", "median", "min", "std", "sum", "var", "linregress"])
def test_all_default(self, xrfreq, op):
o = 12 if xrfreq == "MS" else 1

ds = timeseries(
np.tile(np.arange(1, o + 1), 30),
variable="tas",
start="2001-01-01",
freq=xrfreq,
as_dataset=True,
)
out = xs.climatological_op(ds, op=op)
expected = (dict.fromkeys(("max", "mean", "median", "min"), np.arange(1, o + 1)) |
dict.fromkeys(("std", "var"), np.zeros(o)) |
dict({"sum": np.arange(1, o + 1) * 30}) |
dict({"linregress": np.array([
np.zeros(o),
np.arange(1, o + 1),
np.zeros(o),
np.ones(o),
np.zeros(o),
np.zeros(o)]).T})
)
# Test output variable name, values, length, horizon
assert list(out.data_vars.keys()) == [f"tas_clim_{op}"]
np.testing.assert_array_equal(out[f"tas_clim_{op}"], expected[op])
assert len(out.time) == (o * len(np.unique(out.horizon.values)))
np.testing.assert_array_equal(out.time[0], ds.time[0])
assert (out.horizon == "2001-2030").all()
# Test metadata
operation = self._format(op) if op not in ['median', 'linregress'] else op
assert (
out[f"tas_clim_{op}"].attrs["description"]
== f"Climatological 30-year {operation} of {ds.tas.attrs['description']}"
)
assert (
f"Climatological 30-year {operation} (non-centered) with a minimum of 30 years of data"
in out[f"tas_clim_{op}"].attrs["history"]
)
assert out.attrs["cat:processing_level"] == "climatology"

@pytest.mark.parametrize("xrfreq", ["MS", "AS-JAN"])
@pytest.mark.parametrize("op", ['max', 'mean', 'median', 'min', 'std', 'sum', 'var', 'linregress'])
def test_options(self, xrfreq, op):
o = 12 if xrfreq == "MS" else 1

ds = timeseries(
np.tile(np.arange(1, o + 1), 30),
variable="tas",
start="2001-01-01",
freq=xrfreq,
as_dataset=True,
)
out = xs.climatological_op(ds, op=op, window=15, stride=5, to_level="for_testing")
expected = (dict.fromkeys(("max", "mean", "median", "min"),
np.tile(np.arange(1, o + 1), len(np.unique(out.horizon.values)))) |
dict.fromkeys(("std", "var"),
np.tile(np.zeros(o), len(np.unique(out.horizon.values)))) |
dict({"sum": np.tile(np.arange(1, o + 1) * 15, len(np.unique(out.horizon.values)))}) |
dict({"linregress": np.tile(np.array([
np.zeros(o),
np.arange(1, o + 1),
np.zeros(o),
np.ones(o),
np.zeros(o),
np.zeros(o)]), len(np.unique(out.horizon.values))).T})
)
# Test output values
np.testing.assert_array_equal(
out[f"tas_clim_{op}"],
expected[op],
)
assert len(out.time) == (o * len(np.unique(out.horizon.values)))
np.testing.assert_array_equal(out.time[0], ds.time[0])
assert {"2001-2015", "2006-2020", "2011-2025", "2016-2030"}.issubset(
out.horizon.values
)
# Test metadata
operation = self._format(op) if op not in ['median', 'linregress'] else op
assert (
out[f"tas_clim_{op}"].attrs["description"]
== f"Climatological 15-year {operation} of {ds.tas.attrs['description']}"
)
assert (
f"Climatological 15-year {operation} (non-centered) with a minimum of 15 years of data"
in out[f"tas_clim_{op}"].attrs["history"]
)
assert out.attrs["cat:processing_level"] == "for_testing"

@pytest.mark.parametrize("op", ["mean", "linregress"])
def test_minperiods(self, op):
ds = timeseries(
np.tile(np.arange(1, 5), 30),
variable="tas",
start="2001-03-01",
freq="QS-DEC",
as_dataset=True,
)
ds = ds.where(ds["time"].dt.strftime("%Y-%m-%d") != "2030-12-01")

op = 'mean'
out = xs.climatological_op(ds, op=op, window=30)
assert all(np.isreal(out[f"tas_clim_{op}"]))
assert len(out.time) == 4
np.testing.assert_array_equal(out[f"tas_clim_{op}"], np.arange(1, 5))

# min_periods as int
out = xs.climatological_op(ds, op=op, window=30, min_periods=30)
assert np.sum(np.isnan(out[f"tas_clim_{op}"])) == 1

# min_periods as float
out = xs.climatological_op(ds, op=op, window=30, min_periods=.5)
assert "minimum of 15 years of data" in out[f"tas_clim_{op}"].attrs["history"]
assert np.sum(np.isnan(out[f"tas_clim_{op}"])) == 0

with pytest.raises(ValueError):
xs.climatological_op(ds, op=op, window=5, min_periods=6)

@pytest.mark.parametrize("op", ["mean", "linregress"])
def test_periods(self, op):
ds1 = timeseries(
np.tile(np.arange(1, 2), 10),
variable="tas",
start="2001-01-01",
freq="AS-JAN",
as_dataset=True,
)
ds2 = timeseries(
np.tile(np.arange(1, 2), 10),
variable="tas",
start="2021-01-01",
freq="AS-JAN",
as_dataset=True,
)

ds = xr.concat([ds1, ds2], dim="time")
with pytest.raises(ValueError):
xs.climatological_op(ds, op=op)

out = xs.climatological_op(ds, op='mean', periods=[["2001", "2010"], ["2021", "2030"]])
assert len(out.time) == 2
assert {"2001-2010", "2021-2030"}.issubset(out.horizon.values)

@pytest.mark.parametrize("cal", ["proleptic_gregorian", "noleap", "360_day"])
def test_calendars(self, cal):
ds = timeseries(
np.tile(np.arange(1, 2), 30),
variable="tas",
start="2001-01-01",
freq="AS-JAN",
as_dataset=True,
)

out = xs.climatological_op(ds.convert_calendar(cal, align_on="date"), op='mean')
assert out.time.dt.calendar == cal

def test_periods_as_dim(self):
ds = timeseries(
np.tile(np.arange(1, 13), 30),
variable="tas",
start="2001-01-01",
freq="MS",
as_dataset=True,
)
out = xs.climatological_op(ds, op="mean", window=10, stride=5, periods_as_dim=True)
assert (out.tas_clim_mean.values == np.tile(np.arange(1, 13), (5, 1))).all()
assert out.dims == {'period': 5, 'month': 12}
assert out.time.dims == ('period', 'month')
assert (out.period.values == ['2001-2010', '2006-2015', '2011-2020', '2016-2025', '2021-2030']).all()
assert (out.month.values ==
['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']).all()
Loading