Repository: ESMValGroup/ESMValCore

Issue:
Dataset problem: MCM-UA-1-0 (CMIP6) datasets have wrong coordinate names
**Describe the dataset issue**

- ~~Data file has been changed by you in any way (if you answer yes the issue will be void and closed, we are not
supporting custom-made datasets that present problems, it is your resposability to fix those problems)~~
- Project (CMIP5/CMIP6/OBS/obs4mips/etc): CMIP6
- Full dataset decription (fill out as many as you know, please):
  - dataset: MCM-UA-1-0
  - experiment: piControl, 1pctCO2
  - mip: Amon
  - grid: gn
  - type:
  - version: v20190731
  - tier:
  - variable used: `tas`, `ta` (probably all)
  - data_specs_version: 01.00.28
- Problems encountered (please describe what the actual problems are: e.g. wrong standard name, issue with dimensions):
  - For variable `tas`: `height` coordinate is not correctly marked as coordinate, so `iris` does not assign it correctly as `scalar coordinate`
  - `var_name` of `latitude` is `latitude`, not `lat`
  - `var_name` of `longitude` is `longitude`, not `lon`
  - `parent_time_units` has invalid format (ends with `... (noleap)`)
- Pointer to existing copy of data on ESGF node (it would be very useful if you could provide a physical fullpath to the file(s) that are causing the problem, e.g. on CEDA Jasmin or DKRZ): DKRZ: `/mnt/lustre02/work/ik1017/CMIP6/data/CMIP6/CMIP/UA/MCM-UA-1-0/1pctCO2/r1i1p1f1/Amon/tas/gn/v20190731/tas_Amon_MCM-UA-1-0_1pctCO2_r1i1p1f1_gn_000101-010012.nc`
- Other notes and mentions: I will write a fix for it

Discussion:

Patch:
diff --git a/doc/esmvalcore/preprocessor.rst b/doc/esmvalcore/preprocessor.rst
index 34faaa246..7ee12d139 100644
--- a/doc/esmvalcore/preprocessor.rst
+++ b/doc/esmvalcore/preprocessor.rst
@@ -896,7 +896,7 @@ See also :func:`esmvalcore.preprocessor.anomalies`.
 
 This function aligns the time points of each component dataset so that the Iris
 cubes from different datasets can be subtracted. The operation makes the
-datasets time points common; it also resets the time
+datasets time points common and sets common calendars; it also resets the time
 bounds and auxiliary coordinates to reflect the artifically shifted time
 points. Current implementation for monthly and daily data; the ``frequency`` is
 set automatically from the variable CMOR table unless a custom ``frequency`` is
diff --git a/esmvalcore/cmor/_fixes/cmip6/mcm_ua_1_0.py b/esmvalcore/cmor/_fixes/cmip6/mcm_ua_1_0.py
new file mode 100644
index 000000000..7552a7a8a
--- /dev/null
+++ b/esmvalcore/cmor/_fixes/cmip6/mcm_ua_1_0.py
@@ -0,0 +1,60 @@
+"""Fixes for MCM-UA-1-0 model."""
+import iris
+
+from ..fix import Fix
+from ..shared import add_scalar_height_coord
+
+
+class AllVars(Fix):
+    """Fixes for all variables."""
+
+    def fix_metadata(self, cubes):
+        """Rename ``var_name`` of latitude and longitude.
+
+        Parameters
+        ----------
+        cubes : iris.cube.CubeList
+
+        Returns
+        -------
+        iris.cube.Cube
+
+        """
+        coords_to_change = {
+            'latitude': 'lat',
+            'longitude': 'lon',
+        }
+        for cube in cubes:
+            for (std_name, var_name) in coords_to_change.items():
+                try:
+                    coord = cube.coord(std_name)
+                except iris.exceptions.CoordinateNotFoundError:
+                    pass
+                else:
+                    coord.var_name = var_name
+            time_units = cube.attributes.get('parent_time_units')
+            if time_units is not None:
+                cube.attributes['parent_time_units'] = time_units.replace(
+                    ' (noleap)', '')
+        return cubes
+
+
+class Tas(Fix):
+    """Fixes for tas."""
+
+    def fix_metadata(self, cubes):
+        """
+        Add height (2m) coordinate.
+
+        Parameters
+        ----------
+        cubes : iris.cube.CubeList
+
+        Returns
+        -------
+        iris.cube.Cube
+
+        """
+        cube = self.get_cube_from_list(cubes)
+        add_scalar_height_coord(cube, 2.0)
+        return [cube]
diff --git a/esmvalcore/preprocessor/_time.py b/esmvalcore/preprocessor/_time.py
index 5b8eda710..dc7a058d9 100644
--- a/esmvalcore/preprocessor/_time.py
+++ b/esmvalcore/preprocessor/_time.py
@@ -7,6 +7,7 @@ import datetime
 import logging
 from warnings import filterwarnings
 
