diff --git a/esmvalcore/cmor/tables/custom/CMOR_siextent.dat b/esmvalcore/cmor/tables/custom/CMOR_siextent.dat new file mode 100644 index 0000000000..736744595f --- /dev/null +++ b/esmvalcore/cmor/tables/custom/CMOR_siextent.dat @@ -0,0 +1,23 @@ +SOURCE: CMIP5 +!============ +variable_entry: siextent +!============ +modeling_realm: seaIce +!---------------------------------- +! Variable attributes: +!---------------------------------- +standard_name: +units: m2 +cell_methods: area: mean where sea time: mean +cell_measures: area: areacello +long_name: Sea Ice Extent +comment: +!---------------------------------- +! Additional variable information: +!---------------------------------- +dimensions: longitude latitude time +type: real +valid_min: +valid_max: +!---------------------------------- +! diff --git a/esmvalcore/preprocessor/_derive/siextent.py b/esmvalcore/preprocessor/_derive/siextent.py new file mode 100644 index 0000000000..eee25e5a98 --- /dev/null +++ b/esmvalcore/preprocessor/_derive/siextent.py @@ -0,0 +1,65 @@ +"""Derivation of variable `sithick`.""" +import logging + +import dask.array as da +import iris +from iris import Constraint + +from esmvalcore.exceptions import RecipeError +from ._baseclass import DerivedVariableBase + +logger = logging.getLogger(__name__) + + +class DerivedVariable(DerivedVariableBase): + """Derivation of variable `siextent`.""" + + @staticmethod + def required(project): + """Declare the variables needed for derivation.""" + required = [ + { + 'short_name': 'sic', + 'optional': 'true' + }, + { + 'short_name': 'siconca', + 'optional': 'true' + }] + return required + + @staticmethod + def calculate(cubes): + """Compute sea ice extent. + + Returns an array of ones in every grid point where + the sea ice area fraction has values > 15 . + + Use in combination with the preprocessor + `area_statistics(operator='sum')` to weigh by the area and + compute global or regional sea ice extent values. + + Arguments + --------- + cubes: cubelist containing sea ice area fraction. + + Returns + ------- + Cube containing sea ice extent. + """ + try: + sic = cubes.extract_cube(Constraint(name='sic')) + except iris.exceptions.ConstraintMismatchError: + try: + sic = cubes.extract_cube(Constraint(name='siconca')) + except iris.exceptions.ConstraintMismatchError as exc: + raise RecipeError( + 'Derivation of siextent failed due to missing variables ' + 'sic and siconca.') from exc + + ones = da.ones_like(sic) + siextent_data = da.ma.masked_where(sic.lazy_data() < 15., ones) + siextent = sic.copy(siextent_data) + siextent.units = 'm2' + + return siextent diff --git a/tests/unit/preprocessor/_derive/test_siextent.py b/tests/unit/preprocessor/_derive/test_siextent.py new file mode 100644 index 0000000000..4b362d6731 --- /dev/null +++ b/tests/unit/preprocessor/_derive/test_siextent.py @@ -0,0 +1,118 @@ +"""Test derivation of `ohc`.""" +import cf_units +import iris +import numpy as np +import pytest + +import esmvalcore.preprocessor._derive.siextent as siextent +from esmvalcore.exceptions import RecipeError + + +@pytest.fixture +def cubes_sic(): + sic_name = 'sea_ice_area_fraction' + time_coord = iris.coords.DimCoord([0., 1., 2.], + standard_name='time') + sic_cube = iris.cube.Cube([[[20, 10], [10, 10]], + [[10, 10], [10, 10]], + [[10, 10], [10, 10]]], + units='%', + standard_name=sic_name, + var_name='sic', + dim_coords_and_dims=[(time_coord, 0)]) + return iris.cube.CubeList([sic_cube]) + + +@pytest.fixture +def cubes_siconca(): + sic_name = 'sea_ice_area_fraction' + time_coord = iris.coords.DimCoord([0., 1., 2.], + standard_name='time') + sic_cube = iris.cube.Cube([[[20, 10], [10, 10]], + [[10, 10], [10, 10]], + [[10, 10], [10, 10]]], + units='%', + standard_name=sic_name, + var_name='siconca', + dim_coords_and_dims=[(time_coord, 0)]) + return iris.cube.CubeList([sic_cube]) + + +@pytest.fixture +def cubes(): + sic_name = 'sea_ice_area_fraction' + time_coord = iris.coords.DimCoord([0., 1., 2.], + standard_name='time') + sic_cube = iris.cube.Cube([[[20, 10], [10, 10]], + [[10, 10], [10, 10]], + [[10, 10], [10, 10]]], + units='%', + standard_name=sic_name, + var_name='sic', + dim_coords_and_dims=[(time_coord, 0)]) + siconca_cube = iris.cube.Cube([[[20, 10], [10, 10]], + [[10, 10], [10, 10]], + [[10, 10], [10, 10]]], + units='%', + standard_name=sic_name, + var_name='siconca', + dim_coords_and_dims=[(time_coord, 0)]) + return iris.cube.CubeList([sic_cube, siconca_cube]) + + +def test_siextent_calculation_sic(cubes_sic): + """Test function ``calculate`` when sic is available.""" + derived_var = siextent.DerivedVariable() + out_cube = derived_var.calculate(cubes_sic) + assert out_cube.units == cf_units.Unit('m2') + out_data = out_cube.data + expected = np.ma.ones_like(cubes_sic[0].data) + expected.mask = True + expected[0][0][0] = 1. + np.testing.assert_array_equal(out_data.mask, expected.mask) + np.testing.assert_array_equal(out_data[0][0][0], expected[0][0][0]) + + +def test_siextent_calculation_siconca(cubes_siconca): + """Test function ``calculate`` when siconca is available.""" + derived_var = siextent.DerivedVariable() + out_cube = derived_var.calculate(cubes_siconca) + assert out_cube.units == cf_units.Unit('m2') + out_data = out_cube.data + expected = np.ma.ones_like(cubes_siconca[0].data) + expected.mask = True + expected[0][0][0] = 1. + np.testing.assert_array_equal(out_data.mask, expected.mask) + np.testing.assert_array_equal(out_data[0][0][0], expected[0][0][0]) + + +def test_siextent_calculation(cubes): + """Test function ``calculate`` when sic and siconca are available.""" + derived_var = siextent.DerivedVariable() + out_cube = derived_var.calculate(cubes) + assert out_cube.units == cf_units.Unit('m2') + out_data = out_cube.data + expected = np.ma.ones_like(cubes[0].data) + expected.mask = True + expected[0][0][0] = 1. + np.testing.assert_array_equal(out_data.mask, expected.mask) + np.testing.assert_array_equal(out_data[0][0][0], expected[0][0][0]) + + +def test_siextent_no_data(cubes_sic): + derived_var = siextent.DerivedVariable() + cubes_sic[0].var_name = 'wrong' + msg = ('Derivation of siextent failed due to missing variables ' + 'sic and siconca.') + with pytest.raises(RecipeError, match=msg): + derived_var.calculate(cubes_sic) + + +def test_siextent_required(): + """Test function ``required``.""" + derived_var = siextent.DerivedVariable() + output = derived_var.required(None) + assert output == [ + {'short_name': 'sic', 'optional': 'true'}, + {'short_name': 'siconca', 'optional': 'true'} + ]