diff --git a/esmvalcore/preprocessor/_derive/__init__.py b/esmvalcore/preprocessor/_derive/__init__.py index 6ef2ec9d2a..27bffa38d7 100644 --- a/esmvalcore/preprocessor/_derive/__init__.py +++ b/esmvalcore/preprocessor/_derive/__init__.py @@ -52,11 +52,11 @@ def get_required(short_name, project): List of dictionaries (including at least the key `short_name`). """ - if short_name not in ALL_DERIVED_VARIABLES: + if short_name.lower() not in ALL_DERIVED_VARIABLES: raise NotImplementedError( f"Cannot derive variable '{short_name}', no derivation script " f"available") - DerivedVariable = ALL_DERIVED_VARIABLES[short_name] # noqa: N806 + DerivedVariable = ALL_DERIVED_VARIABLES[short_name.lower()] # noqa: N806 variables = deepcopy(DerivedVariable().required(project)) return variables diff --git a/esmvalcore/preprocessor/_derive/sfcwind.py b/esmvalcore/preprocessor/_derive/sfcwind.py new file mode 100644 index 0000000000..2af241f517 --- /dev/null +++ b/esmvalcore/preprocessor/_derive/sfcwind.py @@ -0,0 +1,35 @@ +"""Derivation of variable `sfcWind`.""" + +from iris import NameConstraint + +from ._baseclass import DerivedVariableBase + + +class DerivedVariable(DerivedVariableBase): + """Derivation of variable `sfcWind`.""" + + @staticmethod + def required(project): + """Declare the variables needed for derivation.""" + required = [ + { + 'short_name': 'uas' + }, + { + 'short_name': 'vas' + }, + ] + return required + + @staticmethod + def calculate(cubes): + """Compute near-surface wind speed. + + Wind speed derived from eastward and northward components. + """ + uas_cube = cubes.extract_cube(NameConstraint(var_name='uas')) + vas_cube = cubes.extract_cube(NameConstraint(var_name='vas')) + + sfcwind_cube = (uas_cube**2 + vas_cube**2)**0.5 + + return sfcwind_cube diff --git a/tests/integration/cmor/_fixes/native6/test_era5.py b/tests/integration/cmor/_fixes/native6/test_era5.py index 6b8a4922b7..70b432541d 100644 --- a/tests/integration/cmor/_fixes/native6/test_era5.py +++ b/tests/integration/cmor/_fixes/native6/test_era5.py @@ -999,6 +999,44 @@ def uas_cmor_e1hr(): return iris.cube.CubeList([cube]) +def vas_era5_hourly(): + time = _era5_time('hourly') + cube = iris.cube.Cube( + _era5_data('hourly'), + long_name='10m_v_component_of_wind', + var_name='v10', + units='m s-1', + dim_coords_and_dims=[ + (time, 0), + (_era5_latitude(), 1), + (_era5_longitude(), 2), + ], + ) + return iris.cube.CubeList([cube]) + + +def vas_cmor_e1hr(): + cmor_table = CMOR_TABLES['native6'] + vardef = cmor_table.get_variable('E1hr', 'vas') + time = _cmor_time('E1hr') + data = _cmor_data('E1hr') + cube = iris.cube.Cube( + data.astype('float32'), + long_name=vardef.long_name, + var_name=vardef.short_name, + standard_name=vardef.standard_name, + units=Unit(vardef.units), + dim_coords_and_dims=[ + (time, 0), + (_cmor_latitude(), 1), + (_cmor_longitude(), 2), + ], + attributes={'comment': COMMENT}, + ) + cube.add_aux_coord(_cmor_aux_height(10.)) + return iris.cube.CubeList([cube]) + + VARIABLES = [ pytest.param(a, b, c, d, id=c + '_' + d) for (a, b, c, d) in [ (cl_era5_monthly(), cl_cmor_amon(), 'cl', 'Amon'), @@ -1022,6 +1060,7 @@ def uas_cmor_e1hr(): (tasmax_era5_hourly(), tasmax_cmor_e1hr(), 'tasmax', 'E1hr'), (tasmin_era5_hourly(), tasmin_cmor_e1hr(), 'tasmin', 'E1hr'), (uas_era5_hourly(), uas_cmor_e1hr(), 'uas', 'E1hr'), + (vas_era5_hourly(), vas_cmor_e1hr(), 'vas', 'E1hr'), (zg_era5_monthly(), zg_cmor_amon(), 'zg', 'Amon'), ] ] @@ -1045,6 +1084,16 @@ def test_cmorization(era5_cubes, cmor_cubes, var, mip): coord.points = np.round(coord.points, decimals=7) if coord.bounds is not None: coord.bounds = np.round(coord.bounds, decimals=7) + print("Test results for variable/MIP: ", var, mip) print('cmor_cube:', cmor_cube) print('fixed_cube:', fixed_cube) + print('cmor_cube data:', cmor_cube.data) + print('fixed_cube data:', fixed_cube.data) + print("cmor_cube coords:") + for coord in cmor_cube.coords(): + print(coord) + print("\n") + print("fixed_cube coords:") + for coord in fixed_cube.coords(): + print(coord) assert fixed_cube == cmor_cube diff --git a/tests/unit/preprocessor/_derive/test_sfcwind.py b/tests/unit/preprocessor/_derive/test_sfcwind.py new file mode 100644 index 0000000000..a249c4fe9c --- /dev/null +++ b/tests/unit/preprocessor/_derive/test_sfcwind.py @@ -0,0 +1,49 @@ +"""Test derivation of ``sfcwind``.""" +import numpy as np +import pytest +from iris.cube import CubeList + +from esmvalcore.preprocessor._derive import sfcwind + +from .test_shared import get_cube + + +@pytest.fixture +def cubes(): + """Input cubes for derivation of ``sfcwind``.""" + uas_cube = get_cube([[[3.0]]], + air_pressure_coord=False, + standard_name='eastward_wind', + var_name='uas', + units='m s-1') + vas_cube = get_cube([[[4.0]]], + air_pressure_coord=False, + standard_name='northward_wind', + var_name='vas', + units='m s-1') + return CubeList([uas_cube, vas_cube]) + + +def test_sfcwind_calculate(cubes): + """Test function ``calculate``.""" + derived_var = sfcwind.DerivedVariable() + required_vars = derived_var.required("CMIP5") + expected_required_vars = [ + { + 'short_name': 'uas' + }, + { + 'short_name': 'vas' + }, + ] + assert required_vars == expected_required_vars + out_cube = derived_var.calculate(cubes) + assert out_cube.shape == (1, 1, 1) + assert out_cube.units == 'm s-1' + assert out_cube.coords('time') + assert out_cube.coords('latitude') + assert out_cube.coords('longitude') + np.testing.assert_allclose(out_cube.data, [[[5.0]]]) + np.testing.assert_allclose(out_cube.coord('time').points, [0.0]) + np.testing.assert_allclose(out_cube.coord('latitude').points, [45.0]) + np.testing.assert_allclose(out_cube.coord('longitude').points, [10.0])