+import cf_units
 import dask.array as da
 import iris
 import iris.coord_categorisation
@@ -199,10 +200,8 @@ def daily_statistics(cube, operator='mean'):
     iris.cube.Cube
         Daily statistics cube
     """
-    if not cube.coords('day_of_year'):
-        iris.coord_categorisation.add_day_of_year(cube, 'time')
-    if not cube.coords('year'):
-        iris.coord_categorisation.add_year(cube, 'time')
+    iris.coord_categorisation.add_day_of_year(cube, 'time')
+    iris.coord_categorisation.add_year(cube, 'time')
 
     operator = get_iris_analysis_operation(operator)
     cube = cube.aggregated_by(['day_of_year', 'year'], operator)
@@ -482,11 +481,10 @@ def regrid_time(cube, frequency):
     """
     Align time axis for cubes so they can be subtracted.
 
-    Operations on time units, time points and auxiliary
+    Operations on time units, calendars, time points and auxiliary
     coordinates so that any cube from cubes can be subtracted from any
-    other cube from cubes. Currently this function supports
-    yearly (frequency=yr), monthly (frequency=mon),
-    daily (frequency=day), 6-hourly (frequency=6hr),
+    other cube from cubes. Currently this function supports monthly
+    (frequency=mon), daily (frequency=day), 6-hourly (frequency=6hr),
     3-hourly (frequency=3hr) and hourly (frequency=1hr) data time frequencies.
 
     Parameters
@@ -501,56 +499,45 @@ def regrid_time(cube, frequency):
     iris.cube.Cube
         cube with converted time axis and units.
     """
+    # fix calendars
+    cube.coord('time').units = cf_units.Unit(
+        cube.coord('time').units.origin,
+        calendar='gregorian',
+    )
+
     # standardize time points
     time_c = [cell.point for cell in cube.coord('time').cells()]
-    if frequency == 'yr':
-        time_cells = [
-            datetime.datetime(t.year, 7, 1, 0, 0, 0) for t in time_c
-        ]
-    elif frequency == 'mon':
-        time_cells = [
+    if frequency == 'mon':
+        cube.coord('time').cells = [
             datetime.datetime(t.year, t.month, 15, 0, 0, 0) for t in time_c
         ]
     elif frequency == 'day':
-        time_cells = [
+        cube.coord('time').cells = [
             datetime.datetime(t.year, t.month, t.day, 0, 0, 0) for t in time_c
         ]
     elif frequency == '1hr':
-        time_cells = [
+        cube.coord('time').cells = [
             datetime.datetime(t.year, t.month, t.day, t.hour, 0, 0)
             for t in time_c
         ]
     elif frequency == '3hr':
-        time_cells = [
+        cube.coord('time').cells = [
             datetime.datetime(t.year, t.month, t.day, t.hour - t.hour % 3, 0,
                               0) for t in time_c
         ]
     elif frequency == '6hr':
-        time_cells = [
+        cube.coord('time').cells = [
             datetime.datetime(t.year, t.month, t.day, t.hour - t.hour % 6, 0,
                               0) for t in time_c
         ]
 
     cube.coord('time').points = [
         cube.coord('time').units.date2num(cl)
-        for cl in time_cells]
+        for cl in cube.coord('time').cells
+    ]
 
     # uniformize bounds
     cube.coord('time').bounds = None
     cube.coord('time').guess_bounds()
 
-    # remove aux coords that will differ
-    reset_aux = ['day_of_month', 'day_of_year']
-    for auxcoord in cube.aux_coords:
-        if auxcoord.long_name in reset_aux:
-            cube.remove_coord(auxcoord)
-
-    # re-add the converted aux coords
-    iris.coord_categorisation.add_day_of_month(cube,
-                                               cube.coord('time'),
-                                               name='day_of_month')
-    iris.coord_categorisation.add_day_of_year(cube,
-                                              cube.coord('time'),
-                                              name='day_of_year')
-
     return cube


Test patch:
diff --git a/tests/integration/cmor/_fixes/cmip6/test_mcm_ua_1_0.py b/tests/integration/cmor/_fixes/cmip6/test_mcm_ua_1_0.py
new file mode 100644
index 000000000..8723a8bba
--- /dev/null
+++ b/tests/integration/cmor/_fixes/cmip6/test_mcm_ua_1_0.py
@@ -0,0 +1,91 @@
+"""Tests for the fixes of MCM-UA-1-0."""
+import iris
+import pytest
+from cf_units import Unit
+
+from esmvalcore.cmor._fixes.cmip6.mcm_ua_1_0 import AllVars, Tas
+from esmvalcore.cmor.fix import Fix
+
+
+@pytest.fixture
+def cubes():
+    correct_lat_coord = iris.coords.DimCoord([0.0], var_name='lat',
+                                             standard_name='latitude')
+    wrong_lat_coord = iris.coords.DimCoord([0.0], var_name='latitudeCoord',
+                                           standard_name='latitude')
+    correct_lon_coord = iris.coords.DimCoord([0.0], var_name='lon',
+                                             standard_name='longitude')
+    wrong_lon_coord = iris.coords.DimCoord([0.0], var_name='longitudeCoord',
+                                           standard_name='longitude')
+    correct_cube = iris.cube.Cube(
+        [[10.0]],
+        var_name='tas',
+        dim_coords_and_dims=[(correct_lat_coord, 0), (correct_lon_coord, 1)],
+    )
+    wrong_cube = iris.cube.Cube(
+        [[10.0]],
+        var_name='ta',
+        dim_coords_and_dims=[(wrong_lat_coord, 0), (wrong_lon_coord, 1)],
+        attributes={'parent_time_units': 'days since 0000-00-00 (noleap)'},
+    )
+    scalar_cube = iris.cube.Cube(0.0, var_name='ps')
+    return iris.cube.CubeList([correct_cube, wrong_cube, scalar_cube])
+
+
+def test_get_allvars_fix():
+    fix = Fix.get_fixes('CMIP6', 'MCM-UA-1-0', 'arbitrary_var_name')
+    assert fix == [AllVars()]
+
+
+def test_get_tas_fix():
+    fix = Fix.get_fixes('CMIP6', 'MCM-UA-1-0', 'tas')
+    assert fix == [AllVars(), Tas()]
+
+
+def test_allvars_fix_metadata(cubes):
+    fix = AllVars()
+    out_cubes = fix.fix_metadata(cubes)
+    assert cubes is out_cubes
+    for cube in out_cubes:
+        try:
+            lat_coord = cube.coord('latitude')
+        except iris.exceptions.CoordinateNotFoundError:
+            pass
+        else:
+            assert lat_coord.var_name == 'lat'
+            assert lat_coord.standard_name == 'latitude'
+        try:
+            lon_coord = cube.coord('longitude')
+        except iris.exceptions.CoordinateNotFoundError:
+            pass
+        else:
+            assert lon_coord.var_name == 'lon'
+            assert lon_coord.standard_name == 'longitude'
+        if 'parent_time_units' in cube.attributes:
+            assert cube.attributes['parent_time_units'] == (
+                'days since 0000-00-00')
+
+
+def test_tas_fix_metadata(cubes):
+    for cube in cubes:
+        with pytest.raises(iris.exceptions.CoordinateNotFoundError):
+            cube.coord('height')
+    height_coord = iris.coords.AuxCoord(2.0,
+                                        var_name='height',
+                                        standard_name='height',
+                                        long_name='height',
+                                        units=Unit('m'),
+                                        attributes={'positive': 'up'})
+    fix = Tas()
+
+    # Check fix
+    out_cubes = fix.fix_metadata(cubes)
+    assert out_cubes[0].var_name == 'tas'
+    coord = out_cubes[0].coord('height')
+    assert coord == height_coord
+
+    # Check that height coordinate is not added twice
+    out_cubes_2 = fix.fix_metadata(out_cubes)
+    assert out_cubes_2[0].var_name == 'tas'
+    coord = out_cubes_2[0].coord('height')
+    assert coord == height_coord
diff --git a/tests/integration/cmor/_fixes/test_shared.py b/tests/integration/cmor/_fixes/test_shared.py
index 6fad24675..efc3df999 100644
--- a/tests/integration/cmor/_fixes/test_shared.py
+++ b/tests/integration/cmor/_fixes/test_shared.py
@@ -42,6 +42,10 @@ def test_add_scalar_depth_coord(cube_in, depth):
     assert cube_out is cube_in
     coord = cube_in.coord('depth')
     assert coord == depth_coord
+    cube_out_2 = add_scalar_depth_coord(cube_out)
+    assert cube_out_2 is cube_out
+    coord = cube_in.coord('depth')
+    assert coord == depth_coord
 
 
 @pytest.mark.parametrize('cube_in,height', TEST_ADD_SCALAR_COORD)
@@ -64,6 +68,10 @@ def test_add_scalar_height_coord(cube_in, height):
     assert cube_out is cube_in
     coord = cube_in.coord('height')
     assert coord == height_coord
+    cube_out_2 = add_scalar_height_coord(cube_out)
+    assert cube_out_2 is cube_out
+    coord = cube_in.coord('height')
+    assert coord == height_coord
 
 
 @pytest.mark.parametrize('cube_in,typeland', TEST_ADD_SCALAR_COORD)
@@ -85,6 +93,10 @@ def test_add_scalar_typeland_coord(cube_in, typeland):
     assert cube_out is cube_in
     coord = cube_in.coord('area_type')
     assert coord == typeland_coord
+    cube_out_2 = add_scalar_typeland_coord(cube_out)
+    assert cube_out_2 is cube_out
+    coord = cube_in.coord('area_type')
+    assert coord == typeland_coord
 
 
 @pytest.mark.parametrize('cube_in,typesea', TEST_ADD_SCALAR_COORD)
@@ -106,6 +118,10 @@ def test_add_scalar_typesea_coord(cube_in, typesea):
     assert cube_out is cube_in
     coord = cube_in.coord('area_type')
     assert coord == typesea_coord
+    cube_out_2 = add_scalar_typesea_coord(cube_out)
+    assert cube_out_2 is cube_out
+    coord = cube_in.coord('area_type')
+    assert coord == typesea_coord
 
 
 DIM_COORD_NB = iris.coords.DimCoord([3.1415], standard_name='latitude')
diff --git a/tests/unit/preprocessor/_time/test_time.py b/tests/unit/preprocessor/_time/test_time.py
index 510eeb84d..f89f4c71b 100644
--- a/tests/unit/preprocessor/_time/test_time.py
+++ b/tests/unit/preprocessor/_time/test_time.py
@@ -36,13 +36,6 @@ def _create_sample_cube():
     return cube
 
 
-def add_auxiliary_coordinate(cubelist):
-    """Add AuxCoords to cubes in cubelist."""
-    for cube in cubelist:
-        iris.coord_categorisation.add_day_of_month(cube, cube.coord('time'))
-        iris.coord_categorisation.add_day_of_year(cube, cube.coord('time'))
-
-
 class TestExtractMonth(tests.Test):
     """Tests for extract_month."""
 
@@ -521,52 +514,6 @@ class TestDailyStatistics(tests.Test):
         assert_array_equal(result.data, expected)
 
 
-class TestRegridTimeYearly(tests.Test):
-    """Tests for regrid_time with monthly frequency."""
-    def setUp(self):
-        """Prepare tests."""
-        self.cube_1 = _create_sample_cube()
-        self.cube_2 = _create_sample_cube()
-        self.cube_2.data = self.cube_2.data * 2.
-        self.cube_2.remove_coord('time')
-        self.cube_1.remove_coord('time')
-        self.cube_1.add_dim_coord(
-            iris.coords.DimCoord(
-                np.arange(11., 8770., 365.),
-                standard_name='time',
-                units=Unit(
-                    'days since 1950-01-01 00:00:00', calendar='gregorian'),
-            ),
-            0,
-        )
-        self.cube_2.add_dim_coord(
-            iris.coords.DimCoord(
-                np.arange(91., 8851., 365.),
-                standard_name='time',
-                units=Unit(
-                    'days since 1950-01-01 00:00:00', calendar='gregorian'),
-            ),
-            0,
-        )
-        add_auxiliary_coordinate([self.cube_1, self.cube_2])
-
-    def test_regrid_time_year(self):
-        """Test changes to cubes."""
-        # test yearly
-        newcube_1 = regrid_time(self.cube_1, frequency='yr')
-        newcube_2 = regrid_time(self.cube_2, frequency='yr')
-        # no changes to core data
-        assert_array_equal(newcube_1.data, self.cube_1.data)
-        assert_array_equal(newcube_2.data, self.cube_2.data)
-        # no changes to number of coords and aux_coords
-        assert len(newcube_1.coords()) == len(self.cube_1.coords())
-        assert len(newcube_1.aux_coords) == len(self.cube_1.aux_coords)
-        # test difference; also diff is zero
-        expected = self.cube_1.data
-        diff_cube = newcube_2 - newcube_1
-        assert_array_equal(diff_cube.data, expected)
-
-
 class TestRegridTimeMonthly(tests.Test):
     """Tests for regrid_time with monthly frequency."""
 
@@ -581,11 +528,10 @@ class TestRegridTimeMonthly(tests.Test):
                 np.arange(14., 719., 30.),
                 standard_name='time',
                 units=Unit(
-                    'days since 1950-01-01 00:00:00', calendar='gregorian'),
+                    'days since 1950-01-01 00:00:00', calendar='360_day'),
             ),
             0,
         )
-        add_auxiliary_coordinate([self.cube_1, self.cube_2])
 
     def test_regrid_time_mon(self):
         """Test changes to cubes."""
@@ -619,7 +565,7 @@ class TestRegridTimeDaily(tests.Test):
                 np.arange(14. * 24. + 6., 38. * 24. + 6., 24.),
                 standard_name='time',
                 units=Unit(
-                    'hours since 1950-01-01 00:00:00', calendar='gregorian'),
+                    'hours since 1950-01-01 00:00:00', calendar='360_day'),
             ),
             0,
         )
@@ -628,11 +574,10 @@ class TestRegridTimeDaily(tests.Test):
                 np.arange(14. * 24. + 3., 38. * 24. + 3., 24.),
                 standard_name='time',
                 units=Unit(
-                    'hours since 1950-01-01 00:00:00', calendar='gregorian'),
+                    'hours since 1950-01-01 00:00:00', calendar='360_day'),
             ),
             0,
         )
-        add_auxiliary_coordinate([self.cube_1, self.cube_2])
 
     def test_regrid_time_day(self):
         """Test changes to cubes."""
@@ -679,7 +624,6 @@ class TestRegridTime6Hourly(tests.Test):
             ),
             0,
         )
-        add_auxiliary_coordinate([self.cube_1, self.cube_2])
 
     def test_regrid_time_6hour(self):
         """Test changes to cubes."""
@@ -713,7 +657,7 @@ class TestRegridTime3Hourly(tests.Test):
                 np.arange(17. * 180. + 40., 41. * 180. + 40., 180.),
                 standard_name='time',
                 units=Unit(
-                    'minutes since 1950-01-01 00:00:00', calendar='gregorian'),
+                    'minutes since 1950-01-01 00:00:00', calendar='360_day'),
             ),
             0,
         )
@@ -722,11 +666,10 @@ class TestRegridTime3Hourly(tests.Test):
                 np.arange(17. * 180. + 150., 41. * 180. + 150., 180.),
                 standard_name='time',
                 units=Unit(
-                    'minutes since 1950-01-01 00:00:00', calendar='gregorian'),
+                    'minutes since 1950-01-01 00:00:00', calendar='360_day'),
             ),
             0,
         )
-        add_auxiliary_coordinate([self.cube_1, self.cube_2])
 
     def test_regrid_time_3hour(self):
         """Test changes to cubes."""
@@ -773,7 +716,6 @@ class TestRegridTime1Hourly(tests.Test):
             ),
             0,
         )
-        add_auxiliary_coordinate([self.cube_1, self.cube_2])
 
     def test_regrid_time_hour(self):
         """Test changes to cubes."""