Repository: ESMValGroup/ESMValCore

Issue:
Switch from vmprof to vprof as supported diagnostic profiler?
We currently support profiling diagnostic with [vmprof](https://github.com/vmprof/vmprof-python) when the `profile_diagnostic` option is set to `true` in config-user.yml. I tried to use the output, but was not successful in getting it to display. The project does not look very active on GitHub either and we regularly have installation issues with it, see e.g. [here](https://app.circleci.com/pipelines/github/ESMValGroup/ESMValTool/3065/workflows/247fde2e-e729-4ee6-b60d-ca1a7ba66827/jobs/35980).

We could switch to [vprof](https://github.com/nvdv/vprof), which looks slightly more maintained, is easier to install, and produces json output (instead of custom file format) as well as a kind of working local website.

@jvegasbsc since you first implemented this feature, do you have an opinion on this?

Or does anyone have a suggestion for a more user friendly profiler?

Discussion:
bouweandela: You would also need to change something here:
https://github.com/ESMValGroup/ESMValCore/blob/7cad3f5a103b151c7b833e2097def74c2b9b2e6f/esmvalcore/_task.py#L303-L308
and check that profiling the diagnostic actually works by running a recipe with profile_diagnostic: true in config-user.yml.
valeriupredoi: good call @bouweandela :beer: Doing it now!
valeriupredoi: done @bouweandela - well spotted - here's the example page https://github.com/nvdv/vprof - I am using the basic profiler `p` option (I found the `c` option to `-c` to be problematic)
valeriupredoi: man, why are the new Circle tests so unreliable? :confounded: 
bouweandela: > I am using the basic profiler p option (I found the c option to -c to be problematic)

What problems are you encountering? It seems to work fine for me?
bouweandela: We would also need some documentation on how to use the results
valeriupredoi: > We would also need some documentation on how to use the results

I was afraid of that - I don't know much about this `vprof` thing, you mind if you patch together a couple sentences :grin: 
bouweandela: For documentation, you can just shortly introduce the concept of profiling, mention that if you enable `profile_diagnostic` in config user, the tool with use `vprof` to profile diagnostics (link to vprof documentatoin) and that you can view the results of profiling with something like
```
vprof --input-file /home/bandela/esmvaltool_output/recipe_python_20200915_141839/run/diagnostic1/script1/profile.json
```
e.g. [here}(https://docs.esmvaltool.org/projects/esmvalcore/en/latest/quickstart/configure.html).

Patch:
diff --git a/doc/quickstart/configure.rst b/doc/quickstart/configure.rst
index 6d94dad4c..d2d9b6498 100644
--- a/doc/quickstart/configure.rst
+++ b/doc/quickstart/configure.rst
@@ -100,7 +100,9 @@ with explanations in a commented line above each option:
   # See config-developer.yml for an example. Set to None to use the default
   config_developer_file: null
 
-  # Get profiling information for diagnostics
+  # Use a profiling tool for the diagnostic run [false]/true
+  # A profiler tells you which functions in your code take most time to run.
+  # For this purpose we use vprof, see below for notes
   # Only available for Python diagnostics
   profile_diagnostic: false
 
@@ -147,6 +149,22 @@ downloaded at runtime.
    This setting is not for model or observational datasets, rather it is for
    data files used in plotting such as coastline descriptions and so on.
 
+The ``profile_diagnostic`` setting triggers profiling of Python diagnostics,
+this will tell you which functions in the diagnostic took most time to run.
+For this purpose we use `vprof <https://github.com/nvdv/vprof>`_.
+For each diagnostic script in the recipe, the profiler writes a ``.json`` file
+that can be used to plot a
+`flame graph <https://queue.acm.org/detail.cfm?id=2927301>`__
+of the profiling information by running
+
+.. code-block:: bash
+
+  vprof --input-file esmvaltool_output/recipe_output/run/diagnostic/script/profile.json
+
+Note that it is also possible to use vprof to understand other resources used
+while running the diagnostic, including execution time of different code blocks
+and memory usage.
+
 A detailed explanation of the data finding-related sections of the
 ``config-user.yml`` (``rootpath`` and ``drs``) is presented in the
 :ref:`data-retrieval` section. This section relates directly to the data
@@ -186,10 +204,10 @@ Users can get a copy of this file with default values by running
 
 .. code-block:: bash
 
-  ``esmvaltool config get-config-developer --path=${TARGET_FOLDER}``.
+  esmvaltool config get-config-developer --path=${TARGET_FOLDER}
 
 If the option ``--path`` is omitted, the file will be created in
-`${HOME}/.esmvaltool`
+```${HOME}/.esmvaltool``.
 
 .. note::
 
@@ -281,7 +299,7 @@ following documentation section:
 
 .. code-block:: yaml
 
-  documentation
+  documentation:
     authors:
       - demo_le
 
diff --git a/environment.yml b/environment.yml
index edce7f108..2f000c8e4 100644
--- a/environment.yml
+++ b/environment.yml
@@ -8,6 +8,5 @@ dependencies:
   - esmpy
   - iris>=2.2.1
   - graphviz
-  - libunwind  # Needed for Python3.7+
   - python>=3.6  # if 3.7 lxml will not import correctly if <4.5.0
   - python-stratify
diff --git a/esmvalcore/_main.py b/esmvalcore/_main.py
index c7841e89a..c6724559e 100755
--- a/esmvalcore/_main.py
+++ b/esmvalcore/_main.py
@@ -198,13 +198,13 @@ class Recipes():
         recipes_folder = os.path.join(DIAGNOSTICS_PATH, 'recipes')
         logger.info("Showing recipes installed in %s", recipes_folder)
         print('# Installed recipes')
-        for root, _, files in os.walk(recipes_folder):
+        for root, _, files in sorted(os.walk(recipes_folder)):
             root = os.path.relpath(root, recipes_folder)
             if root == '.':
                 root = ''
             if root:
                 print(f"\n# {root.replace(os.sep, ' - ').title()}")
-            for filename in files:
+            for filename in sorted(files):
                 if filename.endswith('.yml'):
                     print(os.path.join(root, filename))
 
diff --git a/esmvalcore/_task.py b/esmvalcore/_task.py
index c3d09f7c8..637c8cb5d 100644
--- a/esmvalcore/_task.py
+++ b/esmvalcore/_task.py
@@ -301,10 +301,10 @@ class DiagnosticTask(BaseTask):
                 'ncl': ['-n', '-p'],
             }
             if self.settings['profile_diagnostic']:
-                profile_file = Path(self.settings['run_dir']) / 'profile.bin'
+                profile_file = Path(self.settings['run_dir']) / 'profile.json'
                 args['py'] = [
-                    '-m', 'vmprof', '--lines', '-o',
-                    str(profile_file)
+                    '-m', 'vprof', '-o',
+                    str(profile_file), '-c', 'c'
                 ]
 
             ext = script_file.suffix.lower()[1:]
@@ -488,7 +488,12 @@ class DiagnosticTask(BaseTask):
         if ext == '.ncl':
             env['settings'] = settings_file
         else:
-            cmd.append(settings_file)
+            if self.settings['profile_diagnostic']:
+                script_file = cmd.pop()
+                combo_with_settings = script_file + ' ' + str(settings_file)
+                cmd.append(combo_with_settings)
+            else:
+                cmd.append(settings_file)
 
         process = self._start_diagnostic_script(cmd, env)
 
diff --git a/esmvalcore/cmor/_fixes/cmip6/bcc_csm2_mr.py b/esmvalcore/cmor/_fixes/cmip6/bcc_csm2_mr.py
index 2787e282e..e084fbebb 100644
--- a/esmvalcore/cmor/_fixes/cmip6/bcc_csm2_mr.py
+++ b/esmvalcore/cmor/_fixes/cmip6/bcc_csm2_mr.py
@@ -17,16 +17,41 @@ class Tos(BaseTos):
 
     def fix_metadata(self, cubes):
         """Rename ``var_name`` of 1D-``latitude`` and 1D-``longitude``.
-
         Parameters
         ----------
         cubes : iris.cube.CubeList
             Input cubes.
-
         Returns
         -------
         iris.cube.CubeList
+        """
+        cube = self.get_cube_from_list(cubes)
+        lat_coord = cube.coord('latitude', dimensions=(1, ))
+        lon_coord = cube.coord('longitude', dimensions=(2, ))
+        lat_coord.standard_name = None
+        lat_coord.long_name = 'grid_latitude'
+        lat_coord.var_name = 'i'
+        lat_coord.units = '1'
+        lon_coord.standard_name = None
+        lon_coord.long_name = 'grid_longitude'
+        lon_coord.var_name = 'j'
+        lon_coord.units = '1'
+        lon_coord.circular = False
+        return cubes
+
+
+class Siconc(BaseTos):
+    """Fixes for siconc."""
 
+    def fix_metadata(self, cubes):
+        """Rename ``var_name`` of 1D-``latitude`` and 1D-``longitude``.
+        Parameters
+        ----------
+        cubes : iris.cube.CubeList
+            Input cubes.
+        Returns
+        -------
+        iris.cube.CubeList
         """
         cube = self.get_cube_from_list(cubes)
         lat_coord = cube.coord('latitude', dimensions=(1, ))
diff --git a/esmvalcore/cmor/_fixes/cmip6/cesm2.py b/esmvalcore/cmor/_fixes/cmip6/cesm2.py
index 5f8e7ea81..e358354fd 100644
--- a/esmvalcore/cmor/_fixes/cmip6/cesm2.py
+++ b/esmvalcore/cmor/_fixes/cmip6/cesm2.py
@@ -2,10 +2,12 @@
 from shutil import copyfile
 
 from netCDF4 import Dataset
+import numpy as np
 
 from ..fix import Fix
 from ..shared import (add_scalar_depth_coord, add_scalar_height_coord,
                       add_scalar_typeland_coord, add_scalar_typesea_coord)
+from .gfdl_esm4 import Siconc as Addtypesi
 
 
 class Cl(Fix):
@@ -107,7 +109,10 @@ class Tas(Fix):
     """Fixes for tas."""
 
     def fix_metadata(self, cubes):
-        """Add height (2m) coordinate.
+        """
+        Add height (2m) coordinate.
+
+        Fix latitude_bounds and longitude_bounds data type and round to 4 d.p.
 
         Parameters
         ----------
@@ -121,6 +126,19 @@ class Tas(Fix):
         """
         cube = self.get_cube_from_list(cubes)
         add_scalar_height_coord(cube)
+
+        for cube in cubes:
+            latitude = cube.coord('latitude')
+            if latitude.bounds is None:
+                latitude.guess_bounds()
+            latitude.bounds = latitude.bounds.astype(np.float64)
+            latitude.bounds = np.round(latitude.bounds, 4)
+            longitude = cube.coord('longitude')
+            if longitude.bounds is None:
+                longitude.guess_bounds()
+            longitude.bounds = longitude.bounds.astype(np.float64)
+            longitude.bounds = np.round(longitude.bounds, 4)
+
         return cubes
 
 
@@ -164,3 +182,34 @@ class Sftof(Fix):
         cube = self.get_cube_from_list(cubes)
         add_scalar_typesea_coord(cube)
         return cubes
+
+
+class Tos(Fix):
+    """Fixes for tos."""
+
+    def fix_metadata(self, cubes):
+        """
+        Round times to 1 d.p. for monthly means.
+
+        Required to get hist-GHG and ssp245-GHG Omon tos to concatenate.
+
+        Parameters
+        ----------
+        cubes : iris.cube.CubeList
+            Input cubes.
+
+        Returns
+        -------
+        iris.cube.CubeList
+
+        """
+        cube = self.get_cube_from_list(cubes)
+
+        for cube in cubes:
+            if cube.attributes['mipTable'] == 'Omon':
+                cube.coord('time').points = \
+                    np.round(cube.coord('time').points, 1)
+        return cubes
+
+
+Siconc = Addtypesi
diff --git a/esmvalcore/cmor/_fixes/cmip6/fgoals_g3.py b/esmvalcore/cmor/_fixes/cmip6/fgoals_g3.py
index df6de9428..c752ca207 100644
--- a/esmvalcore/cmor/_fixes/cmip6/fgoals_g3.py
+++ b/esmvalcore/cmor/_fixes/cmip6/fgoals_g3.py
@@ -1,5 +1,7 @@
 """Fixes for FGOALS-g3 model."""
 from ..cmip5.fgoals_g2 import Cl as BaseCl
+from ..common import OceanFixGrid
+
 
 Cl = BaseCl
 
@@ -8,3 +10,9 @@ Cli = BaseCl
 
 
 Clw = BaseCl
+
+
+Tos = OceanFixGrid
+
+
+Siconc = OceanFixGrid
diff --git a/esmvalcore/cmor/_fixes/cmip6/gfdl_esm4.py b/esmvalcore/cmor/_fixes/cmip6/gfdl_esm4.py
new file mode 100644
index 000000000..a4e937842
--- /dev/null
+++ b/esmvalcore/cmor/_fixes/cmip6/gfdl_esm4.py
@@ -0,0 +1,32 @@
+"""Fixes for GFDL-ESM4 model."""
+import iris
+from ..fix import Fix
+
+
+class Siconc(Fix):
+    """Fixes for siconc."""
+
+    def fix_metadata(self, cubes):
+        """
+        Fix missing type.
+
+        Parameters
+        ----------
+        cubes: iris CubeList
+            List of cubes to fix
+
+        Returns
+        -------
+        iris.cube.CubeList
+
+        """
+        typesi = iris.coords.AuxCoord(
+            'siconc',
+            standard_name='area_type',
+            long_name='Sea Ice area type',
+            var_name='type',
+            units='1',
+            bounds=None)
+        for cube in cubes:
+            cube.add_aux_coord(typesi)
+        return cubes
diff --git a/esmvalcore/cmor/_fixes/cmip6/noresm2_lm.py b/esmvalcore/cmor/_fixes/cmip6/noresm2_lm.py
new file mode 100644
index 000000000..0e25c90fb
--- /dev/null
+++ b/esmvalcore/cmor/_fixes/cmip6/noresm2_lm.py
@@ -0,0 +1,33 @@
+"""Fixes for NorESM2-LM model."""
+import numpy as np
+from ..fix import Fix
+
+
+class Siconc(Fix):
+    """Fixes for siconc."""
+
+    def fix_metadata(self, cubes):
+        """
+        Fix metadata.
+
+        Some coordinate points vary for different files of this dataset (for
+        different time range). This fix removes these inaccuracies by rounding
+        the coordinates.
+
+        Parameters
+        ----------
+        cubes: iris.cube.CubeList
+            Input cubes to fix.
+
+        Returns
+        -------
+        iris.cube.CubeList
+
+        """
+        for cube in cubes:
+            latitude = cube.coord('latitude')
+            latitude.bounds = np.round(latitude.bounds, 4)
+            longitude = cube.coord('longitude')
+            longitude.bounds = np.round(longitude.bounds, 4)
+
+        return cubes
diff --git a/esmvalcore/cmor/_fixes/common.py b/esmvalcore/cmor/_fixes/common.py
index d60b2eb7f..2e3165dc3 100644
--- a/esmvalcore/cmor/_fixes/common.py
+++ b/esmvalcore/cmor/_fixes/common.py
@@ -1,5 +1,7 @@
 """Common fixes used for multiple datasets."""
 import iris
+import numpy as np
+from scipy.ndimage import map_coordinates
 
 from .fix import Fix
 from .shared import add_plev_from_altitude, fix_bounds
@@ -101,3 +103,96 @@ class ClFixHybridPressureCoord(Fix):
         cube.coord(var_name='ps').attributes = {}
 
         return iris.cube.CubeList([cube])
+
+
+class OceanFixGrid(Fix):
+    """Fixes for tos, siconc in FGOALS-g3."""
+
+    def fix_data(self, cube):
+        """
+        Fix data.
+
+        Calculate missing latitude/longitude boundaries using interpolation.
+        Based on a similar fix for BCC-CSM2-MR.
+
+        Parameters
+        ----------
+        cube: iris.cube.Cube
+            Input cube to fix.
+
+        Returns
+        -------
+        iris.cube.Cube
+        """
+        rlat = cube.coord('grid_latitude').points
+        rlon = cube.coord('grid_longitude').points
+
+        # Guess coordinate bounds in rlat, rlon (following BCC-CSM2-MR-1).
+        rlat_idx_bnds = np.zeros((len(rlat), 2))
+        rlat_idx_bnds[:, 0] = np.arange(len(rlat)) - 0.5
+        rlat_idx_bnds[:, 1] = np.arange(len(rlat)) + 0.5
+        rlat_idx_bnds[0, 0] = 0.
+        rlat_idx_bnds[len(rlat) - 1, 1] = len(rlat)
+        rlon_idx_bnds = np.zeros((len(rlon), 2))
+        rlon_idx_bnds[:, 0] = np.arange(len(rlon)) - 0.5
+        rlon_idx_bnds[:, 1] = np.arange(len(rlon)) + 0.5
+
+        # Calculate latitude/longitude vertices by interpolation
+        lat_vertices = []
+        lon_vertices = []
+        for (i, j) in [(0, 0), (0, 1), (1, 1), (1, 0)]:
+            (rlat_v, rlon_v) = np.meshgrid(rlat_idx_bnds[:, i],
+                                           rlon_idx_bnds[:, j],
+                                           indexing='ij')
+            lat_vertices.append(
+                map_coordinates(cube.coord('latitude').points,
+                                [rlat_v, rlon_v],
+                                mode='nearest'))
+            lon_vertices.append(
+                map_coordinates(cube.coord('longitude').points,
+                                [rlat_v, rlon_v],
+                                mode='wrap'))
+        lat_vertices = np.array(lat_vertices)
+        lon_vertices = np.array(lon_vertices)
+        lat_vertices = np.moveaxis(lat_vertices, 0, -1)
+        lon_vertices = np.moveaxis(lon_vertices, 0, -1)
+
+        # Copy vertices to cube
+        cube.coord('latitude').bounds = lat_vertices
+        cube.coord('longitude').bounds = lon_vertices
+        return cube
+
+    def fix_metadata(self, cubes):
+        """
+        Rename ``var_name`` of 1D-``latitude`` and 1D-``longitude``.
+
+        Parameters
+        ----------
+        cubes : iris.cube.CubeList
+            Input cubes.
+
+        Returns
+        -------
+        iris.cube.CubeList
+        """
+        cube = self.get_cube_from_list(cubes)
+        lat_coord = cube.coord('cell index along second dimension',
+                               dimensions=(1, ))
+        lon_coord = cube.coord('cell index along first dimension',
+                               dimensions=(2, ))
+        lat_coord.standard_name = None
+        lat_coord.long_name = 'grid_latitude'
+        lat_coord.var_name = 'i'
+        lat_coord.units = '1'
+        lon_coord.standard_name = None
+        lon_coord.long_name = 'grid_longitude'
+        lon_coord.var_name = 'j'
+        lon_coord.units = '1'
+        lon_coord.circular = False
+        # FGOALS-g3 data contain latitude and longitude data set to
+        # >1e30 in some places. Set to 0. to avoid problem in check.py.
+        cube.coord('latitude').points[cube.coord('latitude').points > 1000.]\
+            = 0.
+        cube.coord('longitude').points[cube.coord('longitude').points > 1000.]\
+            = 0.
+        return cubes
diff --git a/package/meta.yaml b/package/meta.yaml
index 4996b4f77..917277fe2 100644
--- a/package/meta.yaml
+++ b/package/meta.yaml
@@ -36,7 +36,6 @@ requirements:
   run:
     # esmvaltool
     - python>=3.6
-    - libunwind  # specifically for Python3.7+
     - graphviz
     - iris>=2.2.1
     - python-stratify
diff --git a/setup.cfg b/setup.cfg
index a68dd76d3..6fd9f1591 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -27,3 +27,7 @@ parallel = true
 
 [pydocstyle]
 convention = numpy
+
+[isort]
+multi_line_output = 3
+include_trailing_comma = true
diff --git a/setup.py b/setup.py
index 7a875b65c..90aa1cb9e 100755
--- a/setup.py
+++ b/setup.py
@@ -68,7 +68,7 @@ REQUIREMENTS = {
         'prospector[with_pyroma]!=1.1.6.3,!=1.1.6.4',
         'sphinx>2',
         'sphinx_rtd_theme',
-        'vmprof',
+        'vprof',
         'yamllint',
         'yapf',
     ],


Test patch:
diff --git a/tests/integration/cmor/_fixes/cmip6/test_bcc_csm2_mr.py b/tests/integration/cmor/_fixes/cmip6/test_bcc_csm2_mr.py
index d6081042e..4316bbc17 100644
--- a/tests/integration/cmor/_fixes/cmip6/test_bcc_csm2_mr.py
+++ b/tests/integration/cmor/_fixes/cmip6/test_bcc_csm2_mr.py
@@ -3,7 +3,8 @@ import unittest.mock
 
 import iris
 
-from esmvalcore.cmor._fixes.cmip6.bcc_csm2_mr import Cl, Cli, Clw, Tos
+from esmvalcore.cmor._fixes.cmip6.bcc_csm2_mr import (Cl, Cli,
+                                                      Clw, Tos, Siconc)
 from esmvalcore.cmor._fixes.common import ClFixHybridPressureCoord
 from esmvalcore.cmor._fixes.fix import Fix
 from esmvalcore.cmor.table import get_var_info
@@ -48,6 +49,12 @@ def test_get_tos_fix():
     assert fix == [Tos(None)]
 
 
+def test_get_siconc_fix():
+    """Test getting of fix."""
+    fix = Fix.get_fixes('CMIP6', 'BCC-CSM2-MR', 'Omon', 'siconc')
+    assert fix == [Siconc(None)]
+
+
 @unittest.mock.patch(
     'esmvalcore.cmor._fixes.cmip6.bcc_csm2_mr.BaseTos.fix_data',
     autospec=True)
@@ -58,6 +65,16 @@ def test_tos_fix_data(mock_base_fix_data):
     mock_base_fix_data.assert_called_once_with(fix, 'cubes')
 
 
+@unittest.mock.patch(
+    'esmvalcore.cmor._fixes.cmip6.bcc_csm2_mr.BaseTos.fix_data',
+    autospec=True)
+def test_siconc_fix_data(mock_base_fix_data):
+    """Test ``fix_data`` for ``siconc``."""
+    fix = Siconc(None)
+    fix.fix_data('cubes')
+    mock_base_fix_data.assert_called_once_with(fix, 'cubes')
+
+
 def test_tos_fix_metadata():
     """Test ``fix_metadata`` for ``tos``."""
     grid_lat = iris.coords.DimCoord([1.0],
@@ -115,3 +132,63 @@ def test_tos_fix_metadata():
     assert grid_lon.standard_name is None
     assert grid_lon.units == '1'
     assert not grid_lon.circular
+
+
+def test_siconc_fix_metadata():
+    """Test ``fix_metadata`` for ``tos``."""
+    grid_lat = iris.coords.DimCoord([1.0],
+                                    var_name='lat',
+                                    standard_name='latitude',
+                                    long_name='latitude',
+                                    units='degrees_north',
+                                    attributes={'1D': '1'})
+    grid_lon = iris.coords.DimCoord([1.0],
+                                    var_name='lon',
+                                    standard_name='longitude',
+                                    long_name='longitude',
+                                    units='degrees_east',
+                                    circular=True,
+                                    attributes={'1D': '1'})
+    latitude = iris.coords.AuxCoord([[0.0]],
+                                    var_name='lat',
+                                    standard_name='latitude',
+                                    long_name='latitude',
+                                    units='degrees_north')
+    longitude = iris.coords.AuxCoord([[0]],
+                                     var_name='lon',
+                                     standard_name='longitude',
+                                     long_name='longitude',
+                                     units='degrees_east')
+
+    cube = iris.cube.Cube(
+        [[[0.0]]],
+        var_name='siconc',
+        long_name='sea_ice_area_fraction',
+        dim_coords_and_dims=[(grid_lat.copy(), 1), (grid_lon.copy(), 2)],
+        aux_coords_and_dims=[(latitude.copy(), (1, 2)),
+                             (longitude.copy(), (1, 2))],
+    )
+    cubes = iris.cube.CubeList([cube, iris.cube.Cube(0.0)])
+    vardef = get_var_info('CMIP6', 'SImon', 'siconc')
+    fix = Siconc(vardef)
+    fixed_cubes = fix.fix_metadata(cubes)
+    siconc_cube = fixed_cubes.extract_strict('sea_ice_area_fraction')
+
+    # No duplicates anymore
+    assert len(siconc_cube.coords('latitude')) == 1
+    assert len(siconc_cube.coords('longitude')) == 1
+
+    # Latitude
+    grid_lat = siconc_cube.coord('grid_latitude')
+    assert grid_lat.var_name == 'i'
+    assert grid_lat.long_name == 'grid_latitude'
+    assert grid_lat.standard_name is None
+    assert grid_lat.units == '1'
+
+    # Longitude
+    grid_lon = siconc_cube.coord('grid_longitude')
+    assert grid_lon.var_name == 'j'
+    assert grid_lon.long_name == 'grid_longitude'
+    assert grid_lon.standard_name is None
+    assert grid_lon.units == '1'
+    assert not grid_lon.circular
diff --git a/tests/integration/cmor/_fixes/cmip6/test_cesm2.py b/tests/integration/cmor/_fixes/cmip6/test_cesm2.py
index 269ecf44c..20d2a351a 100644
--- a/tests/integration/cmor/_fixes/cmip6/test_cesm2.py
+++ b/tests/integration/cmor/_fixes/cmip6/test_cesm2.py
@@ -9,7 +9,7 @@ import pytest
 from cf_units import Unit
 from netCDF4 import Dataset
 
-from esmvalcore.cmor._fixes.cmip6.cesm2 import Cl, Cli, Clw, Tas
+from esmvalcore.cmor._fixes.cmip6.cesm2 import Cl, Cli, Clw, Tas, Tos
 from esmvalcore.cmor.fix import Fix
 from esmvalcore.cmor.table import get_var_info
 
@@ -18,67 +18,70 @@ from esmvalcore.cmor.table import get_var_info
 def cl_file(tmp_path):
     """Create netcdf file with similar issues as ``cl``."""
     nc_path = os.path.join(tmp_path, 'cesm2_cl.nc')
-    dataset = Dataset(nc_path, mode='w')
-    dataset.createDimension('time', size=1)
-    dataset.createDimension('lev', size=2)
-    dataset.createDimension('lat', size=3)
-    dataset.createDimension('lon', size=4)
-    dataset.createDimension('bnds', size=2)
-
-    # Dimensional variables
-    dataset.createVariable('time', np.float64, dimensions=('time',))
-    dataset.createVariable('lev', np.float64, dimensions=('lev',))
-    dataset.createVariable('lev_bnds', np.float64, dimensions=('lev', 'bnds'))
-    dataset.createVariable('lat', np.float64, dimensions=('lat',))
-    dataset.createVariable('lon', np.float64, dimensions=('lon',))
-    dataset.variables['time'][:] = [0.0]
-    dataset.variables['time'].standard_name = 'time'
-    dataset.variables['time'].units = 'days since 6543-2-1'
-    dataset.variables['lev'][:] = [1.0, 2.0]
-    dataset.variables['lev'].bounds = 'lev_bnds'
-    dataset.variables['lev'].units = 'hPa'
-    dataset.variables['lev_bnds'][:] = [[0.5, 1.5], [1.5, 3.0]]
-    dataset.variables['lev_bnds'].standard_name = (
-        'atmosphere_hybrid_sigma_pressure_coordinate')
-    dataset.variables['lev_bnds'].units = '1'
-    dataset.variables['lev_bnds'].formula_terms = (
-        'p0: p0 a: a_bnds b: b_bnds ps: ps')
-    dataset.variables['lat'][:] = [-30.0, 0.0, 30.0]
-    dataset.variables['lat'].standard_name = 'latitude'
-    dataset.variables['lat'].units = 'degrees_north'
-    dataset.variables['lon'][:] = [30.0, 60.0, 90.0, 120.0]
-    dataset.variables['lon'].standard_name = 'longitude'
-    dataset.variables['lon'].units = 'degrees_east'
-
-    # Coordinates for derivation of pressure coordinate
-    dataset.createVariable('a', np.float64, dimensions=('lev',))
-    dataset.createVariable('a_bnds', np.float64, dimensions=('lev', 'bnds'))
-    dataset.createVariable('b', np.float64, dimensions=('lev',))
-    dataset.createVariable('b_bnds', np.float64, dimensions=('lev', 'bnds'))
-    dataset.createVariable('p0', np.float64, dimensions=())
-    dataset.createVariable('ps', np.float64,
-                           dimensions=('time', 'lat', 'lon'))
-    dataset.variables['a'][:] = [1.0, 2.0]
-    dataset.variables['a'].bounds = 'a_bnds'
-    dataset.variables['a_bnds'][:] = [[1.5, 3.0], [0.0, 1.5]]  # intended
-    dataset.variables['b'][:] = [0.0, 1.0]
-    dataset.variables['b'].bounds = 'b_bnds'
-    dataset.variables['b_bnds'][:] = [[0.5, 2.0], [-1.0, 0.5]]  # intended
-    dataset.variables['p0'][:] = 1.0
-    dataset.variables['p0'].units = 'Pa'
-    dataset.variables['ps'][:] = np.arange(1 * 3 * 4).reshape(1, 3, 4)
-    dataset.variables['ps'].standard_name = 'surface_air_pressure'
-    dataset.variables['ps'].units = 'Pa'
-
-    # Cl variable
-    dataset.createVariable('cl', np.float32,
-                           dimensions=('time', 'lev', 'lat', 'lon'))
-    dataset.variables['cl'][:] = np.full((1, 2, 3, 4), 0.0, dtype=np.float32)
-    dataset.variables['cl'].standard_name = (
-        'cloud_area_fraction_in_atmosphere_layer')
-    dataset.variables['cl'].units = '%'
+    with Dataset(nc_path, mode='w') as dataset:
+        dataset.createDimension('time', size=1)
+        dataset.createDimension('lev', size=2)
+        dataset.createDimension('lat', size=3)
+        dataset.createDimension('lon', size=4)
+        dataset.createDimension('bnds', size=2)
+
+        # Dimensional variables
+        dataset.createVariable('time', np.float64, dimensions=('time',))
+        dataset.createVariable('lev', np.float64, dimensions=('lev',))
+        dataset.createVariable('lev_bnds', np.float64, dimensions=('lev',
+                                                                   'bnds'))
+        dataset.createVariable('lat', np.float64, dimensions=('lat',))
+        dataset.createVariable('lon', np.float64, dimensions=('lon',))
+        dataset.variables['time'][:] = [0.0]
+        dataset.variables['time'].standard_name = 'time'
+        dataset.variables['time'].units = 'days since 6543-2-1'
+        dataset.variables['lev'][:] = [1.0, 2.0]
+        dataset.variables['lev'].bounds = 'lev_bnds'
+        dataset.variables['lev'].units = 'hPa'
+        dataset.variables['lev_bnds'][:] = [[0.5, 1.5], [1.5, 3.0]]
+        dataset.variables['lev_bnds'].standard_name = (
+            'atmosphere_hybrid_sigma_pressure_coordinate')
+        dataset.variables['lev_bnds'].units = '1'
+        dataset.variables['lev_bnds'].formula_terms = (
+            'p0: p0 a: a_bnds b: b_bnds ps: ps')
+        dataset.variables['lat'][:] = [-30.0, 0.0, 30.0]
+        dataset.variables['lat'].standard_name = 'latitude'
+        dataset.variables['lat'].units = 'degrees_north'
+        dataset.variables['lon'][:] = [30.0, 60.0, 90.0, 120.0]
+        dataset.variables['lon'].standard_name = 'longitude'
+        dataset.variables['lon'].units = 'degrees_east'
+
+        # Coordinates for derivation of pressure coordinate
+        dataset.createVariable('a', np.float64, dimensions=('lev',))
+        dataset.createVariable('a_bnds', np.float64, dimensions=('lev',
+                                                                 'bnds'))
+        dataset.createVariable('b', np.float64, dimensions=('lev',))
+        dataset.createVariable('b_bnds', np.float64, dimensions=('lev',
+                                                                 'bnds'))
+        dataset.createVariable('p0', np.float64, dimensions=())
+        dataset.createVariable('ps', np.float64,
+                               dimensions=('time', 'lat', 'lon'))
+        dataset.variables['a'][:] = [1.0, 2.0]
+        dataset.variables['a'].bounds = 'a_bnds'
+        dataset.variables['a_bnds'][:] = [[1.5, 3.0], [0.0, 1.5]]  # intended
+        dataset.variables['b'][:] = [0.0, 1.0]
+        dataset.variables['b'].bounds = 'b_bnds'
+        dataset.variables['b_bnds'][:] = [[0.5, 2.0], [-1.0, 0.5]]  # intended
+        dataset.variables['p0'][:] = 1.0
+        dataset.variables['p0'].units = 'Pa'
+        dataset.variables['ps'][:] = np.arange(1 * 3 * 4).reshape(1, 3, 4)
+        dataset.variables['ps'].standard_name = 'surface_air_pressure'
+        dataset.variables['ps'].units = 'Pa'
+
+        # Cl variable
+        dataset.createVariable('cl', np.float32,
+                               dimensions=('time', 'lev', 'lat', 'lon'))
+        dataset.variables['cl'][:] = np.full((1, 2, 3, 4),
+                                             0.0, dtype=np.float32)
+        dataset.variables['cl'].standard_name = (
+            'cloud_area_fraction_in_atmosphere_layer')
+        dataset.variables['cl'].units = '%'
 
-    dataset.close()
     return nc_path
 
 
@@ -241,17 +244,70 @@ def test_clw_fix():
 @pytest.fixture
 def tas_cubes():
     """Cubes to test fixes for ``tas``."""
-    ta_cube = iris.cube.Cube([1.0], var_name='ta')
-    tas_cube = iris.cube.Cube([3.0], var_name='tas')
+    time_coord = iris.coords.DimCoord(
+        [0.0, 1.0], var_name='time', standard_name='time',
+        units='days since 1850-01-01 00:00:00')
+    lat_coord = iris.coords.DimCoord(
+        [0.0, 1.0], var_name='lat', standard_name='latitude', units='degrees')
+    lon_coord = iris.coords.DimCoord(
+        [0.0, 1.0], var_name='lon', standard_name='longitude', units='degrees')
+    coord_specs = [
+        (time_coord, 0),
+        (lat_coord, 1),
+        (lon_coord, 2),
+    ]
+    ta_cube = iris.cube.Cube(
+        np.ones((2, 2, 2)),
+        var_name='ta',
+        dim_coords_and_dims=coord_specs,
+    )
+    tas_cube = iris.cube.Cube(
+        np.ones((2, 2, 2)),
+        var_name='tas',
+        dim_coords_and_dims=coord_specs,
+    )
+
     return iris.cube.CubeList([ta_cube, tas_cube])
 
 
+@pytest.fixture
+def tos_cubes():
+    """Cubes to test fixes for ``tos``."""
+    time_coord = iris.coords.DimCoord(
+        [0.0004, 1.09776], var_name='time', standard_name='time',
+        units='days since 1850-01-01 00:00:00')
+    lat_coord = iris.coords.DimCoord(
+        [0.0, 1.0], var_name='lat', standard_name='latitude', units='degrees')
+    lon_coord = iris.coords.DimCoord(
+        [0.0, 1.0], var_name='lon', standard_name='longitude', units='degrees')
+    coord_specs = [
+        (time_coord, 0),
+        (lat_coord, 1),
+        (lon_coord, 2),
+    ]
+    tos_cube = iris.cube.Cube(
+        np.ones((2, 2, 2)),
+        var_name='tos',
+        dim_coords_and_dims=coord_specs,
+    )
+    tos_cube.attributes = {}
+    tos_cube.attributes['mipTable'] = 'Omon'
+
+    return iris.cube.CubeList([tos_cube])
+
+
 def test_get_tas_fix():
     """Test getting of fix."""
     fix = Fix.get_fixes('CMIP6', 'CESM2', 'Amon', 'tas')
     assert fix == [Tas(None)]
 
 
+def test_get_tos_fix():
+    """Test getting of fix."""
+    fix = Fix.get_fixes('CMIP6', 'CESM2', 'Amon', 'tos')
+    assert fix == [Tos(None)]
+
+
 def test_tas_fix_metadata(tas_cubes):
     """Test ``fix_metadata`` for ``tas``."""
     for cube in tas_cubes:
@@ -268,9 +324,21 @@ def test_tas_fix_metadata(tas_cubes):
     out_cubes = fix.fix_metadata(tas_cubes)
     assert out_cubes is tas_cubes
     for cube in out_cubes:
+        assert cube.coord("longitude").has_bounds()
+        assert cube.coord("latitude").has_bounds()
         if cube.var_name == 'tas':
             coord = cube.coord('height')
             assert coord == height_coord
         else:
             with pytest.raises(iris.exceptions.CoordinateNotFoundError):
                 cube.coord('height')
+
+
+def test_tos_fix_metadata(tos_cubes):
+    """Test ``fix_metadata`` for ``tos``."""
+    vardef = get_var_info('CMIP6', 'Omon', 'tos')
+    fix = Tos(vardef)
+    out_cubes = fix.fix_metadata(tos_cubes)
+    assert out_cubes is tos_cubes
+    for cube in out_cubes:
+        np.testing.assert_equal(cube.coord("time").points, [0.,  1.1])
diff --git a/tests/integration/cmor/_fixes/cmip6/test_gfdl_esm4.py b/tests/integration/cmor/_fixes/cmip6/test_gfdl_esm4.py
new file mode 100644
index 000000000..ecc8f0179
--- /dev/null
+++ b/tests/integration/cmor/_fixes/cmip6/test_gfdl_esm4.py
@@ -0,0 +1,78 @@
+"""Tests for the fixes of GFDL-ESM4."""
+import os
+
+import iris
+import numpy as np
+import pytest
+from cf_units import Unit
+from netCDF4 import Dataset
+
+from esmvalcore.cmor._fixes.cmip6.gfdl_esm4 import Siconc
+from esmvalcore.cmor.fix import Fix
+from esmvalcore.cmor.table import get_var_info
+
+
+@pytest.fixture
+def siconc_file(tmp_path):
+    """Create netcdf file with similar issues as ``cl``."""
+    nc_path = os.path.join(tmp_path, 'gfdl_esm4_siconc.nc')
+    with Dataset(nc_path, mode='w') as dataset:
+        dataset.createDimension('time', size=1)
+        dataset.createDimension('lat', size=1)
+        dataset.createDimension('lon', size=1)
+
+        # Dimensional variables
+        dataset.createVariable('time', np.float64, dimensions=('time',))
+        dataset.createVariable('lat', np.float64, dimensions=('lat',))
+        dataset.createVariable('lon', np.float64, dimensions=('lon',))
+        dataset.variables['time'][:] = [0.0]
+        dataset.variables['time'].standard_name = 'time'
+        dataset.variables['time'].units = 'days since 6543-2-1'
+        dataset.variables['lat'][:] = [-30.0]
+        dataset.variables['lat'].standard_name = 'latitude'
+        dataset.variables['lat'].units = 'degrees_north'
+        dataset.variables['lon'][:] = [30.0]
+        dataset.variables['lon'].standard_name = 'longitude'
+        dataset.variables['lon'].units = 'degrees_east'
+        dataset.createVariable('siconc', np.float64,
+                               dimensions=('time', 'lat', 'lon'))
+        dataset.variables['siconc'][:] = 22.
+        dataset.variables['siconc'].standard_name = 'sea_ice_area_fraction'
+        dataset.variables['siconc'].units = '%'
+
+    return nc_path
+
+
+def test_get_siconc_fix():
+    """Test getting of fix."""
+    fix = Fix.get_fixes('CMIP6', 'GFDL-ESM4', 'SImon', 'siconc')
+    assert fix == [Siconc(None)]
+
+
+def test_siconc_fix_metadata(siconc_file):
+    """Test ``fix_metadata`` for ``cl``."""
+    cubes = iris.load(siconc_file)
+
+    # Raw cubes
+    assert len(cubes) == 1
+    siconc_cube = cubes[0]
+    assert siconc_cube.var_name == "siconc"
+
+    # Extract siconc cube
+    siconc_cube = cubes.extract_strict('sea_ice_area_fraction')
+    assert not siconc_cube.coords('typesi')
+
+    # Apply fix
+    vardef = get_var_info('CMIP6', 'SImon', 'siconc')
+    fix = Siconc(vardef)
+    fixed_cubes = fix.fix_metadata(cubes)
+    assert len(fixed_cubes) == 1
+    fixed_siconc_cube = fixed_cubes.extract_strict(
+        'sea_ice_area_fraction')
+    fixed_typesi_coord = fixed_siconc_cube.coord('area_type')
+    assert fixed_typesi_coord.points is not None
+    assert fixed_typesi_coord.bounds is None
+    np.testing.assert_equal(fixed_typesi_coord.points,
+                            ['siconc'])
+    np.testing.assert_equal(fixed_typesi_coord.units,
+                            Unit('1'))
diff --git a/tests/integration/cmor/_fixes/cmip6/test_noresm2_lm.py b/tests/integration/cmor/_fixes/cmip6/test_noresm2_lm.py
new file mode 100644
index 000000000..54a87dd84
--- /dev/null
+++ b/tests/integration/cmor/_fixes/cmip6/test_noresm2_lm.py
@@ -0,0 +1,80 @@
+"""Tests for the fixes of GFDL-ESM4."""
+import os
+
+import iris
+import numpy as np
+import pytest
+from netCDF4 import Dataset
+
+from esmvalcore.cmor._fixes.cmip6.noresm2_lm import Siconc
+from esmvalcore.cmor.fix import Fix
+from esmvalcore.cmor.table import get_var_info
+
+
+@pytest.fixture
+def siconc_file(tmp_path):
+    """Create netcdf file."""
+    nc_path = os.path.join(tmp_path, 'noresm2_lm_siconc.nc')
+    with Dataset(nc_path, mode='w') as dataset:
+        dataset.createDimension('time', size=1)
+        dataset.createDimension('lat', size=1)
+        dataset.createDimension('lon', size=1)
+
+        # Dimensional variables
+        dataset.createVariable('time', np.float64, dimensions=('time',))
+        dataset.createVariable('lat', np.float64, dimensions=('lat',))
+        dataset.createVariable('lon', np.float64, dimensions=('lon',))
+        dataset.variables['time'][:] = [0.2]
+        dataset.variables['time'].standard_name = 'time'
+        dataset.variables['time'].units = 'days since 1850-01-01'
+        dataset.variables['lat'][:] = [30.0]
+        dataset.variables['lat'].standard_name = 'latitude'
+        dataset.variables['lat'].units = 'degrees_north'
+        dataset.variables['lon'][:] = [30.0]
+        dataset.variables['lon'].standard_name = 'longitude'
+        dataset.variables['lon'].units = 'degrees_east'
+        dataset.createVariable('siconc', np.float64,
+                               dimensions=('time', 'lat', 'lon'))
+        dataset.variables['siconc'][:] = 22.
+        dataset.variables['siconc'].standard_name = 'sea_ice_area_fraction'
+        dataset.variables['siconc'].units = '%'
+
+    return nc_path
+
+
+def test_get_siconc_fix():
+    """Test getting of fix."""
+    fix = Fix.get_fixes('CMIP6', 'NorESM2-LM', 'SImon', 'siconc')
+    assert fix == [Siconc(None)]
+
+
+def test_siconc_fix_metadata(siconc_file):
+    """Test ``fix_metadata``."""
+    print(siconc_file)
+    cubes = iris.load(siconc_file)
+    for cube in cubes:
+        cube.coord("latitude").bounds = [28.9956255, 32.3445677]
+        cube.coord("longitude").bounds = [28.9956255, 32.3445677]
+
+    # Raw cubes
+    assert len(cubes) == 1
+    siconc_cube = cubes[0]
+    assert siconc_cube.var_name == "siconc"
+
+    # Extract siconc cube
+    siconc_cube = cubes.extract_strict('sea_ice_area_fraction')
+    assert not siconc_cube.coords('typesi')
+
+    # Apply fix
+    vardef = get_var_info('CMIP6', 'SImon', 'siconc')
+    fix = Siconc(vardef)
+    fixed_cubes = fix.fix_metadata(cubes)
+    assert len(fixed_cubes) == 1
+    fixed_siconc_cube = fixed_cubes.extract_strict(
+        'sea_ice_area_fraction')
+    fixed_lon = fixed_siconc_cube.coord('longitude')
+    fixed_lat = fixed_siconc_cube.coord('latitude')
+    assert fixed_lon.bounds is not None
+    assert fixed_lat.bounds is not None
+    np.testing.assert_equal(fixed_lon.bounds, [[28.9956, 32.3446]])
+    np.testing.assert_equal(fixed_lat.bounds, [[28.9956, 32.3446]])
diff --git a/tests/unit/task/test_diagnostic_task.py b/tests/unit/task/test_diagnostic_task.py
index 823e9a580..ebb06d4ad 100644
--- a/tests/unit/task/test_diagnostic_task.py
+++ b/tests/unit/task/test_diagnostic_task.py
@@ -46,7 +46,7 @@ def test_initialize_env(ext, tmp_path, monkeypatch):
 CMD = {
     # ext, profile: expected command
     ('.py', False): ['python'],
-    ('.py', True): ['python', '-m', 'vmprof', '--lines', '-o'],
+    ('.py', True): ['python', '-m', 'vprof', '-o'],
     ('.ncl', False): ['ncl', '-n', '-p'],
     ('.ncl', True): ['ncl', '-n', '-p'],
     ('.R', False): ['Rscript'],
@@ -86,9 +86,10 @@ def test_initialize_cmd(ext_profile, cmd, tmp_path, monkeypatch):
 
     # Append filenames to expected command
     if ext == '.py' and profile:
-        cmd.append(str(run_dir / 'profile.bin'))
+        cmd.append(str(run_dir / 'profile.json'))
+        cmd.append('-c')
+        cmd.append('c')
     cmd.append(str(script))
-
     assert task.cmd == cmd