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

Add derivation of sea ice extent #1695

Merged
merged 18 commits into from
Nov 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions esmvalcore/cmor/tables/custom/CMOR_siextent.dat
Original file line number Diff line number Diff line change
@@ -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:
!----------------------------------
!
65 changes: 65 additions & 0 deletions esmvalcore/preprocessor/_derive/siextent.py
Original file line number Diff line number Diff line change
@@ -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
118 changes: 118 additions & 0 deletions tests/unit/preprocessor/_derive/test_siextent.py
Original file line number Diff line number Diff line change
@@ -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'}
]