diff --git a/environment.yml b/environment.yml index b7f30b1167..c17f9de011 100644 --- a/environment.yml +++ b/environment.yml @@ -30,6 +30,7 @@ dependencies: - pip!=21.3 - prov - psutil + - py-cordex - pybtex - python>=3.8 - python-stratify diff --git a/esmvalcore/cmor/_fixes/cordex/__init__ .py b/esmvalcore/cmor/_fixes/cordex/__init__ .py new file mode 100644 index 0000000000..093969370f --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/__init__ .py @@ -0,0 +1 @@ +"""Fixes for CORDEX data.""" diff --git a/esmvalcore/cmor/_fixes/cordex/cnrm_cerfacs_cnrm_cm5/__init__.py b/esmvalcore/cmor/_fixes/cordex/cnrm_cerfacs_cnrm_cm5/__init__.py new file mode 100644 index 0000000000..093969370f --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/cnrm_cerfacs_cnrm_cm5/__init__.py @@ -0,0 +1 @@ +"""Fixes for CORDEX data.""" diff --git a/esmvalcore/cmor/_fixes/cordex/cnrm_cerfacs_cnrm_cm5/cnrm_aladin63.py b/esmvalcore/cmor/_fixes/cordex/cnrm_cerfacs_cnrm_cm5/cnrm_aladin63.py new file mode 100644 index 0000000000..af0348767a --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/cnrm_cerfacs_cnrm_cm5/cnrm_aladin63.py @@ -0,0 +1,33 @@ +"""Fixes for rcm CNRM-ALADIN63 driven by CNRM-CERFACS-CNRM-CM5.""" +import numpy as np + +from esmvalcore.cmor._fixes.cordex.cordex_fixes import TimeLongName as BaseFix +from esmvalcore.cmor._fixes.shared import add_scalar_height_coord +from esmvalcore.cmor.fix import Fix + + +class Tas(Fix): + """Fixes for tas.""" + + def fix_metadata(self, cubes): + """Add height (2m) coordinate and correct long_name for time. + + Parameters + ---------- + cubes : iris.cube.CubeList + Input cubes. + + Returns + ------- + iris.cube.CubeList + """ + for cube in cubes: + add_scalar_height_coord(cube) + if cube.coord('height').points != 2.: + cube.coord('height').points = np.ma.array([2.0]) + cube.coord('time').long_name = 'time' + + return cubes + + +Pr = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/cnrm_cerfacs_cnrm_cm5/mohc_hadrem3_ga7_05.py b/esmvalcore/cmor/_fixes/cordex/cnrm_cerfacs_cnrm_cm5/mohc_hadrem3_ga7_05.py new file mode 100644 index 0000000000..fd31e9b8a6 --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/cnrm_cerfacs_cnrm_cm5/mohc_hadrem3_ga7_05.py @@ -0,0 +1,7 @@ +"""Fixes for rcm MOHC-HadREM3-GA7-05 driven by CNRM-CERFACS-CNRM-CM5.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + MOHCHadREM3GA705 as BaseFix) + +Tas = BaseFix + +Pr = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/cordex_fixes.py b/esmvalcore/cmor/_fixes/cordex/cordex_fixes.py new file mode 100644 index 0000000000..9ccbfd4ed3 --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/cordex_fixes.py @@ -0,0 +1,206 @@ +"""Fixes that are shared between datasets and drivers.""" +import logging +from functools import lru_cache + +import cordex as cx +import iris +import numpy as np +from cf_units import Unit +from iris.coord_systems import LambertConformal, RotatedGeogCS + +from esmvalcore.cmor.fix import Fix +from esmvalcore.exceptions import RecipeError + +logger = logging.getLogger(__name__) + + +@lru_cache +def _get_domain(data_domain): + domain = cx.cordex_domain(data_domain, add_vertices=True) + return domain + + +@lru_cache +def _get_domain_info(data_domain): + domain_info = cx.domain_info(data_domain) + return domain_info + + +class MOHCHadREM3GA705(Fix): + """General fix for MOHC-HadREM3-GA7-05.""" + + def fix_metadata(self, cubes): + """Fix time long_name, and latitude and longitude var_name. + + Parameters + ---------- + cubes : iris.cube.CubeList + Input cubes. + + Returns + ------- + iris.cube.CubeList + """ + for cube in cubes: + cube.coord('latitude').var_name = 'lat' + cube.coord('longitude').var_name = 'lon' + cube.coord('time').long_name = 'time' + + return cubes + + +class TimeLongName(Fix): + """Fixes for time coordinate.""" + + def fix_metadata(self, cubes): + """Fix time long_name. + + Parameters + ---------- + cubes : iris.cube.CubeList + Input cubes. + + Returns + ------- + iris.cube.CubeList + """ + for cube in cubes: + cube.coord('time').long_name = 'time' + + return cubes + + +class CLMcomCCLM4817(Fix): + """Fixes for CLMcom-CCLM4-8-17.""" + + def fix_metadata(self, cubes): + """Fix calendars. + + Set calendar to 'proleptic_gregorian' to avoid + concatenation issues between historical and + scenario runs. + + Fix dtype value of coordinates and coordinate bounds. + + Parameters + ---------- + cubes : iris.cube.CubeList + Input cubes. + + Returns + ------- + iris.cube.CubeList + """ + for cube in cubes: + time_unit = cube.coord('time').units + if time_unit.calendar == 'standard': + new_unit = time_unit.change_calendar('proleptic_gregorian') + cube.coord('time').units = new_unit + for coord in cube.coords(): + if coord.dtype in ['>f8', '>f4']: + coord.points = coord.core_points().astype( + np.float64, casting='same_kind') + if coord.bounds is not None: + coord.bounds = coord.core_bounds().astype( + np.float64, casting='same_kind') + return cubes + + +class AllVars(Fix): + """General CORDEX grid fix.""" + + def _check_grid_differences(self, old_coord, new_coord): + """Check differences between coords.""" + diff = np.max(np.abs(old_coord.points - new_coord.points)) + logger.debug( + "Maximum difference between original %s" + "points and standard %s domain points " + "for dataset %s and driver %s is: %s.", new_coord.var_name, + self.extra_facets['domain'], self.extra_facets['dataset'], + self.extra_facets['driver'], str(diff)) + + if diff > 10e-4: + raise RecipeError( + "Differences between the original grid and the " + f"standardised grid are above 10e-4 {new_coord.units}.") + + def _fix_rotated_coords(self, cube, domain, domain_info): + """Fix rotated coordinates.""" + for dim_coord in ['rlat', 'rlon']: + old_coord = cube.coord(domain[dim_coord].standard_name) + old_coord_dims = old_coord.cube_dims(cube) + points = domain[dim_coord].data + coord_system = iris.coord_systems.RotatedGeogCS( + grid_north_pole_latitude=domain_info['pollat'], + grid_north_pole_longitude=domain_info['pollon']) + new_coord = iris.coords.DimCoord( + points, + var_name=dim_coord, + standard_name=domain[dim_coord].standard_name, + long_name=domain[dim_coord].long_name, + units=Unit('degrees'), + coord_system=coord_system, + ) + self._check_grid_differences(old_coord, new_coord) + new_coord.guess_bounds() + cube.remove_coord(old_coord) + cube.add_dim_coord(new_coord, old_coord_dims) + + def _fix_geographical_coords(self, cube, domain): + """Fix geographical coordinates.""" + for aux_coord in ['lat', 'lon']: + old_coord = cube.coord(domain[aux_coord].standard_name) + cube.remove_coord(old_coord) + points = domain[aux_coord].data + bounds = domain[f'{aux_coord}_vertices'].data + new_coord = iris.coords.AuxCoord( + points, + var_name=aux_coord, + standard_name=domain[aux_coord].standard_name, + long_name=domain[aux_coord].long_name, + units=Unit(domain[aux_coord].units), + bounds=bounds, + ) + self._check_grid_differences(old_coord, new_coord) + aux_coord_dims = (cube.coord(var_name='rlat').cube_dims(cube) + + cube.coord(var_name='rlon').cube_dims(cube)) + cube.add_aux_coord(new_coord, aux_coord_dims) + + def fix_metadata(self, cubes): + """Fix CORDEX rotated grids. + + Set rotated and geographical coordinates to the + values given by each domain specification. + + The domain specifications are retrieved from the + py-cordex package. + + Parameters + ---------- + cubes : iris.cube.CubeList + Input cubes. + + Returns + ------- + iris.cube.CubeList + """ + data_domain = self.extra_facets['domain'] + domain = _get_domain(data_domain) + domain_info = _get_domain_info(data_domain) + for cube in cubes: + coord_system = cube.coord_system() + if isinstance(coord_system, RotatedGeogCS): + self._fix_rotated_coords(cube, domain, domain_info) + self._fix_geographical_coords(cube, domain) + elif isinstance(coord_system, LambertConformal): + logger.warning( + "Support for CORDEX datasets in a Lambert Conformal " + "coordinate system is ongoing. Certain preprocessor " + "functions may fail.") + else: + raise RecipeError( + f"Coordinate system {coord_system.grid_mapping_name} " + "not supported in CORDEX datasets. Must be " + "rotated_latitude_longitude or lambert_conformal_conic.") + + return cubes diff --git a/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/__init__.py b/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/__init__.py new file mode 100644 index 0000000000..093969370f --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/__init__.py @@ -0,0 +1 @@ +"""Fixes for CORDEX data.""" diff --git a/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/clmcom_cclm4_8_17.py b/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/clmcom_cclm4_8_17.py new file mode 100644 index 0000000000..883e38cecd --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/clmcom_cclm4_8_17.py @@ -0,0 +1,5 @@ +"""Fixes for rcm CLMcom-CCLM4-8-17 driven by ICHEC-EC-EARTH.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + CLMcomCCLM4817 as BaseFix) + +AllVars = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/gerics_remo2015.py b/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/gerics_remo2015.py new file mode 100644 index 0000000000..2f6b9a4d62 --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/gerics_remo2015.py @@ -0,0 +1,5 @@ +"""Fixes for rcm GERICS-REMO2015 driven by ICHEC-EC-EARTH.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + TimeLongName as BaseFix) + +Pr = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/knmi_racmo22e.py b/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/knmi_racmo22e.py new file mode 100644 index 0000000000..96fd620afe --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/knmi_racmo22e.py @@ -0,0 +1,5 @@ +"""Fixes for rcm KNMI-RACMO22E driven by ICHEC-EC-EARTH.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + TimeLongName as BaseFix) + +Pr = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/mohc_hadrem3_ga7_05.py b/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/mohc_hadrem3_ga7_05.py new file mode 100644 index 0000000000..15b643aa36 --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/mohc_hadrem3_ga7_05.py @@ -0,0 +1,7 @@ +"""Fixes for rcm MOHC-HadREM3-GA7-05 driven by ICHEC-EC-EARTH.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + MOHCHadREM3GA705 as BaseFix) + +Tas = BaseFix + +Pr = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/smhi_rca4.py b/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/smhi_rca4.py new file mode 100644 index 0000000000..de1d8d6536 --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/ichec_ec_earth/smhi_rca4.py @@ -0,0 +1,7 @@ +"""Fixes for rcm SMHI-RCA4 driven by ICHEC-EC-EARTH.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + TimeLongName as BaseFix) + +Pr = BaseFix + +Tas = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/miroc_miroc5/__init__.py b/esmvalcore/cmor/_fixes/cordex/miroc_miroc5/__init__.py new file mode 100644 index 0000000000..093969370f --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/miroc_miroc5/__init__.py @@ -0,0 +1 @@ +"""Fixes for CORDEX data.""" diff --git a/esmvalcore/cmor/_fixes/cordex/miroc_miroc5/clmcom_cclm4_8_17.py b/esmvalcore/cmor/_fixes/cordex/miroc_miroc5/clmcom_cclm4_8_17.py new file mode 100644 index 0000000000..51d645f591 --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/miroc_miroc5/clmcom_cclm4_8_17.py @@ -0,0 +1,5 @@ +"""Fixes for rcm CLMcom-CCLM4-8-17 driven by MIROC-MIROC5.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + CLMcomCCLM4817 as BaseFix) + +AllVars = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/miroc_miroc5/gerics_remo2015.py b/esmvalcore/cmor/_fixes/cordex/miroc_miroc5/gerics_remo2015.py new file mode 100644 index 0000000000..9f75b595f9 --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/miroc_miroc5/gerics_remo2015.py @@ -0,0 +1,5 @@ +"""Fixes for rcm GERICS-REMO2015 driven by MIROC-MIROC5.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + TimeLongName as BaseFix) + +Pr = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/miroc_miroc5/uhoh_wrf361h.py b/esmvalcore/cmor/_fixes/cordex/miroc_miroc5/uhoh_wrf361h.py new file mode 100644 index 0000000000..3767c653ea --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/miroc_miroc5/uhoh_wrf361h.py @@ -0,0 +1,34 @@ +"""Fixes for rcm UHOH-WRF361H driven by MIROC-MIROC5.""" +import iris +from esmvalcore.cmor.fix import Fix + + +class Tas(Fix): + """Fixes for tas.""" + + def fix_metadata(self, cubes): + """Fix tas coordinates. + + Set height as an auxiliary coordinate instead + of as a dimensional coordinate. + + Parameters + ---------- + cubes : iris.cube.CubeList + Input cubes. + + Returns + ------- + iris.cube.CubeList + + """ + fixed_cubes = iris.cube.CubeList() + for cube in cubes: + height = cube.coord('height') + if isinstance(height, iris.coords.DimCoord): + iris.util.demote_dim_coord_to_aux_coord( + cube, + height + ) + fixed_cubes.append(iris.util.squeeze(cube)) + return fixed_cubes diff --git a/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/__init__.py b/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/__init__.py new file mode 100644 index 0000000000..093969370f --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/__init__.py @@ -0,0 +1 @@ +"""Fixes for CORDEX data.""" diff --git a/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/dmi_hirham5.py b/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/dmi_hirham5.py new file mode 100644 index 0000000000..fbf5633d13 --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/dmi_hirham5.py @@ -0,0 +1,25 @@ +"""Fixes for rcm DMI-HIRHAM driven by MOHC-HadGEM2.""" +from esmvalcore.cmor.fix import Fix + + +class Pr(Fix): + """Fixes for pr.""" + + def fix_metadata(self, cubes): + """Remove latitude and longitude attributes. + + Parameters + ---------- + cubes : iris.cube.CubeList + Input cubes. + + Returns + ------- + iris.cube.CubeList + + """ + for cube in cubes: + cube.coord('latitude').attributes = {} + cube.coord('longitude').attributes = {} + + return cubes diff --git a/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/gerics_remo2015.py b/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/gerics_remo2015.py new file mode 100644 index 0000000000..85bb2e352c --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/gerics_remo2015.py @@ -0,0 +1,7 @@ +"""Fixes for rcm GERICS-REMO2015 driven by MOHC-HadGEM2.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + TimeLongName as BaseFix) + +Pr = BaseFix + +Tas = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/mohc_hadrem3_ga7_05.py b/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/mohc_hadrem3_ga7_05.py new file mode 100644 index 0000000000..004cbc412f --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/mohc_hadrem3_ga7_05.py @@ -0,0 +1,7 @@ +"""Fixes for rcm MOHC-HadREM3-GA7-05 driven by MOHC-HadGEM2-ES.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + MOHCHadREM3GA705 as BaseFix) + +Tas = BaseFix + +Pr = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/smhi_rca4.py b/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/smhi_rca4.py new file mode 100644 index 0000000000..543df3670a --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/mohc_hadgem2_es/smhi_rca4.py @@ -0,0 +1,7 @@ +"""Fixes for rcm SMHI-RCA4 driven by MOHC-HadGEM2-ES.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + TimeLongName as BaseFix) + +Pr = BaseFix + +Tas = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/mpi_m_mpi_esm_lr/__init__.py b/esmvalcore/cmor/_fixes/cordex/mpi_m_mpi_esm_lr/__init__.py new file mode 100644 index 0000000000..093969370f --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/mpi_m_mpi_esm_lr/__init__.py @@ -0,0 +1 @@ +"""Fixes for CORDEX data.""" diff --git a/esmvalcore/cmor/_fixes/cordex/mpi_m_mpi_esm_lr/ictp_regcm4_6.py b/esmvalcore/cmor/_fixes/cordex/mpi_m_mpi_esm_lr/ictp_regcm4_6.py new file mode 100644 index 0000000000..9a92874b03 --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/mpi_m_mpi_esm_lr/ictp_regcm4_6.py @@ -0,0 +1,7 @@ +"""Fixes for rcm ICTP-RegCM4-6 driven by MPI-M-MPI-ESM-LR.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + TimeLongName as BaseFix) + +Pr = BaseFix + +Tas = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/mpi_m_mpi_esm_lr/knmi_racmo22e.py b/esmvalcore/cmor/_fixes/cordex/mpi_m_mpi_esm_lr/knmi_racmo22e.py new file mode 100644 index 0000000000..ab944112b3 --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/mpi_m_mpi_esm_lr/knmi_racmo22e.py @@ -0,0 +1,5 @@ +"""Fixes for rcm KNMI-RACMO22E driven by MPI-M-MPI-ESM-LR.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + TimeLongName as BaseFix) + +Pr = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/mpi_m_mpi_esm_lr/mohc_hadrem3_ga7_05.py b/esmvalcore/cmor/_fixes/cordex/mpi_m_mpi_esm_lr/mohc_hadrem3_ga7_05.py new file mode 100644 index 0000000000..0abfc3dff1 --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/mpi_m_mpi_esm_lr/mohc_hadrem3_ga7_05.py @@ -0,0 +1,7 @@ +"""Fixes for rcm MOHC-HadREM3-GA7-05 driven by MPI-M-MPI-ESM-LR.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + MOHCHadREM3GA705 as BaseFix) + +Tas = BaseFix + +Pr = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/__init__.py b/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/__init__.py new file mode 100644 index 0000000000..093969370f --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/__init__.py @@ -0,0 +1 @@ +"""Fixes for CORDEX data.""" diff --git a/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/gerics_remo2015.py b/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/gerics_remo2015.py new file mode 100644 index 0000000000..b1d118cd38 --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/gerics_remo2015.py @@ -0,0 +1,5 @@ +"""Fixes for rcm GERICS-REMO2015 driven by NCC-NorESM1-M.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + TimeLongName as BaseFix) + +Pr = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/knmi_racmo22e.py b/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/knmi_racmo22e.py new file mode 100644 index 0000000000..cc2440a2b8 --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/knmi_racmo22e.py @@ -0,0 +1,5 @@ +"""Fixes for rcm KNMI-RACMO22E driven by NCC-NorESM1-M.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + TimeLongName as BaseFix) + +Pr = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/mohc_hadrem3_ga7_05.py b/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/mohc_hadrem3_ga7_05.py new file mode 100644 index 0000000000..1aa2d11b1b --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/mohc_hadrem3_ga7_05.py @@ -0,0 +1,7 @@ +"""Fixes for rcm HadREM3-GA7-05 driven by NCC-NorESM1-M.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + MOHCHadREM3GA705 as BaseFix) + +Tas = BaseFix + +Pr = BaseFix diff --git a/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/smhi_rca4.py b/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/smhi_rca4.py new file mode 100644 index 0000000000..5c9059f74a --- /dev/null +++ b/esmvalcore/cmor/_fixes/cordex/ncc_noresm1_m/smhi_rca4.py @@ -0,0 +1,7 @@ +"""Fixes for rcm SMHI-RCA4 driven by NCC-NorESM1-M.""" +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + TimeLongName as BaseFix) + +Pr = BaseFix + +Tas = BaseFix diff --git a/esmvalcore/cmor/_fixes/fix.py b/esmvalcore/cmor/_fixes/fix.py index be94c4a774..f7239a7a33 100644 --- a/esmvalcore/cmor/_fixes/fix.py +++ b/esmvalcore/cmor/_fixes/fix.py @@ -157,10 +157,27 @@ def get_fixes(project, dataset, mip, short_name, extra_facets=None): extra_facets = {} fixes = [] - try: - fixes_module = importlib.import_module( - 'esmvalcore.cmor._fixes.{0}.{1}'.format(project, dataset)) + fixes_modules = [] + if project == 'cordex': + driver = extra_facets['driver'].replace('-', '_').lower() + extra_facets['dataset'] = dataset + try: + fixes_modules.append(importlib.import_module( + f'esmvalcore.cmor._fixes.{project}.{driver}.{dataset}' + )) + except ImportError: + pass + fixes_modules.append(importlib.import_module( + 'esmvalcore.cmor._fixes.cordex.cordex_fixes')) + else: + try: + fixes_modules.append(importlib.import_module( + f'esmvalcore.cmor._fixes.{project}.{dataset}')) + except ImportError: + pass + + for fixes_module in fixes_modules: classes = inspect.getmembers(fixes_module, inspect.isclass) classes = dict((name.lower(), value) for name, value in classes) for fix_name in (short_name, mip.lower(), 'allvars'): @@ -168,8 +185,7 @@ def get_fixes(project, dataset, mip, short_name, extra_facets=None): fixes.append(classes[fix_name](vardef, extra_facets)) except KeyError: pass - except ImportError: - pass + return fixes @staticmethod diff --git a/esmvalcore/cmor/check.py b/esmvalcore/cmor/check.py index 5712ecc22b..328e9ce374 100644 --- a/esmvalcore/cmor/check.py +++ b/esmvalcore/cmor/check.py @@ -1081,6 +1081,10 @@ def _get_cmor_checker(table, table, ', '.join(CMOR_TABLES))) cmor_table = CMOR_TABLES[table] + if table == 'CORDEX' and mip.endswith('hr'): + # CORDEX X-hourly tables define the mip + # as ending in 'h' instead of 'hr'. + mip = mip.replace('hr', 'h') var_info = cmor_table.get_variable(mip, short_name) if var_info is None: var_info = CMOR_TABLES['custom'].get_variable(mip, short_name) diff --git a/setup.py b/setup.py index 49daac53ca..698011b115 100755 --- a/setup.py +++ b/setup.py @@ -50,6 +50,7 @@ 'pillow', 'prov', 'psutil', + 'py-cordex', 'pybtex', 'pyyaml', 'requests', diff --git a/tests/integration/cmor/_fixes/cordex/__init__.py b/tests/integration/cmor/_fixes/cordex/__init__.py new file mode 100644 index 0000000000..7aed60134a --- /dev/null +++ b/tests/integration/cmor/_fixes/cordex/__init__.py @@ -0,0 +1 @@ +"""Integration tests for CORDEX fixes.""" diff --git a/tests/integration/cmor/_fixes/cordex/test_cnrm_cerfacs_cnrm_cm5.py b/tests/integration/cmor/_fixes/cordex/test_cnrm_cerfacs_cnrm_cm5.py new file mode 100644 index 0000000000..2b29c7185c --- /dev/null +++ b/tests/integration/cmor/_fixes/cordex/test_cnrm_cerfacs_cnrm_cm5.py @@ -0,0 +1,61 @@ +"""Tests for the fixes for driver CNRM-CERFACS-CNRM-CM5.""" +import iris +import pytest + +from esmvalcore.cmor._fixes.cordex.cnrm_cerfacs_cnrm_cm5 import cnrm_aladin63 +from esmvalcore.cmor.fix import Fix + + +@pytest.fixture +def cubes(): + correct_time_coord = iris.coords.DimCoord([0.0], + var_name='time', + standard_name='time', + long_name='time') + correct_height_coord = iris.coords.AuxCoord([2.0], + var_name='height') + wrong_height_coord = iris.coords.AuxCoord([10.0], + var_name='height') + correct_cube = iris.cube.Cube( + [10.0], + var_name='tas', + dim_coords_and_dims=[(correct_time_coord, 0)], + aux_coords_and_dims=[(correct_height_coord, ())] + ) + wrong_cube = iris.cube.Cube( + [10.0], + var_name='tas', + dim_coords_and_dims=[(correct_time_coord, 0)], + aux_coords_and_dims=[(wrong_height_coord, ())] + ) + return iris.cube.CubeList([correct_cube, wrong_cube]) + + +@pytest.mark.parametrize('short_name', ['pr', 'tas']) +def test_get_mohc_hadrem3ga705_fix(short_name): + fix = Fix.get_fixes( + 'CORDEX', + 'MOHC-HadREM3-GA7-05', + 'Amon', + short_name, + extra_facets={'driver': 'CNRM-CERFACS-CNRM-CM5'}) + assert isinstance(fix[0], Fix) + + +@pytest.mark.parametrize('short_name', ['pr', 'tas']) +def test_get_cnrm_aladin63_fix(short_name): + fix = Fix.get_fixes( + 'CORDEX', + 'CNRM-ALADIN63', + 'Amon', + short_name, + extra_facets={'driver': 'CNRM-CERFACS-CNRM-CM5'}) + assert isinstance(fix[0], Fix) + + +def test_cnrm_aladin63_height_fix(cubes): + fix = cnrm_aladin63.Tas(None) + out_cubes = fix.fix_metadata(cubes) + assert cubes is out_cubes + for cube in out_cubes: + assert cube.coord('height').points == 2.0 diff --git a/tests/integration/cmor/_fixes/cordex/test_cordex_fixes.py b/tests/integration/cmor/_fixes/cordex/test_cordex_fixes.py new file mode 100644 index 0000000000..a831d37b32 --- /dev/null +++ b/tests/integration/cmor/_fixes/cordex/test_cordex_fixes.py @@ -0,0 +1,229 @@ +"""Tests for general CORDEX fixes.""" +import cordex as cx +import iris +import numpy as np +import pytest +from cf_units import Unit + +from esmvalcore.cmor._fixes.cordex.cordex_fixes import ( + AllVars, + CLMcomCCLM4817, + MOHCHadREM3GA705, + TimeLongName, +) +from esmvalcore.exceptions import RecipeError + + +@pytest.fixture +def cubes(): + correct_time_coord = iris.coords.DimCoord([0.0], + var_name='time', + standard_name='time', + long_name='time') + wrong_time_coord = iris.coords.DimCoord([0.0], + var_name='time', + standard_name='time', + long_name='wrong') + correct_lat_coord = iris.coords.DimCoord([0.0, 1.0], + var_name='lat', + standard_name='latitude', + long_name='latitude') + wrong_lat_coord = iris.coords.DimCoord([0.0, 1.0], + var_name='latitudeCoord', + standard_name='latitude', + long_name='latitude') + correct_lon_coord = iris.coords.DimCoord([0.0], + var_name='lon', + standard_name='longitude', + long_name='longitude') + wrong_lon_coord = iris.coords.DimCoord([0.0], + var_name='longitudeCoord', + standard_name='longitude', + long_name='longitude') + correct_cube = iris.cube.Cube( + [[[10.0], [10.0]]], + var_name='tas', + dim_coords_and_dims=[ + (correct_time_coord, 0), + (correct_lat_coord, 1), + (correct_lon_coord, 2)], + ) + wrong_cube = iris.cube.Cube( + [[[10.0], [10.0]]], + var_name='tas', + dim_coords_and_dims=[ + (wrong_time_coord, 0), + (wrong_lat_coord, 1), + (wrong_lon_coord, 2)], + ) + return iris.cube.CubeList([correct_cube, wrong_cube]) + + +@pytest.fixture +def cordex_cubes(): + coord_system = iris.coord_systems.RotatedGeogCS( + grid_north_pole_latitude=39.25, + grid_north_pole_longitude=-162, + ) + time = iris.coords.DimCoord(np.arange(0, 3), + var_name='time', + standard_name='time') + + rlat = iris.coords.DimCoord(np.arange(0, 412), + var_name='rlat', + standard_name='grid_latitude', + coord_system=coord_system, + ) + rlon = iris.coords.DimCoord(np.arange(0, 424), + var_name='rlon', + standard_name='grid_longitude', + coord_system=coord_system, + ) + lat = iris.coords.AuxCoord(np.ones((412, 424)), + var_name='lat', + standard_name='latitude') + lon = iris.coords.AuxCoord(np.ones((412, 424)), + var_name='lon', + standard_name='longitude') + + cube = iris.cube.Cube( + np.ones((3, 412, 424)), + var_name='tas', + dim_coords_and_dims=[ + (time, 0), + (rlat, 1), + (rlon, 2)], + aux_coords_and_dims=[ + (lat, (1, 2)), + (lon, (1, 2)) + ] + + ) + return iris.cube.CubeList([cube]) + + +@pytest.mark.parametrize( + 'coord, var_name, long_name', + [ + ('time', 'time', 'time'), + ('latitude', 'lat', 'latitude'), + ('longitude', 'lon', 'longitude'), + ]) +def test_mohchadrem3ga705_fix_metadata(cubes, coord, var_name, long_name): + fix = MOHCHadREM3GA705(None) + out_cubes = fix.fix_metadata(cubes) + assert cubes is out_cubes + for cube in out_cubes: + assert cube.coord(standard_name=coord).var_name == var_name + assert cube.coord(standard_name=coord).long_name == long_name + + +def test_timelongname_fix_metadata(cubes): + fix = TimeLongName(None) + out_cubes = fix.fix_metadata(cubes) + assert cubes is out_cubes + for cube in out_cubes: + assert cube.coord('time').long_name == 'time' + + +def test_clmcomcclm4817_fix_metadata(cubes): + cubes[0].coord('time').units = Unit( + 'days since 1850-1-1 00:00:00', + calendar='proleptic_gregorian') + cubes[1].coord('time').units = Unit( + 'days since 1850-1-1 00:00:00', + calendar='standard') + for coord in cubes[1].coords(): + coord.points = coord.core_points().astype( + '>f8', casting='same_kind') + lat = cubes[1].coord('latitude') + lat.guess_bounds() + lat.bounds = lat.core_bounds().astype( + '>f4', casting='same_kind') + + fix = CLMcomCCLM4817(None) + out_cubes = fix.fix_metadata(cubes) + assert cubes is out_cubes + for cube in out_cubes: + assert cube.coord('time').units == Unit( + 'days since 1850-1-1 00:00:00', + calendar='proleptic_gregorian') + for coord in cube.coords(): + assert coord.points.dtype == np.float64 + + +def test_rotated_grid_fix(cordex_cubes): + fix = AllVars( + vardef=None, + extra_facets={ + 'domain': 'EUR-11', + 'dataset': 'DATASET', + 'driver': 'DRIVER' + } + ) + domain = cx.cordex_domain('EUR-11', add_vertices=True) + for cube in cordex_cubes: + for coord in ['rlat', 'rlon', 'lat', 'lon']: + cube_coord = cube.coord(var_name=coord) + cube_coord.points = domain[coord].data + 1e-6 + out_cubes = fix.fix_metadata(cordex_cubes) + assert cordex_cubes is out_cubes + for out_cube in out_cubes: + for coord in ['rlat', 'rlon', 'lat', 'lon']: + cube_coord = out_cube.coord(var_name=coord) + domain_coord = domain[coord].data + np.testing.assert_array_equal( + cube_coord.points, domain_coord) + + +def test_rotated_grid_fix_error(cordex_cubes): + fix = AllVars( + vardef=None, + extra_facets={ + 'domain': 'EUR-11', + 'dataset': 'DATASET', + 'driver': 'DRIVER' + } + ) + msg = ("Differences between the original grid and the " + "standardised grid are above 10e-4 degrees.") + with pytest.raises(RecipeError) as exc: + fix.fix_metadata(cordex_cubes) + assert msg == exc.value.message + + +def test_lambert_grid_warning(cubes, caplog): + fix = AllVars( + vardef=None, + extra_facets={ + 'domain': 'EUR-11', + 'dataset': 'DATASET', + 'driver': 'DRIVER' + } + ) + for cube in cubes: + cube.coord_system = iris.coord_systems.LambertConformal + fix.fix_metadata(cubes) + msg = ("Support for CORDEX datasets in a Lambert Conformal " + "coordinate system is ongoing. Certain preprocessor " + "functions may fail.") + assert msg in caplog.text + + +def test_wrong_coord_system(cubes): + fix = AllVars( + vardef=None, + extra_facets={ + 'domain': 'EUR-11', + 'dataset': 'DATASET', + 'driver': 'DRIVER' + } + ) + for cube in cubes: + cube.coord_system = iris.coord_systems.AlbersEqualArea + msg = ("Coordinate system albers_conical_equal_area not supported in " + "CORDEX datasets. Must be rotated_latitude_longitude " + "or lambert_conformal_conic.") + with pytest.raises(RecipeError) as exc: + fix.fix_metadata(cubes) + assert msg == exc.value.message diff --git a/tests/integration/cmor/_fixes/cordex/test_ichec_ec_earth.py b/tests/integration/cmor/_fixes/cordex/test_ichec_ec_earth.py new file mode 100644 index 0000000000..09c345ba01 --- /dev/null +++ b/tests/integration/cmor/_fixes/cordex/test_ichec_ec_earth.py @@ -0,0 +1,46 @@ +"""Tests for the fixes for driver ICHEC-EC-Earth.""" +import pytest + +from esmvalcore.cmor.fix import Fix + + +def test_get_gerics_remo2015_fix(): + fix = Fix.get_fixes( + 'CORDEX', + 'GERICS-REMO2015', + 'Amon', + 'pr', + extra_facets={'driver': 'ICHEC-EC-Earth'}) + assert isinstance(fix[0], Fix) + + +def test_get_knmi_racmo22e_fix(): + fix = Fix.get_fixes( + 'CORDEX', + 'KNMI-RACMO22E', + 'Amon', + 'pr', + extra_facets={'driver': 'ICHEC-EC-Earth'}) + assert isinstance(fix[0], Fix) + + +@pytest.mark.parametrize('short_name', ['pr', 'tas']) +def test_get_mohc_hadrem3ga705_fix(short_name): + fix = Fix.get_fixes( + 'CORDEX', + 'MOHC-HadREM3-GA7-05', + 'Amon', + short_name, + extra_facets={'driver': 'ICHEC-EC-Earth'}) + assert isinstance(fix[0], Fix) + + +@pytest.mark.parametrize('short_name', ['pr', 'tas']) +def test_get_smhi_rca4_fix(short_name): + fix = Fix.get_fixes( + 'CORDEX', + 'SMHI-RCA4', + 'Amon', + short_name, + extra_facets={'driver': 'ICHEC-EC-Earth'}) + assert isinstance(fix[0], Fix) diff --git a/tests/integration/cmor/_fixes/cordex/test_miroc_miroc5.py b/tests/integration/cmor/_fixes/cordex/test_miroc_miroc5.py new file mode 100644 index 0000000000..a177498930 --- /dev/null +++ b/tests/integration/cmor/_fixes/cordex/test_miroc_miroc5.py @@ -0,0 +1,62 @@ +"""Tests for the fixes of AWI-CM-1-1-MR.""" +import pytest +import iris + +from esmvalcore.cmor._fixes.cordex.miroc_miroc5 import uhoh_wrf361h +from esmvalcore.cmor.fix import Fix + + +@pytest.fixture +def cubes(): + correct_time_coord = iris.coords.DimCoord([0.0, 1.0], + var_name='time', + standard_name='time', + long_name='time') + wrong_height_coord = iris.coords.DimCoord([2.0], + var_name='height') + wrong_cube = iris.cube.Cube( + [[10.0], [10.0]], + var_name='tas', + dim_coords_and_dims=[ + (correct_time_coord, 0), + (wrong_height_coord, 1)], + ) + return iris.cube.CubeList([wrong_cube]) + + +@pytest.mark.parametrize('short_name', ['pr', 'tas']) +def test_get_clmcom_cclm4_8_17fix(short_name): + fix = Fix.get_fixes( + 'CORDEX', + 'CLMCOM-CCLM4-8-17', + 'Amon', + short_name, + extra_facets={'driver': 'MIROC-MIROC5'}) + assert isinstance(fix[0], Fix) + + +def test_get_gerics_remo2015_fix(): + fix = Fix.get_fixes( + 'CORDEX', + 'GERICS-REMO2015', + 'Amon', + 'pr', + extra_facets={'driver': 'MIROC-MIROC5'}) + assert isinstance(fix[0], Fix) + + +def test_get_uhoh_wrf361h_fix(): + fix = Fix.get_fixes( + 'CORDEX', + 'UHOH-WRF361H', + 'Amon', + 'tas', + extra_facets={'driver': 'MIROC-MIROC5'}) + assert isinstance(fix[0], Fix) + + +def test_uhoh_wrf361h_height_fix(cubes): + fix = uhoh_wrf361h.Tas(None) + out_cubes = fix.fix_metadata(cubes) + for cube in out_cubes: + assert cube.ndim == 1 diff --git a/tests/integration/cmor/_fixes/cordex/test_mohc_hadgem2_es.py b/tests/integration/cmor/_fixes/cordex/test_mohc_hadgem2_es.py new file mode 100644 index 0000000000..408fe4d5a6 --- /dev/null +++ b/tests/integration/cmor/_fixes/cordex/test_mohc_hadgem2_es.py @@ -0,0 +1,105 @@ +"""Tests for the fixes for driver MOHC-HadGEM2-ES.""" +import iris +import pytest + +from esmvalcore.cmor._fixes.cordex.mohc_hadgem2_es import dmi_hirham5 +from esmvalcore.cmor.fix import Fix + + +@pytest.fixture +def cubes(): + correct_time_coord = iris.coords.DimCoord([0.0], + var_name='time', + standard_name='time', + long_name='time') + wrong_time_coord = iris.coords.DimCoord([0.0], + var_name='time', + standard_name='time', + long_name='wrong') + correct_lat_coord = iris.coords.DimCoord([0.0], + var_name='lat', + standard_name='latitude', + long_name='latitude') + wrong_lat_coord = iris.coords.DimCoord([0.0], + var_name='latitudeCoord', + standard_name='latitude', + long_name='latitude', + attributes={'wrong': 'attr'}) + correct_lon_coord = iris.coords.DimCoord([0.0], + var_name='lon', + standard_name='longitude', + long_name='longitude') + wrong_lon_coord = iris.coords.DimCoord([0.0], + var_name='longitudeCoord', + standard_name='longitude', + long_name='longitude', + attributes={'wrong': 'attr'}) + correct_cube = iris.cube.Cube( + [[[10.0]]], + var_name='tas', + dim_coords_and_dims=[ + (correct_time_coord, 0), + (correct_lat_coord, 1), + (correct_lon_coord, 2)], + ) + wrong_cube = iris.cube.Cube( + [[[10.0]]], + var_name='tas', + dim_coords_and_dims=[ + (wrong_time_coord, 0), + (wrong_lat_coord, 1), + (wrong_lon_coord, 2)], + ) + return iris.cube.CubeList([correct_cube, wrong_cube]) + + +def test_get_dmi_hirham5_fix(): + fix = Fix.get_fixes( + 'CORDEX', + 'DMI-HIRHAM5', + 'Amon', + 'pr', + extra_facets={'driver': 'MOHC-HadGEM2-ES'}) + assert isinstance(fix[0], Fix) + + +@pytest.mark.parametrize('short_name', ['pr', 'tas']) +def test_get_gerics_remo2015_fix(short_name): + fix = Fix.get_fixes( + 'CORDEX', + 'GERICS-REMO2015', + 'Amon', + short_name, + extra_facets={'driver': 'MOHC-HadGEM2-ES'}) + assert isinstance(fix[0], Fix) + + +@pytest.mark.parametrize('short_name', ['pr', 'tas']) +def test_get_mohc_hadrem3ga705_fix(short_name): + fix = Fix.get_fixes( + 'CORDEX', + 'MOHC-HadREM3-GA7-05', + 'Amon', + short_name, + extra_facets={'driver': 'MOHC-HadGEM2-ES'}) + assert isinstance(fix[0], Fix) + + +@pytest.mark.parametrize('short_name', ['pr', 'tas']) +def test_get_smhi_rca4_fix(short_name): + fix = Fix.get_fixes( + 'CORDEX', + 'SMHI-RCA4', + 'Amon', + short_name, + extra_facets={'driver': 'MOHC-HadGEM2-ES'}) + assert isinstance(fix[0], Fix) + + +def test_dmi_hirham5_fix(cubes): + fix = dmi_hirham5.Pr(None) + out_cubes = fix.fix_metadata(cubes) + assert cubes is out_cubes + for cube in out_cubes: + assert cube.coord('latitude').attributes == {} + assert cube.coord('longitude').attributes == {} diff --git a/tests/integration/cmor/_fixes/cordex/test_mpi_m_mpi_esm_lr.py b/tests/integration/cmor/_fixes/cordex/test_mpi_m_mpi_esm_lr.py new file mode 100644 index 0000000000..30c813297f --- /dev/null +++ b/tests/integration/cmor/_fixes/cordex/test_mpi_m_mpi_esm_lr.py @@ -0,0 +1,36 @@ +"""Tests for the fixes of driver MPI-M-MPI-ESM-LR.""" +import pytest + +from esmvalcore.cmor.fix import Fix + + +@pytest.mark.parametrize('short_name', ['pr', 'tas']) +def test_get_ictp_regcm4_6_fix(short_name): + fix = Fix.get_fixes( + 'CORDEX', + 'ICTP-REGCM4-6', + 'Amon', + short_name, + extra_facets={'driver': 'MPI-M-MPI-ESM-LR'}) + assert isinstance(fix[0], Fix) + + +def test_get_knmi_racmo22e_fix(): + fix = Fix.get_fixes( + 'CORDEX', + 'KNMI-RACMO22E', + 'Amon', + 'pr', + extra_facets={'driver': 'MPI-M-MPI-ESM-LR'}) + assert isinstance(fix[0], Fix) + + +@pytest.mark.parametrize('short_name', ['pr', 'tas']) +def test_get_mohc_hadrem3ga705_fix(short_name): + fix = Fix.get_fixes( + 'CORDEX', + 'MOHC-HadREM3-GA7-05', + 'Amon', + short_name, + extra_facets={'driver': 'MPI-M-MPI-ESM-LR'}) + assert isinstance(fix[0], Fix) diff --git a/tests/integration/cmor/_fixes/cordex/test_ncc_noresm1_m.py b/tests/integration/cmor/_fixes/cordex/test_ncc_noresm1_m.py new file mode 100644 index 0000000000..e071c8d7ea --- /dev/null +++ b/tests/integration/cmor/_fixes/cordex/test_ncc_noresm1_m.py @@ -0,0 +1,46 @@ +"""Tests for the fixes of driver NCC-NorESM1-M.""" +import pytest + +from esmvalcore.cmor.fix import Fix + + +def test_get_gerics_remo2015_fix(): + fix = Fix.get_fixes( + 'CORDEX', + 'GERICS-REMO2015', + 'Amon', + 'pr', + extra_facets={'driver': 'NCC-NorESM1-M'}) + assert isinstance(fix[0], Fix) + + +def test_get_knmi_racmo22e_fix(): + fix = Fix.get_fixes( + 'CORDEX', + 'KNMI-RACMO22E', + 'Amon', + 'pr', + extra_facets={'driver': 'NCC-NorESM1-M'}) + assert isinstance(fix[0], Fix) + + +@pytest.mark.parametrize('short_name', ['pr', 'tas']) +def test_get_mohc_hadrem3ga705_fix(short_name): + fix = Fix.get_fixes( + 'CORDEX', + 'MOHC-HadREM3-GA7-05', + 'Amon', + short_name, + extra_facets={'driver': 'NCC-NorESM1-M'}) + assert isinstance(fix[0], Fix) + + +@pytest.mark.parametrize('short_name', ['pr', 'tas']) +def test_get_smhi_rca4_fix(short_name): + fix = Fix.get_fixes( + 'CORDEX', + 'SMHI-RCA4', + 'Amon', + short_name, + extra_facets={'driver': 'NCC-NorESM1-M'}) + assert isinstance(fix[0], Fix) diff --git a/tests/integration/cmor/_fixes/test_fix.py b/tests/integration/cmor/_fixes/test_fix.py index 5ffc4ca868..16629549fe 100644 --- a/tests/integration/cmor/_fixes/test_fix.py +++ b/tests/integration/cmor/_fixes/test_fix.py @@ -12,6 +12,9 @@ from esmvalcore.cmor._fixes.cmip5.canesm2 import FgCo2 from esmvalcore.cmor._fixes.cmip5.cesm1_bgc import Gpp from esmvalcore.cmor._fixes.cmip6.cesm2 import Omon, Tos +from esmvalcore.cmor._fixes.cordex.cnrm_cerfacs_cnrm_cm5.cnrm_aladin63 import ( + Tas) +from esmvalcore.cmor._fixes.cordex.cordex_fixes import AllVars from esmvalcore.cmor.fix import Fix @@ -32,6 +35,26 @@ def test_get_fix_case_insensitive(self): self.assertListEqual( Fix.get_fixes('CMIP5', 'CanESM2', 'Amon', 'fgCo2'), [FgCo2(None)]) + def test_get_fix_cordex(self): + self.assertListEqual( + Fix.get_fixes( + 'CORDEX', + 'CNRM-ALADIN63', + 'Amon', + 'tas', + extra_facets={'driver': 'CNRM-CERFACS-CNRM-CM5'}), + [Tas(None), AllVars(None)]) + + def test_get_grid_fix_cordex(self): + self.assertListEqual( + Fix.get_fixes( + 'CORDEX', + 'CNRM-ALADIN53', + 'Amon', + 'tas', + extra_facets={'driver': 'CNRM-CERFACS-CNRM-CM5'}), + [AllVars(None)]) + def test_get_fixes_with_replace(self): self.assertListEqual(Fix.get_fixes('CMIP5', 'BNU-ESM', 'Amon', 'ch4'), [Ch4(None)]) diff --git a/tests/unit/cmor/test_cmor_check.py b/tests/unit/cmor/test_cmor_check.py index fe965b5981..4e17f9ca99 100644 --- a/tests/unit/cmor/test_cmor_check.py +++ b/tests/unit/cmor/test_cmor_check.py @@ -11,7 +11,9 @@ import numpy as np from cf_units import Unit -from esmvalcore.cmor.check import CheckLevels, CMORCheck, CMORCheckError +from esmvalcore.cmor.check import ( + CheckLevels, CMORCheck, + CMORCheckError, _get_cmor_checker) class VariableInfoMock: @@ -1076,6 +1078,12 @@ def test_no_time_bounds(self): guessed_bounds = self.cube.coord('time').bounds assert guessed_bounds is None + def test_hr_mip_cordex(self): + """Test hourly CORDEX tables are found.""" + checker = _get_cmor_checker('CORDEX', '3hr', 'tas', '3hr') + assert checker(self.cube)._cmor_var.short_name == 'tas' + assert checker(self.cube)._cmor_var.frequency == '3hr' + def _check_fails_on_data(self): checker = CMORCheck(self.cube, self.var_info) checker.check_metadata()