Skip to content

Cubes won't merge but will concatenate after promoting scalar coordinate to singleton dimension #6790

@david-bentley

Description

@david-bentley

🐛 Bug Report

Loading some NetCDF files fail to merge into a single cube, but promoting the relevant scalar coordinates and concatenating does collapse into a single cube.

How To Reproduce

I have some NetCDF files which I would expect to merge into a single cube when loaded, but the cubes fail to merge. For example

>>> from pathlib import Path

>>> import iris

>>> p = Path("/path/to/files/")
>>> files = list(p.glob("*nc"))

>>> def callback(cube, field, filename):
...     del cube.attributes["history"]

>>> name_con = iris.Constraint(cube_func=lambda cube: cube.name().startswith("air_temperature"))
>>> coord_con = iris.Constraint(cube_func=lambda cube: any(coord.name() == "pressure"
...                                                        for coord in cube.dim_coords))

>>> cubes = iris.load(files, name_con & coord_con, callback=callback)
>>> print(cubes)
0: air_temperature / (K)               (time: 2; pressure: 33; projection_y_coordinate: 970; projection_x_coordinate: 1042)
1: air_temperature / (K)               (pressure: 33; projection_y_coordinate: 970; projection_x_coordinate: 1042)

If I load the cubes individually and then attempt to manually merge them I get a MergeError as you might expect:

>>> # now load raw and attempt to coerce them into a single cube
>>> cubes = iris.load_raw(files, name_con & coord_con, callback=callback)
>>> print(cubes)
0: air_temperature / (K)               (pressure: 33; projection_y_coordinate: 970; projection_x_coordinate: 1042)
1: air_temperature / (K)               (pressure: 33; projection_y_coordinate: 970; projection_x_coordinate: 1042)
2: air_temperature / (K)               (pressure: 33; projection_y_coordinate: 970; projection_x_coordinate: 1042)

>>> # attempt to merge into a single cube
>>> print(cubes.merge_cube())
Traceback (most recent call last):
  ...
  File ".../lib/python3.12/site-packages/iris/cube.py", line 348, in merge_cube
    proto_cube.register(c, error_on_mismatch=True)
  File ".../lib/python3.12/site-packages/iris/_merge.py", line 1301, in register
    match = coord_payload.match_signature(
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../lib/python3.12/site-packages/iris/_merge.py", line 271, in match_signature
    raise iris.exceptions.MergeError(msgs)
iris.exceptions.MergeError: failed to merge into a single cube.
  Coordinates in cube.aux_coords (non-scalar) differ: air_temperature status_flag.

Metadata wise the coordinates look compatible

>>> # examine the status_flag coordinate
>>> for cube in cubes:
...     print(cube.coord("air_temperature status_flag"))
AuxCoord :  air_temperature status_flag / (no_unit)
    points: <lazy>
    shape: (33, 970, 1042)
    dtype: int8
    standard_name: 'air_temperature status_flag'
    var_name: 'flag'
    attributes:
        flag_meanings  'above_surface_pressure below_surface_pressure'
        flag_values    [0 1]
AuxCoord :  air_temperature status_flag / (no_unit)
    points: <lazy>
    shape: (33, 970, 1042)
    dtype: int8
    standard_name: 'air_temperature status_flag'
    var_name: 'flag'
    attributes:
        flag_meanings  'above_surface_pressure below_surface_pressure'
        flag_values    [0 1]
AuxCoord :  air_temperature status_flag / (no_unit)
    points: <lazy>
    shape: (33, 970, 1042)
    dtype: int8
    standard_name: 'air_temperature status_flag'
    var_name: 'flag'
    attributes:
        flag_meanings  'above_surface_pressure below_surface_pressure'
        flag_values    [0 1]

If I promote the relevant scalar coordinate to a singleton dimension (along with other coordinates spanning that dimension) I can then successfully concatenate into a single cube

>>> # attempt to concatenate the cubes by promoting the scalar dimension to a
>>> # singleton dimension
>>> cubes = iris.cube.CubeList(iris.util.new_axis(cube, "time",
...                                               expand_extras=(cube.coord("forecast_period"),
...                                                              cube.coord("air_temperature status_flag")))
...                            for cube in cubes)
>>> print(cubes.concatenate_cube())
air_temperature / (K)               (time: 3; pressure: 33; projection_y_coordinate: 970; projection_x_coordinate: 1042)
    Dimension coordinates:
        time                             x            -                            -                             -
        pressure                         -            x                            -                             -
        projection_y_coordinate          -            -                            x                             -
        projection_x_coordinate          -            -                            -                             x
    Auxiliary coordinates:
        forecast_period                  x            -                            -                             -
        air_temperature status_flag      x            x                            x                             x
    Scalar coordinates:
        forecast_reference_time     2025-10-01 00:00:00
    Attributes:
        Conventions                 'CF-1.7, UKMO-1.0'
        institution                 'Met Office'
        least_significant_digit     np.int64(1)
        mosg__forecast_run_duration 'PT54H'
        mosg__grid_domain           'uk_extended'
        mosg__grid_type             'standard'
        mosg__grid_version          '1.7.0'
        mosg__model_configuration   'uk_det'
        source                      'Met Office Unified Model'
        title                       'UKV Model Forecast on UK 2 km Standard Grid'
        um_version                  '13.1'

which is what I would expect to happen. It is unclear to me why the status_flag coordinate should prevent a merge but not prevent concatenation?

Expected behaviour

On load there would be a single cube with a time dimension of length 3.

Environment

  • OS & Version: RHEL7
  • Iris Version: 3.13.1

Additional context

I get the same result with all of the COMBINE_POLICY options:

# try the different load policies
>>> for policy in ("legacy", "recommended", "default", "comprehensive"):
...     iris.COMBINE_POLICY.set(policy)
...     cubes = iris.load(files, name_con & coord_con, callback=callback)
...     print(policy)
...     print(cubes)
legacy
0: air_temperature / (K)               (time: 2; pressure: 33; projection_y_coordinate: 970; projection_x_coordinate: 1042)
1: air_temperature / (K)               (pressure: 33; projection_y_coordinate: 970; projection_x_coordinate: 1042)
recommended
0: air_temperature / (K)               (time: 2; pressure: 33; projection_y_coordinate: 970; projection_x_coordinate: 1042)
1: air_temperature / (K)               (pressure: 33; projection_y_coordinate: 970; projection_x_coordinate: 1042)
default
0: air_temperature / (K)               (time: 2; pressure: 33; projection_y_coordinate: 970; projection_x_coordinate: 1042)
1: air_temperature / (K)               (pressure: 33; projection_y_coordinate: 970; projection_x_coordinate: 1042)
comprehensive
0: air_temperature / (K)               (time: 2; pressure: 33; projection_y_coordinate: 970; projection_x_coordinate: 1042)
1: air_temperature / (K)               (pressure: 33; projection_y_coordinate: 970; projection_x_coordinate: 1042)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    Status

    No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions