Iris cube attribute 'grid_staggering' an issue with soil ancillary generation

Hi all.

TLDR; Does anyone know what the iris cube attribute grid_staggering (which appears to have integer values) refers to? I came across an error related to this attribute which caused some dramas. I thought I’d share it in case it pops up again.

Details :

I’m building a regional ancillary suite for a coupled model. This is mostly the same as the RNS ancillary suite, except that we pass it a target land-sea mask (created in regional MOM6) and enforce the ancillaries to respect this land-sea definition.

I came across a strange ‘feature’ which I thought I’d share.

The task ancil_soil_albedo runs in two steps:

  1. Run ancil_soil_albedo.py
  2. Run a second script append.py which combines various soil cubes into a single ancillary file.

The ‘append’ step failed with:

RuntimeError: Cubes provided are defined with different grid staggering: ([6, 6, 6, 6, 6, 6, 6, 6, 3, 6, 6, 6, 6, 6])

Which was a weird error. The script append.py is very simple

#!/usr/bin/env python
"""
The CAP reads specifically 10 fields from the input file when generating the
snow ancillaries (ancilSmcsnow).  There is more than 10 fields present in the
soils ancillary which means that ancilSmcsnow is dependent on field order...
We move those fields it doesn't need to the end of the file to avoid a
segmentation fault.

"""
import warnings
import ants
import ants.io.save as save

def main(filenames, output):
    cubes = ants.load(filenames)

    # Sort by the following stash items (lbuser4).
    items = [40, 41, 43, 207, 47, 44, 46, 48, 220, 223, 8, 418, 419, 420]
    end = len(items) + 1
    
    def sortme(cube):
        stash = cube.attributes['STASH']
        try:
            order = items.index(stash.item)
        except ValueError:
            warnings.warn('Was this stash expected?? {}'.format(stash))
        return order
    cubes = sorted(cubes, key=sortme)
    save.ancil(cubes, output)

if __name__ == '__main__':
    parser = ants.AntsArgParser()
    args = parser.parse_args()
    main(args.sources, args.output)

The comments of this function suggests the RAS suite is using it for a task it wasn’t designed for (what could go wrong, eh?).

Here are the offending cubes:

In [160]: cubes
Out[160]: 
[<iris 'Cube' of volume_fraction_of_condensed_water_in_soil_at_wilting_point / (1) (latitude: 50; longitude: 50)>,
 <iris 'Cube' of volume_fraction_of_condensed_water_in_soil_at_critical_point / (1) (latitude: 50; longitude: 50)>,
 <iris 'Cube' of soil_porosity / (1) (latitude: 50; longitude: 50)>,
 <iris 'Cube' of m01s00i207 / (unknown) (latitude: 50; longitude: 50)>,
 <iris 'Cube' of soil_thermal_conductivity / (W m-1 K-1) (latitude: 50; longitude: 50)>,
 <iris 'Cube' of soil_hydraulic_conductivity_at_saturation / (m s-1) (latitude: 50; longitude: 50)>,
 <iris 'Cube' of soil_thermal_capacity / (J kg-1 K-1) (latitude: 50; longitude: 50)>,
 <iris 'Cube' of soil_suction_at_saturation / (Pa) (latitude: 50; longitude: 50)>,
 <iris 'Cube' of soil_albedo / (1) (latitude: 50; longitude: 50)>,
 <iris 'Cube' of soil_carbon_content / (kg m-2) (latitude: 50; longitude: 50)>,
 <iris 'Cube' of m01s00i008 / (unknown) (latitude: 50; longitude: 50)>,
 <iris 'Cube' of volume_fraction_of_clay_in_soil / (m3 m-3) (latitude: 50; longitude: 50)>,
 <iris 'Cube' of volume_fraction_of_silt_in_soil / (m3 m-3) (latitude: 50; longitude: 50)>,
 <iris 'Cube' of volume_fraction_of_sand_in_soil / (m3 m-3) (latitude: 50; longitude: 50)>]

And their attributes

In [159]: [ cube.attributes for cube in cubes]
Out[159]: 
[CubeAttrsDict(globals={}, locals={'STASH': STASH(model=1, section=0, item=40), 'grid_staggering': 6}),
 CubeAttrsDict(globals={}, locals={'STASH': STASH(model=1, section=0, item=41), 'grid_staggering': 6}),
 CubeAttrsDict(globals={}, locals={'STASH': STASH(model=1, section=0, item=43), 'grid_staggering': 6}),
 CubeAttrsDict(globals={}, locals={'STASH': STASH(model=1, section=0, item=207), 'grid_staggering': 6}),
 CubeAttrsDict(globals={}, locals={'STASH': STASH(model=1, section=0, item=47), 'grid_staggering': 6}),
 CubeAttrsDict(globals={}, locals={'STASH': STASH(model=1, section=0, item=44), 'grid_staggering': 6}),
 CubeAttrsDict(globals={}, locals={'STASH': STASH(model=1, section=0, item=46), 'grid_staggering': 6}),
 CubeAttrsDict(globals={}, locals={'STASH': STASH(model=1, section=0, item=48), 'grid_staggering': 6}),
 CubeAttrsDict(globals={}, locals={'source': 'Data from Met Office Unified Model', 'um_version': '6.4', 'STASH': STASH(model=1, section=0, item=220), 'grid_staggering': 6}),
 CubeAttrsDict(globals={}, locals={'STASH': STASH(model=1, section=0, item=223), 'grid_staggering': 6}),
 CubeAttrsDict(globals={}, locals={'STASH': STASH(model=1, section=0, item=8), 'grid_staggering': 6}),
 CubeAttrsDict(globals={}, locals={'STASH': STASH(model=1, section=0, item=418), 'grid_staggering': 6}),
 CubeAttrsDict(globals={}, locals={'STASH': STASH(model=1, section=0, item=419), 'grid_staggering': 6}),
 CubeAttrsDict(globals={}, locals={'STASH': STASH(model=1, section=0, item=420), 'grid_staggering': 6})]

I have no idea what this attribute refers to. I inserted some logic into a local copy of append.py to search for the most common value, and then enforce this value throughout the cube list.

So I got the ancil to generate, but I’m absolutely stumped as to what’s going on here, and I hope it doesn’t come back to bite me at some point in the future.

Seems it’s marking the field as an endgame or new dynamics grid

One of them has U gridpoints at the pole, the other V (don’t ask me which :slight_smile: )

Well I managed to fix this glitch in the (prototype) rCM3 ancillary suite by generating the UM target mask with these global attributes:

                    attrs=dict(Conventions='CF-1.7',
                               grid_staggering=6,
                               um_version='13.5')    

This produces a qrparm.mask_cci file with attributes

CubeAttrsDict(globals={}, locals={'source': 'Data from Met Office Unified Model', 'um_version': '13.5', 'STASH': STASH(model=1, section=0, item=30)})

I got this idea by looking at this warning during various tasks in the suite

UserWarning: Ancillary files do not define the UM version number in the Fixed Length Header. No STASHmaster file loaded:

and examining the master albedo file ancil_master_ants//soil_albedo/classic/v3/soil_albedo.nc which has attributes:

{'grid_staggering': 3,
 'history': '2019-08-14T11:06:08: soil_albedo_preproc.py /cray_hpc/projects/um1/ancil/atmos/master/soil_albedo/classic/v2/qrparm.soil.wsa.bb3.flags /cray_hpc/projects/um1/ancil/atmos/master/soil_albedo/classic/v2/qrparm.soil.wsa.bb3.cmg -o /scratch/cpelley/GL9/preproc/soil_albedo.nc --ants-config /home/h05/cpelley/cylc-run/GL9/work/1/ancil_soil_albedo_preproc/rose-app-run.conf (GL9@128051)',
 'source': 'Data from Met Office Unified Model',
 'um_version': '6.4',
 'Conventions': 'CF-1.5'}

So the ancillary task ancil_soil_albedo now correctly concatenates the local, regridded and masked albedo cube to the soil hydrology cubes.

Evidently the UM ancil tasks and ants libraries have some hidden features which write the UM and/or grid stagger information into the binary header, and this information is used in subsequent tasks.