Skip to content

Commit 3666bfa

Browse files
authored
Add derivation of sea ice extent (#1695)
<!-- Thank you for contributing to our project! Please do not delete this text completely, but read the text below and keep items that seem relevant. If in doubt, just keep everything and add your own text at the top, a reviewer will update the checklist for you. --> ## Description <!-- Please describe your changes here, especially focusing on why this pull request makes ESMValCore better and what problem it solves. Before you start, please read our contribution guidelines: https://docs.esmvaltool.org/projects/ESMValCore/en/latest/contributing.html Please fill in the GitHub issue that is closed by this pull request, e.g. Closes #1903 --> This PR allows to compute the sea ice extent as a derived variable in order to reduce the amount of saved data during the preprocessing Closes #1693 Link to documentation: *** ## [Before you get started](https://docs.esmvaltool.org/projects/ESMValCore/en/latest/contributing.html#getting-started) - [x] [☝ Create an issue](https://github.com/ESMValGroup/ESMValCore/issues) to discuss what you are going to do ## [Checklist](https://docs.esmvaltool.org/projects/ESMValCore/en/latest/contributing.html#checklist-for-pull-requests) It is the responsibility of the author to make sure the pull request is ready to review. The icons indicate whether the item will be subject to the [🛠 Technical][1] or [🧪 Scientific][2] review. <!-- The next two lines turn the 🛠 and 🧪 below into hyperlinks --> [1]: https://docs.esmvaltool.org/en/latest/community/review.html#technical-review [2]: https://docs.esmvaltool.org/en/latest/community/review.html#scientific-review - [x] [🧪][2] The new functionality is [relevant and scientifically sound](https://docs.esmvaltool.org/projects/ESMValCore/en/latest/contributing.html#scientific-relevance) - [x] [🛠][1] This pull request has a [descriptive title and labels](https://docs.esmvaltool.org/projects/ESMValCore/en/latest/contributing.html#pull-request-title-and-label) - [x] [🛠][1] Code is written according to the [code quality guidelines](https://docs.esmvaltool.org/projects/ESMValCore/en/latest/contributing.html#code-quality) ~- [ ] [🧪][2] and [🛠][1] [Documentation](https://docs.esmvaltool.org/projects/ESMValCore/en/latest/contributing.html#documentation) is available~ - [x] [🛠][1] [Unit tests](https://docs.esmvaltool.org/projects/ESMValCore/en/latest/contributing.html#tests) have been added - [x] [🛠][1] Changes are [backward compatible](https://docs.esmvaltool.org/projects/ESMValCore/en/latest/contributing.html#backward-compatibility) ~- [ ] [🛠][1] Any changed [dependencies have been added or removed](https://docs.esmvaltool.org/projects/ESMValCore/en/latest/contributing.html#dependencies) correctly~ - [x] [🛠][1] The [list of authors](https://docs.esmvaltool.org/projects/ESMValCore/en/latest/contributing.html#list-of-authors) is up to date - [x] [🛠][1] All [checks below this pull request](https://docs.esmvaltool.org/projects/ESMValCore/en/latest/contributing.html#pull-request-checks) were successful *** To help with the number pull requests: - 🙏 We kindly ask you to [review](https://docs.esmvaltool.org/en/latest/community/review.html#review-of-pull-requests) two other [open pull requests](https://github.com/ESMValGroup/ESMValCore/pulls) in this repository
1 parent 82946b8 commit 3666bfa

File tree

3 files changed

+206
-0
lines changed

3 files changed

+206
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
SOURCE: CMIP5
2+
!============
3+
variable_entry: siextent
4+
!============
5+
modeling_realm: seaIce
6+
!----------------------------------
7+
! Variable attributes:
8+
!----------------------------------
9+
standard_name:
10+
units: m2
11+
cell_methods: area: mean where sea time: mean
12+
cell_measures: area: areacello
13+
long_name: Sea Ice Extent
14+
comment:
15+
!----------------------------------
16+
! Additional variable information:
17+
!----------------------------------
18+
dimensions: longitude latitude time
19+
type: real
20+
valid_min:
21+
valid_max:
22+
!----------------------------------
23+
!
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""Derivation of variable `sithick`."""
2+
import logging
3+
4+
import dask.array as da
5+
import iris
6+
from iris import Constraint
7+
8+
from esmvalcore.exceptions import RecipeError
9+
from ._baseclass import DerivedVariableBase
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
class DerivedVariable(DerivedVariableBase):
15+
"""Derivation of variable `siextent`."""
16+
17+
@staticmethod
18+
def required(project):
19+
"""Declare the variables needed for derivation."""
20+
required = [
21+
{
22+
'short_name': 'sic',
23+
'optional': 'true'
24+
},
25+
{
26+
'short_name': 'siconca',
27+
'optional': 'true'
28+
}]
29+
return required
30+
31+
@staticmethod
32+
def calculate(cubes):
33+
"""Compute sea ice extent.
34+
35+
Returns an array of ones in every grid point where
36+
the sea ice area fraction has values > 15 .
37+
38+
Use in combination with the preprocessor
39+
`area_statistics(operator='sum')` to weigh by the area and
40+
compute global or regional sea ice extent values.
41+
42+
Arguments
43+
---------
44+
cubes: cubelist containing sea ice area fraction.
45+
46+
Returns
47+
-------
48+
Cube containing sea ice extent.
49+
"""
50+
try:
51+
sic = cubes.extract_cube(Constraint(name='sic'))
52+
except iris.exceptions.ConstraintMismatchError:
53+
try:
54+
sic = cubes.extract_cube(Constraint(name='siconca'))
55+
except iris.exceptions.ConstraintMismatchError as exc:
56+
raise RecipeError(
57+
'Derivation of siextent failed due to missing variables '
58+
'sic and siconca.') from exc
59+
60+
ones = da.ones_like(sic)
61+
siextent_data = da.ma.masked_where(sic.lazy_data() < 15., ones)
62+
siextent = sic.copy(siextent_data)
63+
siextent.units = 'm2'
64+
65+
return siextent
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""Test derivation of `ohc`."""
2+
import cf_units
3+
import iris
4+
import numpy as np
5+
import pytest
6+
7+
import esmvalcore.preprocessor._derive.siextent as siextent
8+
from esmvalcore.exceptions import RecipeError
9+
10+
11+
@pytest.fixture
12+
def cubes_sic():
13+
sic_name = 'sea_ice_area_fraction'
14+
time_coord = iris.coords.DimCoord([0., 1., 2.],
15+
standard_name='time')
16+
sic_cube = iris.cube.Cube([[[20, 10], [10, 10]],
17+
[[10, 10], [10, 10]],
18+
[[10, 10], [10, 10]]],
19+
units='%',
20+
standard_name=sic_name,
21+
var_name='sic',
22+
dim_coords_and_dims=[(time_coord, 0)])
23+
return iris.cube.CubeList([sic_cube])
24+
25+
26+
@pytest.fixture
27+
def cubes_siconca():
28+
sic_name = 'sea_ice_area_fraction'
29+
time_coord = iris.coords.DimCoord([0., 1., 2.],
30+
standard_name='time')
31+
sic_cube = iris.cube.Cube([[[20, 10], [10, 10]],
32+
[[10, 10], [10, 10]],
33+
[[10, 10], [10, 10]]],
34+
units='%',
35+
standard_name=sic_name,
36+
var_name='siconca',
37+
dim_coords_and_dims=[(time_coord, 0)])
38+
return iris.cube.CubeList([sic_cube])
39+
40+
41+
@pytest.fixture
42+
def cubes():
43+
sic_name = 'sea_ice_area_fraction'
44+
time_coord = iris.coords.DimCoord([0., 1., 2.],
45+
standard_name='time')
46+
sic_cube = iris.cube.Cube([[[20, 10], [10, 10]],
47+
[[10, 10], [10, 10]],
48+
[[10, 10], [10, 10]]],
49+
units='%',
50+
standard_name=sic_name,
51+
var_name='sic',
52+
dim_coords_and_dims=[(time_coord, 0)])
53+
siconca_cube = iris.cube.Cube([[[20, 10], [10, 10]],
54+
[[10, 10], [10, 10]],
55+
[[10, 10], [10, 10]]],
56+
units='%',
57+
standard_name=sic_name,
58+
var_name='siconca',
59+
dim_coords_and_dims=[(time_coord, 0)])
60+
return iris.cube.CubeList([sic_cube, siconca_cube])
61+
62+
63+
def test_siextent_calculation_sic(cubes_sic):
64+
"""Test function ``calculate`` when sic is available."""
65+
derived_var = siextent.DerivedVariable()
66+
out_cube = derived_var.calculate(cubes_sic)
67+
assert out_cube.units == cf_units.Unit('m2')
68+
out_data = out_cube.data
69+
expected = np.ma.ones_like(cubes_sic[0].data)
70+
expected.mask = True
71+
expected[0][0][0] = 1.
72+
np.testing.assert_array_equal(out_data.mask, expected.mask)
73+
np.testing.assert_array_equal(out_data[0][0][0], expected[0][0][0])
74+
75+
76+
def test_siextent_calculation_siconca(cubes_siconca):
77+
"""Test function ``calculate`` when siconca is available."""
78+
derived_var = siextent.DerivedVariable()
79+
out_cube = derived_var.calculate(cubes_siconca)
80+
assert out_cube.units == cf_units.Unit('m2')
81+
out_data = out_cube.data
82+
expected = np.ma.ones_like(cubes_siconca[0].data)
83+
expected.mask = True
84+
expected[0][0][0] = 1.
85+
np.testing.assert_array_equal(out_data.mask, expected.mask)
86+
np.testing.assert_array_equal(out_data[0][0][0], expected[0][0][0])
87+
88+
89+
def test_siextent_calculation(cubes):
90+
"""Test function ``calculate`` when sic and siconca are available."""
91+
derived_var = siextent.DerivedVariable()
92+
out_cube = derived_var.calculate(cubes)
93+
assert out_cube.units == cf_units.Unit('m2')
94+
out_data = out_cube.data
95+
expected = np.ma.ones_like(cubes[0].data)
96+
expected.mask = True
97+
expected[0][0][0] = 1.
98+
np.testing.assert_array_equal(out_data.mask, expected.mask)
99+
np.testing.assert_array_equal(out_data[0][0][0], expected[0][0][0])
100+
101+
102+
def test_siextent_no_data(cubes_sic):
103+
derived_var = siextent.DerivedVariable()
104+
cubes_sic[0].var_name = 'wrong'
105+
msg = ('Derivation of siextent failed due to missing variables '
106+
'sic and siconca.')
107+
with pytest.raises(RecipeError, match=msg):
108+
derived_var.calculate(cubes_sic)
109+
110+
111+
def test_siextent_required():
112+
"""Test function ``required``."""
113+
derived_var = siextent.DerivedVariable()
114+
output = derived_var.required(None)
115+
assert output == [
116+
{'short_name': 'sic', 'optional': 'true'},
117+
{'short_name': 'siconca', 'optional': 'true'}
118+
]

0 commit comments

Comments
 (0)