From 219d4a47b4961053a0b3346591960c84a6b7e2ce Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Thu, 26 Jun 2025 15:44:22 +0100 Subject: [PATCH 01/58] Added ability to load multiple coordinate systems for a cube. --- .../fileformats/_nc_load_rules/actions.py | 85 ++++++++++++++++--- .../fileformats/_nc_load_rules/helpers.py | 51 +++++++++++ lib/iris/fileformats/cf.py | 35 +++++--- lib/iris/fileformats/netcdf/loader.py | 1 + 4 files changed, 149 insertions(+), 23 deletions(-) diff --git a/lib/iris/fileformats/_nc_load_rules/actions.py b/lib/iris/fileformats/_nc_load_rules/actions.py index ee19b54b4b..0199259e1c 100644 --- a/lib/iris/fileformats/_nc_load_rules/actions.py +++ b/lib/iris/fileformats/_nc_load_rules/actions.py @@ -203,7 +203,10 @@ def action_provides_grid_mapping(engine, gridmapping_fact): def build_outer(engine_, cf_var_): coordinate_system = builder(engine_, cf_var_) - engine_.cube_parts["coordinate_system"] = coordinate_system + # We can now handle more than one coordinate_system, so store as dictionary: + engine_.cube_parts["coordinate_systems"][cf_var_.cf_name] = ( + coordinate_system + ) # Part 1 - only building - adding takes place downstream in # helpers.build_and_add_dimension/auxiliary_coordinate(). @@ -218,11 +221,8 @@ def build_outer(engine_, cf_var_): ), ) - # Check there is not an existing one. - # ATM this is guaranteed by the caller, "run_actions". - assert engine.fact_list("grid-type") == [] - - engine.add_fact("grid-type", (grid_mapping_type,)) + # Store grid-mapping name along with grid-type to match them later on + engine.add_fact("grid-type", (var_name, grid_mapping_type)) else: message = "Coordinate system not created. Debug info:\n" @@ -343,7 +343,40 @@ def action_build_dimension_coordinate(engine, providescoord_fact): # Non-conforming lon/lat/projection coords will be classed as # dim-coords by cf.py, but 'action_provides_coordinate' will give them # a coord-type of 'miscellaneous' : hence, they have no coord-system. - coord_system = engine.cube_parts.get("coordinate_system") + # + # At this point, we need to match any "coordinate_system" entries in + # the engine to the coord we are building. There are a couple of cases here: + # 1. Simple `grid_mapping = crs` is used, in which case + # we should just apply that mapping to all dim coords. + # 2. Extended `grid_mapping = crs: coord1 coord2 crs: coord3 coord4` + # is used in which case we need to match the crs to the coord here. + + # We can have multiple coordinate_system, so now stored as a list (note plural key) + coord_systems = engine.cube_parts.get("coordinate_systems") + + # parse the grid_mapping attribute to get coord_system -> coordinate mappings + attr_grid_mapping = getattr(engine.cf_var, "grid_mapping") + cs_mappings = hh._parse_extened_grid_mapping(attr_grid_mapping) + + coord_system = None + + # Simple `grid_mapping = "crs"` + # Only one coord_system will be present and cs_grid_mapping will + # contain no coordinate references (set to None). + if len(coord_systems) == 1 and cs_mappings[0][1] is None: + # Only one grid mapping - apply it. + coord_system = list(coord_systems.values())[0] + cs_name = cs_mappings[0][0] + + # Extended `grid_mapping = "crs: coord1 coord2 crs: coord3 coord4"` + # We need to search for coord system that references our coordinate. + else: + for name, ref_coords in cs_mappings: + if cf_var.cf_name in ref_coords: + cs_name = name + coord_system = coord_systems[cs_name] + break + # Translate the specific grid-mapping type to a grid-class if coord_system is None: succeed = True @@ -352,8 +385,13 @@ def action_build_dimension_coordinate(engine, providescoord_fact): # Get a grid-class from the grid-type # i.e. one of latlon/rotated/projected, as for coord_grid_class. gridtypes_factlist = engine.fact_list("grid-type") - (gridtypes_fact,) = gridtypes_factlist # only 1 fact - (cs_gridtype,) = gridtypes_fact # fact contains 1 term + + # potentially multiple grid-type facts; find one for CRS varname + cs_gridtype = None + for fact_cs_name, fact_cs_type in gridtypes_factlist: + if fact_cs_name == cs_name: + cs_gridtype = fact_cs_type + if cs_gridtype == "latitude_longitude": cs_gridclass = "latlon" elif cs_gridtype == "rotated_latitude_longitude": @@ -446,6 +484,7 @@ def action_build_auxiliary_coordinate(engine, auxcoord_fact): """Convert a CFAuxiliaryCoordinateVariable into a cube aux-coord.""" (var_name,) = auxcoord_fact rule_name = "fc_build_auxiliary_coordinate" + cf_var = engine.cf_var.cf_group[var_name] # Identify any known coord "type" : latitude/longitude/time/time_period # If latitude/longitude, this sets the standard_name of the built AuxCoord @@ -473,8 +512,29 @@ def action_build_auxiliary_coordinate(engine, auxcoord_fact): if coord_type: rule_name += f"_{coord_type}" + # Check if we have a coord_system specified for this coordinate. + # (Only possible via extended grid_mapping attribute) + coord_systems = engine.cube_parts.get("coordinate_systems") + + # get grid_mapping from data variable attribute and parse it + grid_mapping_attr = getattr(engine.cf_var, "grid_mapping") + cs_mappings = hh._parse_extened_grid_mapping(grid_mapping_attr) + + if len(coord_systems) == 1 and cs_mappings[0][1] is None: + # Simple grid_mapping - doesn't apply to AuxCoords (we need an explicit mapping) + coord_system = None + else: + # Extended grid_mapping + coord_system = None + for crs_name, coords in cs_mappings: + if cf_var.cf_name in coords: + coord_system = coord_systems[crs_name] + break + cf_var = engine.cf_var.cf_group.auxiliary_coordinates[var_name] - hh.build_and_add_auxiliary_coordinate(engine, cf_var, coord_name=coord_name) + hh.build_and_add_auxiliary_coordinate( + engine, cf_var, coord_name=coord_name, coord_system=coord_system + ) return rule_name @@ -615,10 +675,9 @@ def run_actions(engine): # default (all cubes) action, always runs action_default(engine) # This should run the default rules. - # deal with grid-mappings + # deal with grid-mappings; potentially multiple mappings if extended grid_mapping used. grid_mapping_facts = engine.fact_list("grid_mapping") - # For now, there should be at most *one* of these. - assert len(grid_mapping_facts) in (0, 1) + for grid_mapping_fact in grid_mapping_facts: action_provides_grid_mapping(engine, grid_mapping_fact) diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index fc47625943..033246fce9 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -143,6 +143,33 @@ CF_GRID_MAPPING_OBLIQUE = "oblique_mercator" CF_GRID_MAPPING_ROTATED_MERCATOR = "rotated_mercator" +# +# Regex for parsing grid_mapping (extended format) +# +# (\w+): # Matches ':' and stores in CAPTURE GROUP 1 +# ( # CAPTURE GROUP 2 for capturing multiple coords +# (?: # Non-capturing group for composing match +# \s+ # Matches one or more blank characters +# (?!\w+:) # Negative look-ahead: don't match followed by colon +# (\w+) # Matches a +# )+ # Repeats non-capturing group at least once. +# ) # End of CAPTURE GROUP 2 +# _GRID_MAPPING_PARSE = re.compile(r"(\w+):((?: +(?!\w+:)(\w+))+)+") +_GRID_MAPPING_PARSE_EXTENDED = re.compile( + r""" + (\w+): + ( + (?: + \s+ + (?!\w+:) + (\w+) + )+ + )+ + """, + re.VERBOSE, +) +_GRID_MAPPING_PARSE_SIMPLE = re.compile(r"^\w+$") + # # CF Attribute Names. # @@ -1936,6 +1963,30 @@ def is_grid_mapping(engine, cf_name, grid_mapping): return is_valid +################################################################################ +def _parse_extened_grid_mapping(grid_mapping): + """Parse `grid_mapping` attribute and return list of coordinate system variables and associated coords.""" + # Handles extended grid_mapping too. Possibilities: + # grid_mapping = "crs" : simple mapping; a single variable name with no coords + # grid_mapping = "crs: lat lon" : extended mapping; a variable name and list of coords + # grid_mapping = "crs: lat lon other: var1 var2" : multiple extended mappings + + # TODO(ChrisB): TESTS!! + + # try simple mapping first + if _GRID_MAPPING_PARSE_SIMPLE.match(grid_mapping): + return [(grid_mapping, None)] # simple single grid mapping variable + + # Try extended mapping: + mappings = _GRID_MAPPING_PARSE_EXTENDED.findall(grid_mapping) + if mappings is None: + raise Exception("Bad grid_mapping attribute: %r" % grid_mapping) + + # split second match group into list of coordinates: + mappings = [(m[0], m[1].split()) for m in mappings] + return mappings + + ################################################################################ def _is_rotated(engine, cf_name, cf_attr_value): """Determine whether the CF coordinate variable is rotated.""" diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index e69d686c0f..470d293555 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -24,6 +24,7 @@ import numpy as np import numpy.ma as ma +import iris.fileformats._nc_load_rules.helpers as hh from iris.fileformats.netcdf import _thread_safe_nc from iris.mesh.components import Connectivity import iris.util @@ -666,17 +667,31 @@ def identify(cls, variables, ignore=None, target=None, warn=True): if nc_var_att is not None: name = nc_var_att.strip() - if name not in ignore: - if name not in variables: - if warn: - message = "Missing CF-netCDF grid mapping variable %r, referenced by netCDF variable %r" - warnings.warn( - message % (name, nc_var_name), - category=iris.warnings.IrisCfMissingVarWarning, - ) - else: - result[name] = CFGridMappingVariable(name, variables[name]) + # parse the grid_mappings + mappings = hh._parse_extened_grid_mapping(name) + for name, coords in mappings: + if name not in ignore: + if name not in variables: + if warn: + message = "Missing CF-netCDF grid mapping variable %r, referenced by netCDF variable %r" + warnings.warn( + message % (name, nc_var_name), + category=iris.warnings.IrisCfMissingVarWarning, + ) + else: + # For extended grid_mapping, also check coord references exist: + if coords: + for coord_name in coords: + if coord_name not in variables: + message = "Missing CF-netCDF coordinate variable %r (associated with grid mapping variable %r), referenced by netCDF variable %r" + warnings.warn( + message % (coord_name, name, nc_var_name), + category=iris.warnings.IrisCfMissingVarWarning, + ) + # TODO: Question: A missing coord reference will not stop the coord_system from + # being added as a CFGridMappingVariable. Is this ok? + result[name] = CFGridMappingVariable(name, variables[name]) return result diff --git a/lib/iris/fileformats/netcdf/loader.py b/lib/iris/fileformats/netcdf/loader.py index b7895c4dda..22b9e195b4 100644 --- a/lib/iris/fileformats/netcdf/loader.py +++ b/lib/iris/fileformats/netcdf/loader.py @@ -79,6 +79,7 @@ def _assert_case_specific_facts(engine, cf, cf_group): engine.cube_parts["coordinates"] = [] engine.cube_parts["cell_measures"] = [] engine.cube_parts["ancillary_variables"] = [] + engine.cube_parts["coordinate_systems"] = {} # Assert facts for CF coordinates. for cf_name in cf_group.coordinates.keys(): From b71beaf22bbd52a31fc7e027cc38738b212fdc01 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Thu, 26 Jun 2025 16:18:55 +0100 Subject: [PATCH 02/58] Check for existence of grid_mapping attr --- .../fileformats/_nc_load_rules/actions.py | 67 ++++++++++--------- 1 file changed, 34 insertions(+), 33 deletions(-) diff --git a/lib/iris/fileformats/_nc_load_rules/actions.py b/lib/iris/fileformats/_nc_load_rules/actions.py index 0199259e1c..f0722fbf46 100644 --- a/lib/iris/fileformats/_nc_load_rules/actions.py +++ b/lib/iris/fileformats/_nc_load_rules/actions.py @@ -353,29 +353,29 @@ def action_build_dimension_coordinate(engine, providescoord_fact): # We can have multiple coordinate_system, so now stored as a list (note plural key) coord_systems = engine.cube_parts.get("coordinate_systems") - - # parse the grid_mapping attribute to get coord_system -> coordinate mappings - attr_grid_mapping = getattr(engine.cf_var, "grid_mapping") - cs_mappings = hh._parse_extened_grid_mapping(attr_grid_mapping) - coord_system = None - # Simple `grid_mapping = "crs"` - # Only one coord_system will be present and cs_grid_mapping will - # contain no coordinate references (set to None). - if len(coord_systems) == 1 and cs_mappings[0][1] is None: - # Only one grid mapping - apply it. - coord_system = list(coord_systems.values())[0] - cs_name = cs_mappings[0][0] - - # Extended `grid_mapping = "crs: coord1 coord2 crs: coord3 coord4"` - # We need to search for coord system that references our coordinate. - else: - for name, ref_coords in cs_mappings: - if cf_var.cf_name in ref_coords: - cs_name = name - coord_system = coord_systems[cs_name] - break + # parse the grid_mapping attribute to get coord_system -> coordinate mappings + attr_grid_mapping = getattr(engine.cf_var, "grid_mapping", None) + if attr_grid_mapping: + cs_mappings = hh._parse_extened_grid_mapping(attr_grid_mapping) + + # Simple `grid_mapping = "crs"` + # Only one coord_system will be present and cs_grid_mapping will + # contain no coordinate references (set to None). + if len(coord_systems) == 1 and cs_mappings[0][1] is None: + # Only one grid mapping - apply it. + coord_system = list(coord_systems.values())[0] + cs_name = cs_mappings[0][0] + + # Extended `grid_mapping = "crs: coord1 coord2 crs: coord3 coord4"` + # We need to search for coord system that references our coordinate. + else: + for name, ref_coords in cs_mappings: + if cf_var.cf_name in ref_coords: + cs_name = name + coord_system = coord_systems[cs_name] + break # Translate the specific grid-mapping type to a grid-class if coord_system is None: @@ -515,21 +515,22 @@ def action_build_auxiliary_coordinate(engine, auxcoord_fact): # Check if we have a coord_system specified for this coordinate. # (Only possible via extended grid_mapping attribute) coord_systems = engine.cube_parts.get("coordinate_systems") + coord_system = None # get grid_mapping from data variable attribute and parse it - grid_mapping_attr = getattr(engine.cf_var, "grid_mapping") - cs_mappings = hh._parse_extened_grid_mapping(grid_mapping_attr) + grid_mapping_attr = getattr(engine.cf_var, "grid_mapping", None) + if grid_mapping_attr: + cs_mappings = hh._parse_extened_grid_mapping(grid_mapping_attr) - if len(coord_systems) == 1 and cs_mappings[0][1] is None: - # Simple grid_mapping - doesn't apply to AuxCoords (we need an explicit mapping) - coord_system = None - else: - # Extended grid_mapping - coord_system = None - for crs_name, coords in cs_mappings: - if cf_var.cf_name in coords: - coord_system = coord_systems[crs_name] - break + if len(coord_systems) == 1 and cs_mappings[0][1] is None: + # Simple grid_mapping - doesn't apply to AuxCoords (we need an explicit mapping) + pass + else: + # Extended grid_mapping + for crs_name, coords in cs_mappings: + if cf_var.cf_name in coords: + coord_system = coord_systems[crs_name] + break cf_var = engine.cf_var.cf_group.auxiliary_coordinates[var_name] hh.build_and_add_auxiliary_coordinate( From 94585fa3da505011db95944f1e181ffec1f83353 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 27 Jun 2025 09:51:47 +0100 Subject: [PATCH 03/58] Fix for failing tests for case where no coord systems found --- .../fileformats/_nc_load_rules/actions.py | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/lib/iris/fileformats/_nc_load_rules/actions.py b/lib/iris/fileformats/_nc_load_rules/actions.py index f0722fbf46..18a31405be 100644 --- a/lib/iris/fileformats/_nc_load_rules/actions.py +++ b/lib/iris/fileformats/_nc_load_rules/actions.py @@ -355,27 +355,29 @@ def action_build_dimension_coordinate(engine, providescoord_fact): coord_systems = engine.cube_parts.get("coordinate_systems") coord_system = None - # parse the grid_mapping attribute to get coord_system -> coordinate mappings - attr_grid_mapping = getattr(engine.cf_var, "grid_mapping", None) - if attr_grid_mapping: - cs_mappings = hh._parse_extened_grid_mapping(attr_grid_mapping) - - # Simple `grid_mapping = "crs"` - # Only one coord_system will be present and cs_grid_mapping will - # contain no coordinate references (set to None). - if len(coord_systems) == 1 and cs_mappings[0][1] is None: - # Only one grid mapping - apply it. - coord_system = list(coord_systems.values())[0] - cs_name = cs_mappings[0][0] - - # Extended `grid_mapping = "crs: coord1 coord2 crs: coord3 coord4"` - # We need to search for coord system that references our coordinate. - else: - for name, ref_coords in cs_mappings: - if cf_var.cf_name in ref_coords: - cs_name = name - coord_system = coord_systems[cs_name] - break + if len(coord_systems): + # Find which coord system applies to this coordinate. + # Parse the grid_mapping attribute to get coord_system -> coordinate mappings + attr_grid_mapping = getattr(engine.cf_var, "grid_mapping", None) + if attr_grid_mapping: + cs_mappings = hh._parse_extened_grid_mapping(attr_grid_mapping) + + # Simple `grid_mapping = "crs"` + # Only one coord_system will be present and cs_grid_mapping will + # contain no coordinate references (set to None). + if len(coord_systems) == 1 and cs_mappings[0][1] is None: + # Only one grid mapping - apply it. + coord_system = list(coord_systems.values())[0] + cs_name = cs_mappings[0][0] + + # Extended `grid_mapping = "crs: coord1 coord2 crs: coord3 coord4"` + # We need to search for coord system that references our coordinate. + else: + for name, ref_coords in cs_mappings: + if cf_var.cf_name in ref_coords: + cs_name = name + coord_system = coord_systems[cs_name] + break # Translate the specific grid-mapping type to a grid-class if coord_system is None: From b57e2688aeb9e140e187dee91334c3ab7e010903 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 27 Jun 2025 12:01:20 +0100 Subject: [PATCH 04/58] Some new tests for extended grid mapping `in test__grid_mappings.py` --- .../actions/test__grid_mappings.py | 91 ++++++++++++++++++- 1 file changed, 90 insertions(+), 1 deletion(-) diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py index 8731049ea8..637af2210b 100644 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py @@ -27,6 +27,7 @@ def _make_testcase_cdl( mapping_missingradius=False, mapping_type_name=None, mapping_scalefactor=None, + phenom_grid_mapping="grid", yco_values=None, xco_name=None, yco_name=None, @@ -226,7 +227,7 @@ def _make_testcase_cdl( double phenom({ydim_name}, {xdim_name}) ; phenom:standard_name = "air_temperature" ; phenom:units = "K" ; - phenom:grid_mapping = "grid" ; + phenom:grid_mapping = "{phenom_grid_mapping}" ; {phenom_coords_string} double yco({ydim_name}) ; yco:axis = "Y" ; @@ -755,6 +756,45 @@ def test_mapping__mismatch__nonll_coords_missing_system(self): ) self.check_result(result, cube_no_cs=True, xco_stdname=False, yco_stdname=False) + def test_extended_mapping_basic_latlon(self): + # A basic reference example with a lat-long grid, but using extended + # grid mapping syntax on phenomenon. + # + # Rules Triggered: + # 001 : fc_default + # 002 : fc_provides_grid_mapping_(latitude_longitude) + # 003 : fc_provides_coordinate_(latitude) + # 004 : fc_provides_coordinate_(longitude) + # 005 : fc_build_coordinate_(latitude) + # 006 : fc_build_coordinate_(longitude) + # Notes: + # * grid-mapping identified : regular latlon + # * dim-coords identified : lat+lon + # * coords built : standard latlon (with latlon coord-system) + result = self.run_testcase(phenom_grid_mapping="grid: yco xco") + self.check_result(result) + + def test_extended_mapping_basic_latlon_missing_coords(self): + # A basic reference example with a lat-long grid, but using extended + # grid mapping syntax on phenomenon. + # + # Rules Triggered: + # 001 : fc_default + # 002 : fc_provides_grid_mapping_(latitude_longitude) + # 003 : fc_provides_coordinate_(latitude) + # 004 : fc_provides_coordinate_(longitude) + # 005 : fc_build_coordinate_(latitude) + # 006 : fc_build_coordinate_(longitude) + # Notes: + # * grid-mapping identified : regular latlon + # * dim-coords identified : lat+lon + # * coords built : standard latlon (with latlon coord-system) + result = self.run_testcase( + phenom_grid_mapping="grid: yco bad_coord", + warning_regex="Missing CF-netCDF coordinate variable 'bad_coord'", + ) + self.check_result(result, xco_no_cs=True) + class Test__aux_latlons(Mixin__grid_mapping, tests.IrisTest): # Testcases for translating auxiliary latitude+longitude variables @@ -838,6 +878,55 @@ def test_aux_lat_rotated(self): ) self.check_result(result, yco_is_aux=True, yco_no_cs=True) + def test_extended_grid_mapping_aux_lon(self): + # Change the name of xdim, and put xco on the coords list. + # Uses extended grid mapping syntax. + # In this case, the Aux coord WILL have a coordinate system + # as extended grid mapping allows for specification of + # explicit coordinate_systems for individual coordinate. + # + # Rules Triggered: + # 001 : fc_default + # 002 : fc_provides_grid_mapping_(latitude_longitude) + # 003 : fc_provides_coordinate_(latitude) + # 004 : fc_build_coordinate_(latitude) + # 005 : fc_build_auxiliary_coordinate_longitude + result = self.run_testcase( + xco_is_dim=False, phenom_grid_mapping="grid: yco xco" + ) + self.check_result(result, xco_is_aux=True, xco_no_cs=False) + + def test_extended_grid_mapping_aux_lat(self): + # As previous, but with the Y coord. + # Uses extended grid mapping syntax + # + # Rules Triggered: + # 001 : fc_default + # 002 : fc_provides_grid_mapping_(latitude_longitude) + # 003 : fc_provides_coordinate_(longitude) + # 004 : fc_build_coordinate_(longitude) + # 005 : fc_build_auxiliary_coordinate_latitude + result = self.run_testcase( + yco_is_dim=False, phenom_grid_mapping="grid: yco xco" + ) + self.check_result(result, yco_is_aux=True, yco_no_cs=False) + + def test_extended_grid_mapping_aux_lat_and_lon(self): + # Make *both* X and Y coords into aux-coords. + # Uses extended grid mapping syntax; allows coord_system + # to be added to an AuxCoord, so cube.coord_system() returns + # a valid coordinate_system. + # + # Rules Triggered: + # 001 : fc_default + # 002 : fc_provides_grid_mapping_(latitude_longitude) + # 003 : fc_build_auxiliary_coordinate_longitude + # 004 : fc_build_auxiliary_coordinate_latitude + result = self.run_testcase( + xco_is_dim=False, yco_is_dim=False, phenom_grid_mapping="grid: yco xco" + ) + self.check_result(result, xco_is_aux=True, yco_is_aux=True, cube_no_cs=False) + class Test__nondimcoords(Mixin__grid_mapping, tests.IrisTest): @classmethod From 03a56542f2183a15a8dcba32cb45c4aea410d474 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 27 Jun 2025 13:03:38 +0100 Subject: [PATCH 05/58] Added some validators to the grid_mapping parser --- .../fileformats/_nc_load_rules/helpers.py | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index 033246fce9..67dd9924b0 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -169,7 +169,17 @@ re.VERBOSE, ) _GRID_MAPPING_PARSE_SIMPLE = re.compile(r"^\w+$") - +_GRID_MAPPING_VALIDATORS = ( + ( + re.compile(r"\w+: +\w+:"), + "`:` identifier followed immediately by another `:` identifier", + ), + (re.compile(r"\w+: *$"), "`:` is empty - missing coordinate list"), + ( + re.compile(r"^\w+ +\w+"), + "Multiple coordinates found without `:` identifier", + ), +) # # CF Attribute Names. # @@ -1978,9 +1988,17 @@ def _parse_extened_grid_mapping(grid_mapping): return [(grid_mapping, None)] # simple single grid mapping variable # Try extended mapping: + # 1. Run validators to check for invalid expressions: + for v_re, v_msg in _GRID_MAPPING_VALIDATORS: + if len(match := v_re.findall(grid_mapping)): + msg = f"Invalid syntax in extended grid_mapping: {grid_mapping!r}\n{v_msg} : {match}" + raise iris.exceptions.IrisError(msg) # TODO: Better Exception type + + # 2. Parse grid_mapping into list of [cs, (coords, ...)]: mappings = _GRID_MAPPING_PARSE_EXTENDED.findall(grid_mapping) - if mappings is None: - raise Exception("Bad grid_mapping attribute: %r" % grid_mapping) + if len(mappings) == 0: + msg = f"Failed to parse grid_mapping: {grid_mapping!r}" + raise iris.exceptions.IrisError(msg) # TODO: Better exception type # split second match group into list of coordinates: mappings = [(m[0], m[1].split()) for m in mappings] From 92ed35aa615e19e8871d895308491f76b6d391a1 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 27 Jun 2025 17:44:59 +0100 Subject: [PATCH 06/58] Save cubes with multiple coord_systems using extended grid mapping syntax --- lib/iris/fileformats/netcdf/saver.py | 44 +++++++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 4a2474ba9b..18013db908 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -1864,8 +1864,20 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube): None """ - cs = cube.coord_system("CoordSystem") - if cs is not None: + # TODO(CB): We need ALL coord systems here, not just the fist one found. + # cs = cube.coord_system("CoordSystem") + + # TODO(CB): This is workaround code (gets _unique_ coord systems): + # TODO(CB): Put this code in new cube.coord_systems() function, or modify cube.coord_system() + # coord_systems = [coord.coord_system for coord in cube.coords() if coord.coord_system] + coord_systems = [] + for coord in cube.coords(): + if coord.coord_system and coord.coord_system not in coord_systems: + coord_systems.append(coord.coord_system) + + grid_mappings = [] + + for cs in coord_systems: # Grid var not yet created? if cs not in self._coord_systems: while cs.grid_mapping_name in self._dataset.variables: @@ -2083,8 +2095,32 @@ def add_ellipsoid(ellipsoid): self._coord_systems.append(cs) - # Refer to grid var - _setncattr(cf_var_cube, "grid_mapping", cs.grid_mapping_name) + # create grid mapping string: + coords = cube.coords(coord_system=cs) + # TODO: How do we sort these coords? + # For DimCoords, we can use the dimension order, but what + # about AuxCoords that could be 2D? + + # prefer netCDF variable name, if exists, else default ro coord.name() + coord_string = " ".join( + [coord.var_name if coord.var_name else coord.name() for coord in coords] + ) + grid_mappings.append((cs.grid_mapping_name, coord_string)) + + # Refer to grid var + # TODO: Future flag for extended grid_mapping syntax? + # For now, if only one coord_system, write out in simple form. + # Use extended form for multiple coord_systems + if len(grid_mappings): + if len(grid_mappings) > 1: + # TODO: Check future flag? Warn if not set? + grid_mapping = " ".join( + f"{cs_name}: {cs_coords}" for cs_name, cs_coords in grid_mappings + ) + else: + grid_mapping = grid_mappings[0][0] # just the cs_name + + _setncattr(cf_var_cube, "grid_mapping", grid_mapping) def _create_cf_data_variable( self, From 9a50380f5df7c0aa73949144baaa3daa0bd41b85 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 27 Jun 2025 20:00:46 +0100 Subject: [PATCH 07/58] Typo in function name --- lib/iris/fileformats/_nc_load_rules/actions.py | 4 ++-- lib/iris/fileformats/_nc_load_rules/helpers.py | 2 +- lib/iris/fileformats/cf.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/iris/fileformats/_nc_load_rules/actions.py b/lib/iris/fileformats/_nc_load_rules/actions.py index 18a31405be..b270468596 100644 --- a/lib/iris/fileformats/_nc_load_rules/actions.py +++ b/lib/iris/fileformats/_nc_load_rules/actions.py @@ -360,7 +360,7 @@ def action_build_dimension_coordinate(engine, providescoord_fact): # Parse the grid_mapping attribute to get coord_system -> coordinate mappings attr_grid_mapping = getattr(engine.cf_var, "grid_mapping", None) if attr_grid_mapping: - cs_mappings = hh._parse_extened_grid_mapping(attr_grid_mapping) + cs_mappings = hh._parse_extended_grid_mapping(attr_grid_mapping) # Simple `grid_mapping = "crs"` # Only one coord_system will be present and cs_grid_mapping will @@ -522,7 +522,7 @@ def action_build_auxiliary_coordinate(engine, auxcoord_fact): # get grid_mapping from data variable attribute and parse it grid_mapping_attr = getattr(engine.cf_var, "grid_mapping", None) if grid_mapping_attr: - cs_mappings = hh._parse_extened_grid_mapping(grid_mapping_attr) + cs_mappings = hh._parse_extended_grid_mapping(grid_mapping_attr) if len(coord_systems) == 1 and cs_mappings[0][1] is None: # Simple grid_mapping - doesn't apply to AuxCoords (we need an explicit mapping) diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index 67dd9924b0..bd6a930c1a 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -1974,7 +1974,7 @@ def is_grid_mapping(engine, cf_name, grid_mapping): ################################################################################ -def _parse_extened_grid_mapping(grid_mapping): +def _parse_extended_grid_mapping(grid_mapping): """Parse `grid_mapping` attribute and return list of coordinate system variables and associated coords.""" # Handles extended grid_mapping too. Possibilities: # grid_mapping = "crs" : simple mapping; a single variable name with no coords diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 470d293555..6daf5a8fe2 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -668,7 +668,7 @@ def identify(cls, variables, ignore=None, target=None, warn=True): name = nc_var_att.strip() # parse the grid_mappings - mappings = hh._parse_extened_grid_mapping(name) + mappings = hh._parse_extended_grid_mapping(name) for name, coords in mappings: if name not in ignore: From 2ac5c96ea7639a779883b9c09d48d1fb8b3fb58d Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 27 Jun 2025 20:13:06 +0100 Subject: [PATCH 08/58] Ignore for ruff fmt --- lib/iris/fileformats/_nc_load_rules/helpers.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index bd6a930c1a..5b362a8e7c 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -169,12 +169,15 @@ re.VERBOSE, ) _GRID_MAPPING_PARSE_SIMPLE = re.compile(r"^\w+$") -_GRID_MAPPING_VALIDATORS = ( +_GRID_MAPPING_VALIDATORS = ( # fmt: skip ( re.compile(r"\w+: +\w+:"), "`:` identifier followed immediately by another `:` identifier", ), - (re.compile(r"\w+: *$"), "`:` is empty - missing coordinate list"), + ( + re.compile(r"\w+: *$"), + "`:` is empty - missing coordinate list", + ), ( re.compile(r"^\w+ +\w+"), "Multiple coordinates found without `:` identifier", From 6beb20d686d423ae0ae3ab81d40636ee17b007aa Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 27 Jun 2025 23:38:00 +0100 Subject: [PATCH 09/58] More idiomatic list unpacking syntax --- lib/iris/fileformats/_nc_load_rules/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/fileformats/_nc_load_rules/actions.py b/lib/iris/fileformats/_nc_load_rules/actions.py index b270468596..10508e493c 100644 --- a/lib/iris/fileformats/_nc_load_rules/actions.py +++ b/lib/iris/fileformats/_nc_load_rules/actions.py @@ -367,7 +367,7 @@ def action_build_dimension_coordinate(engine, providescoord_fact): # contain no coordinate references (set to None). if len(coord_systems) == 1 and cs_mappings[0][1] is None: # Only one grid mapping - apply it. - coord_system = list(coord_systems.values())[0] + (coord_system,) = coord_systems.values() cs_name = cs_mappings[0][0] # Extended `grid_mapping = "crs: coord1 coord2 crs: coord3 coord4"` From 08b8985f071ab912bc03627e1d349aabb3642149 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Tue, 1 Jul 2025 16:07:51 +0100 Subject: [PATCH 10/58] Made return value of _parse_grid_mapping a dict. Added type hints. --- .../fileformats/_nc_load_rules/actions.py | 10 ++--- .../fileformats/_nc_load_rules/helpers.py | 40 ++++++++++--------- lib/iris/fileformats/cf.py | 2 +- 3 files changed, 27 insertions(+), 25 deletions(-) diff --git a/lib/iris/fileformats/_nc_load_rules/actions.py b/lib/iris/fileformats/_nc_load_rules/actions.py index 10508e493c..2d064671bb 100644 --- a/lib/iris/fileformats/_nc_load_rules/actions.py +++ b/lib/iris/fileformats/_nc_load_rules/actions.py @@ -365,15 +365,15 @@ def action_build_dimension_coordinate(engine, providescoord_fact): # Simple `grid_mapping = "crs"` # Only one coord_system will be present and cs_grid_mapping will # contain no coordinate references (set to None). - if len(coord_systems) == 1 and cs_mappings[0][1] is None: + if len(coord_systems) == 1 and list(cs_mappings.values()) == [None]: # Only one grid mapping - apply it. (coord_system,) = coord_systems.values() - cs_name = cs_mappings[0][0] + (cs_name,) = cs_mappings.keys() # Extended `grid_mapping = "crs: coord1 coord2 crs: coord3 coord4"` # We need to search for coord system that references our coordinate. else: - for name, ref_coords in cs_mappings: + for name, ref_coords in cs_mappings.items(): if cf_var.cf_name in ref_coords: cs_name = name coord_system = coord_systems[cs_name] @@ -524,12 +524,12 @@ def action_build_auxiliary_coordinate(engine, auxcoord_fact): if grid_mapping_attr: cs_mappings = hh._parse_extended_grid_mapping(grid_mapping_attr) - if len(coord_systems) == 1 and cs_mappings[0][1] is None: + if len(coord_systems) == 1 and list(cs_mappings.values()) == [None]: # Simple grid_mapping - doesn't apply to AuxCoords (we need an explicit mapping) pass else: # Extended grid_mapping - for crs_name, coords in cs_mappings: + for crs_name, coords in cs_mappings.items(): if cf_var.cf_name in coords: coord_system = coord_systems[crs_name] break diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index 5b362a8e7c..9d6f0cea9a 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -19,7 +19,7 @@ import contextlib from functools import partial import re -from typing import TYPE_CHECKING, Any, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional import warnings import cf_units @@ -1977,7 +1977,8 @@ def is_grid_mapping(engine, cf_name, grid_mapping): ################################################################################ -def _parse_extended_grid_mapping(grid_mapping): +# TODO(ChrisB): Typing doesn't like : Dict[str, List[str] | None] +def _parse_extended_grid_mapping(grid_mapping: str) -> Dict[str, Any]: """Parse `grid_mapping` attribute and return list of coordinate system variables and associated coords.""" # Handles extended grid_mapping too. Possibilities: # grid_mapping = "crs" : simple mapping; a single variable name with no coords @@ -1988,23 +1989,24 @@ def _parse_extended_grid_mapping(grid_mapping): # try simple mapping first if _GRID_MAPPING_PARSE_SIMPLE.match(grid_mapping): - return [(grid_mapping, None)] # simple single grid mapping variable - - # Try extended mapping: - # 1. Run validators to check for invalid expressions: - for v_re, v_msg in _GRID_MAPPING_VALIDATORS: - if len(match := v_re.findall(grid_mapping)): - msg = f"Invalid syntax in extended grid_mapping: {grid_mapping!r}\n{v_msg} : {match}" - raise iris.exceptions.IrisError(msg) # TODO: Better Exception type - - # 2. Parse grid_mapping into list of [cs, (coords, ...)]: - mappings = _GRID_MAPPING_PARSE_EXTENDED.findall(grid_mapping) - if len(mappings) == 0: - msg = f"Failed to parse grid_mapping: {grid_mapping!r}" - raise iris.exceptions.IrisError(msg) # TODO: Better exception type - - # split second match group into list of coordinates: - mappings = [(m[0], m[1].split()) for m in mappings] + mappings = {grid_mapping: None} # simple single grid mapping variable + else: + # Try extended mapping: + # 1. Run validators to check for invalid expressions: + for v_re, v_msg in _GRID_MAPPING_VALIDATORS: + if len(match := v_re.findall(grid_mapping)): + msg = f"Invalid syntax in extended grid_mapping: {grid_mapping!r}\n{v_msg} : {match}" + raise iris.exceptions.IrisError(msg) # TODO: Better Exception type + + # 2. Parse grid_mapping into list of [cs, (coords, ...)]: + result = _GRID_MAPPING_PARSE_EXTENDED.findall(grid_mapping) + if len(result) == 0: + msg = f"Failed to parse grid_mapping: {grid_mapping!r}" + raise iris.exceptions.IrisError(msg) # TODO: Better exception type + + # split second match group into list of coordinates: + mappings = {r[0]: r[1].split() for r in result} + return mappings diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 6daf5a8fe2..8e5e620a35 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -670,7 +670,7 @@ def identify(cls, variables, ignore=None, target=None, warn=True): # parse the grid_mappings mappings = hh._parse_extended_grid_mapping(name) - for name, coords in mappings: + for name, coords in mappings.items(): if name not in ignore: if name not in variables: if warn: From 001b0b48d1c9e3a9789a7b628d8e99c8ea26c57d Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Tue, 1 Jul 2025 19:36:12 +0100 Subject: [PATCH 11/58] Added link to regex101 playground --- lib/iris/fileformats/_nc_load_rules/helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index 9d6f0cea9a..d414f8a8e4 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -145,6 +145,7 @@ # # Regex for parsing grid_mapping (extended format) +# Link to online regex101 playground: https://regex101.com/r/jFbFLG/1 # # (\w+): # Matches ':' and stores in CAPTURE GROUP 1 # ( # CAPTURE GROUP 2 for capturing multiple coords @@ -154,7 +155,6 @@ # (\w+) # Matches a # )+ # Repeats non-capturing group at least once. # ) # End of CAPTURE GROUP 2 -# _GRID_MAPPING_PARSE = re.compile(r"(\w+):((?: +(?!\w+:)(\w+))+)+") _GRID_MAPPING_PARSE_EXTENDED = re.compile( r""" (\w+): From fd355920696c44bd57d23f8bc64d388dbc36de8b Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Wed, 2 Jul 2025 17:37:25 +0100 Subject: [PATCH 12/58] Refactored to store grid_mapping parsing results in CFReader. --- .../fileformats/_nc_load_rules/actions.py | 14 ++--- lib/iris/fileformats/cf.py | 54 +++++++++++++++++-- lib/iris/fileformats/netcdf/loader.py | 5 ++ 3 files changed, 58 insertions(+), 15 deletions(-) diff --git a/lib/iris/fileformats/_nc_load_rules/actions.py b/lib/iris/fileformats/_nc_load_rules/actions.py index 2d064671bb..2a89810531 100644 --- a/lib/iris/fileformats/_nc_load_rules/actions.py +++ b/lib/iris/fileformats/_nc_load_rules/actions.py @@ -357,11 +357,8 @@ def action_build_dimension_coordinate(engine, providescoord_fact): if len(coord_systems): # Find which coord system applies to this coordinate. - # Parse the grid_mapping attribute to get coord_system -> coordinate mappings - attr_grid_mapping = getattr(engine.cf_var, "grid_mapping", None) - if attr_grid_mapping: - cs_mappings = hh._parse_extended_grid_mapping(attr_grid_mapping) - + cs_mappings = engine.cube_parts["coordinate_system_mappings"] + if cs_mappings and coord_systems: # Simple `grid_mapping = "crs"` # Only one coord_system will be present and cs_grid_mapping will # contain no coordinate references (set to None). @@ -519,11 +516,8 @@ def action_build_auxiliary_coordinate(engine, auxcoord_fact): coord_systems = engine.cube_parts.get("coordinate_systems") coord_system = None - # get grid_mapping from data variable attribute and parse it - grid_mapping_attr = getattr(engine.cf_var, "grid_mapping", None) - if grid_mapping_attr: - cs_mappings = hh._parse_extended_grid_mapping(grid_mapping_attr) - + cs_mappings = engine.cube_parts.get("coordinate_system_mappings", None) + if cs_mappings and coord_systems: if len(coord_systems) == 1 and list(cs_mappings.values()) == [None]: # Simple grid_mapping - doesn't apply to AuxCoords (we need an explicit mapping) pass diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 8e5e620a35..6882545e12 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -24,6 +24,7 @@ import numpy as np import numpy.ma as ma +import iris.exceptions import iris.fileformats._nc_load_rules.helpers as hh from iris.fileformats.netcdf import _thread_safe_nc from iris.mesh.components import Connectivity @@ -655,7 +656,9 @@ class CFGridMappingVariable(CFVariable): cf_identity = "grid_mapping" @classmethod - def identify(cls, variables, ignore=None, target=None, warn=True): + def identify( + cls, variables, ignore=None, target=None, warn=True, coord_system_mappings=None + ): result = {} ignore, target = cls._identify_common(variables, ignore, target) @@ -667,10 +670,21 @@ def identify(cls, variables, ignore=None, target=None, warn=True): if nc_var_att is not None: name = nc_var_att.strip() - # parse the grid_mappings - mappings = hh._parse_extended_grid_mapping(name) + # All `grid_mapping` attributes will already have been parsed prior + # to `identify` being called and passed in as an argument. We can + # ignore the attribute here (it's just used to identify that a grid + # mapping exists for this data variable) and get the pre-parsed + # mapping from the `coord_mapping_systems` keyword: + cs_mappings = None + if coord_system_mappings: + cs_mappings = coord_system_mappings.get(nc_var_name, None) + + if not cs_mappings: + # If cs_mappings is None, some parse error must have occurred and the + # user will have already been warned by `_parse_extended_grid_mappings` + continue - for name, coords in mappings.items(): + for name, coords in cs_mappings.items(): if name not in ignore: if name not in variables: if warn: @@ -1342,6 +1356,9 @@ def __init__(self, file_source, warn=False, monotonic=False): #: Collection of CF-netCDF variables associated with this netCDF file self.cf_group = self.CFGroup() + # Result of parsing "grid_mapping" attribute; mapping of coordinate_system => coordinates + self._coord_system_mappings = {} + # Issue load optimisation warning. if warn and self._dataset.file_format in [ "NETCDF3_CLASSIC", @@ -1410,6 +1427,18 @@ def _translate(self, variables): """Classify the netCDF variables into CF-netCDF variables.""" netcdf_variable_names = list(variables.keys()) + # Parse all instances of "grid_mapping" attributes and store in CFReader + # This avoids re-parsing the grid_mappings each time they are needed. + for nc_var in variables.values(): + if hasattr(nc_var, "grid_mapping"): + try: + cs_mappings = hh._parse_extended_grid_mapping(nc_var.grid_mapping) + self._coord_system_mappings[nc_var.name] = cs_mappings + except iris.exceptions.IrisError as e: + msg = f"Error parsing grid_grid mapping attribute for {nc_var.name}: {str(e)}" + warnings.warn(msg, category=iris.warnings.IrisCfWarning) + continue + # Identify all CF coordinate variables first. This must be done # first as, by CF convention, the definition of a CF auxiliary # coordinate variable may include a scalar CF coordinate variable, @@ -1428,7 +1457,15 @@ def _translate(self, variables): if issubclass(variable_type, CFGridMappingVariable) else coordinate_names ) - self.cf_group.update(variable_type.identify(variables, ignore=ignore)) + kwargs = ( + {"coord_system_mappings": self._coord_system_mappings} + if issubclass(variable_type, CFGridMappingVariable) + else {} + ) + + self.cf_group.update( + variable_type.identify(variables, ignore=ignore, **kwargs) + ) # Identify global netCDF attributes. attr_dict = { @@ -1570,6 +1607,7 @@ def _span_check( # Build CF variable relationships. for variable_type in self._variable_types: ignore = [] + kwargs = {} # Avoid UGridAuxiliaryCoordinateVariables also being # processed as CFAuxiliaryCoordinateVariables. if not is_mesh_var: @@ -1577,12 +1615,18 @@ def _span_check( # Prevent grid mapping variables being mis-identified as CF coordinate variables. if not issubclass(variable_type, CFGridMappingVariable): ignore += coordinate_names + else: + # pass parsed grid_mappings to CFGridMappingVariable types + kwargs.update( + {"coord_system_mappings": self._coord_system_mappings} + ) match = variable_type.identify( variables, ignore=ignore, target=cf_variable.cf_name, warn=False, + **kwargs, ) # Sanity check dimensionality coverage. for cf_name in match: diff --git a/lib/iris/fileformats/netcdf/loader.py b/lib/iris/fileformats/netcdf/loader.py index 22b9e195b4..254cc3fa06 100644 --- a/lib/iris/fileformats/netcdf/loader.py +++ b/lib/iris/fileformats/netcdf/loader.py @@ -81,6 +81,11 @@ def _assert_case_specific_facts(engine, cf, cf_group): engine.cube_parts["ancillary_variables"] = [] engine.cube_parts["coordinate_systems"] = {} + # Add the parsed coordinate reference system mappings + engine.cube_parts["coordinate_system_mappings"] = cf._coord_system_mappings.get( + engine.cf_var.cf_name, None + ) + # Assert facts for CF coordinates. for cf_name in cf_group.coordinates.keys(): engine.add_case_specific_fact("coordinate", (cf_name,)) From baa91efeb693739150b73da4de25e4e9b788a62b Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Wed, 2 Jul 2025 18:30:30 +0100 Subject: [PATCH 13/58] Small update to keep tests working with Mocks --- lib/iris/fileformats/cf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 6882545e12..a87e407a87 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -1430,9 +1430,9 @@ def _translate(self, variables): # Parse all instances of "grid_mapping" attributes and store in CFReader # This avoids re-parsing the grid_mappings each time they are needed. for nc_var in variables.values(): - if hasattr(nc_var, "grid_mapping"): + if grid_mapping_attr := getattr(nc_var, "grid_mapping", None): try: - cs_mappings = hh._parse_extended_grid_mapping(nc_var.grid_mapping) + cs_mappings = hh._parse_extended_grid_mapping(grid_mapping_attr) self._coord_system_mappings[nc_var.name] = cs_mappings except iris.exceptions.IrisError as e: msg = f"Error parsing grid_grid mapping attribute for {nc_var.name}: {str(e)}" From bd401c9b97c7fda2e865f2d1e15012df7e958896 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Thu, 3 Jul 2025 13:06:07 +0100 Subject: [PATCH 14/58] Changed cs_mapping to return dict of `{coord: cs}` rather than `{cs: [coords, ...]}` --- .../fileformats/_nc_load_rules/actions.py | 36 +++++++++---------- .../fileformats/_nc_load_rules/helpers.py | 13 ++++--- lib/iris/fileformats/cf.py | 16 +++++++-- 3 files changed, 39 insertions(+), 26 deletions(-) diff --git a/lib/iris/fileformats/_nc_load_rules/actions.py b/lib/iris/fileformats/_nc_load_rules/actions.py index 2a89810531..8239d72f65 100644 --- a/lib/iris/fileformats/_nc_load_rules/actions.py +++ b/lib/iris/fileformats/_nc_load_rules/actions.py @@ -359,22 +359,18 @@ def action_build_dimension_coordinate(engine, providescoord_fact): # Find which coord system applies to this coordinate. cs_mappings = engine.cube_parts["coordinate_system_mappings"] if cs_mappings and coord_systems: - # Simple `grid_mapping = "crs"` - # Only one coord_system will be present and cs_grid_mapping will - # contain no coordinate references (set to None). - if len(coord_systems) == 1 and list(cs_mappings.values()) == [None]: - # Only one grid mapping - apply it. + if len(coord_systems) == 1 and None in cs_mappings: + # Simple grid mapping (a single coord_system with no explicit coords) + # Applies to spatial DimCoord(s) only. In this case only one + # coordinate_system will have been built, so just use it. (coord_system,) = coord_systems.values() - (cs_name,) = cs_mappings.keys() - - # Extended `grid_mapping = "crs: coord1 coord2 crs: coord3 coord4"` - # We need to search for coord system that references our coordinate. + (cs_name,) = cs_mappings.values() else: - for name, ref_coords in cs_mappings.items(): - if cf_var.cf_name in ref_coords: - cs_name = name - coord_system = coord_systems[cs_name] - break + # Extended grid mapping, e.g. + # `grid_mapping = "crs: coord1 coord2 crs: coord3 coord4"` + # We need to search for coord system that references our coordinate. + if cs_name := cs_mappings.get(cf_var.cf_name): + coord_system = coord_systems[cs_name] # Translate the specific grid-mapping type to a grid-class if coord_system is None: @@ -518,15 +514,15 @@ def action_build_auxiliary_coordinate(engine, auxcoord_fact): cs_mappings = engine.cube_parts.get("coordinate_system_mappings", None) if cs_mappings and coord_systems: - if len(coord_systems) == 1 and list(cs_mappings.values()) == [None]: + if len(coord_systems) == 1 and None in cs_mappings: # Simple grid_mapping - doesn't apply to AuxCoords (we need an explicit mapping) pass else: - # Extended grid_mapping - for crs_name, coords in cs_mappings.items(): - if cf_var.cf_name in coords: - coord_system = coord_systems[crs_name] - break + # Extended grid mapping, e.g. + # `grid_mapping = "crs: coord1 coord2 crs: coord3 coord4"` + # We need to search for coord system that references our coordinate. + if cs_name := cs_mappings.get(cf_var.cf_name): + coord_system = coord_systems[cs_name] cf_var = engine.cf_var.cf_group.auxiliary_coordinates[var_name] hh.build_and_add_auxiliary_coordinate( diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index d414f8a8e4..3b7e99211c 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -1977,8 +1977,7 @@ def is_grid_mapping(engine, cf_name, grid_mapping): ################################################################################ -# TODO(ChrisB): Typing doesn't like : Dict[str, List[str] | None] -def _parse_extended_grid_mapping(grid_mapping: str) -> Dict[str, Any]: +def _parse_extended_grid_mapping(grid_mapping: str) -> Dict[Any, str]: """Parse `grid_mapping` attribute and return list of coordinate system variables and associated coords.""" # Handles extended grid_mapping too. Possibilities: # grid_mapping = "crs" : simple mapping; a single variable name with no coords @@ -1989,7 +1988,7 @@ def _parse_extended_grid_mapping(grid_mapping: str) -> Dict[str, Any]: # try simple mapping first if _GRID_MAPPING_PARSE_SIMPLE.match(grid_mapping): - mappings = {grid_mapping: None} # simple single grid mapping variable + mappings = {None: grid_mapping} # simple single grid mapping variable else: # Try extended mapping: # 1. Run validators to check for invalid expressions: @@ -2005,7 +2004,13 @@ def _parse_extended_grid_mapping(grid_mapping: str) -> Dict[str, Any]: raise iris.exceptions.IrisError(msg) # TODO: Better exception type # split second match group into list of coordinates: - mappings = {r[0]: r[1].split() for r in result} + mappings = {} + # TODO: below could possibly be a nested list/dict comprehension, but wold + # likely be overly complicated? + for r in result: + cs = r[0] + coords = r[1].split() + mappings.update({coord: cs for coord in coords}) return mappings diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index a87e407a87..24770e4f4f 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -684,7 +684,18 @@ def identify( # user will have already been warned by `_parse_extended_grid_mappings` continue - for name, coords in cs_mappings.items(): + # group the cs_mappings by coordinate system, as we want to iterate over coord systems: + uniq_cs = set(cs_mappings.values()) + cs_coord_mappings = { + cs: [ + coord + for coord, coord_cs in cs_mappings.items() + if cs == coord_cs + ] + for cs in uniq_cs + } + + for name, coords in cs_coord_mappings.items(): if name not in ignore: if name not in variables: if warn: @@ -697,7 +708,8 @@ def identify( # For extended grid_mapping, also check coord references exist: if coords: for coord_name in coords: - if coord_name not in variables: + # coord_name could be None if simple grid_mapping is used. + if coord_name and coord_name not in variables: message = "Missing CF-netCDF coordinate variable %r (associated with grid mapping variable %r), referenced by netCDF variable %r" warnings.warn( message % (coord_name, name, nc_var_name), From 8fd4f0c9ab9033a2ace8e1efbafc4a57dd809d13 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 13:51:21 +0100 Subject: [PATCH 15/58] Added sorting of coordinates in grid mapping to saver. Also addes Future flag for extended grid mapping saving. --- lib/iris/__init__.py | 10 + lib/iris/fileformats/netcdf/saver.py | 520 +++++++++++++++------------ lib/iris/tests/unit/test_Future.py | 3 +- 3 files changed, 293 insertions(+), 240 deletions(-) diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index be113bc912..6480761b03 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -183,6 +183,7 @@ def __init__( date_microseconds=False, derived_bounds=False, lam_pole_offset=False, + extended_grid_mapping=False, ): """Container for run-time options controls. @@ -223,6 +224,14 @@ def __init__( to a PP file will set the pole longitude (PP field ``bplon``) to 180.0 degrees if the grid is defined on a standard pole. Does not affect global or rotated-pole domains. + extended_grid_mapping : bool, default=False + When True, Iris will use the extended grid mapping syntax for the + `grid_mapping` attribute of a data variable. This allows for multiple + coordinate systems to be associated with a data variable and explicitly + defines an ordered mapping between coordinate systems and coordinates. + See: + https://cfconventions.org/Data/cf-conventions/cf-conventions-1.9/cf-conventions.html#grid-mappings-and-projections + for more information on extended grid mapping. """ # The flag 'example_future_flag' is provided as a reference for the @@ -238,6 +247,7 @@ def __init__( self.__dict__["date_microseconds"] = date_microseconds self.__dict__["derived_bounds"] = derived_bounds self.__dict__["lam_pole_offset"] = lam_pole_offset + self.__dict__["extended_grid_mapping"] = extended_grid_mapping # TODO: next major release: set IrisDeprecation to subclass # DeprecationWarning instead of UserWarning. diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 6478722519..a4dec6ff7b 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -1845,6 +1845,213 @@ def _create_cf_cell_methods(self, cube, dimension_names): return " ".join(cell_methods) + def _add_grid_mapping_to_dataset(self, cs, cf_var_grid): + """Create a CF-netCDF grid mapping variable and add to the dataset. + + Parameters + ---------- + cs : :class:`iris.coord_system.CoordSystem` + The :class:`iris.coord_system.CoordSystem` used to generate the + new netCDF CF grid mapping variable. + + Returns + ------- + None + """ + cf_var_grid = self._dataset.createVariable(cs.grid_mapping_name, np.int32) + _setncattr(cf_var_grid, "grid_mapping_name", cs.grid_mapping_name) + + def add_ellipsoid(ellipsoid): + cf_var_grid.longitude_of_prime_meridian = ( + ellipsoid.longitude_of_prime_meridian + ) + semi_major = ellipsoid.semi_major_axis + semi_minor = ellipsoid.semi_minor_axis + if semi_minor == semi_major: + cf_var_grid.earth_radius = semi_major + else: + cf_var_grid.semi_major_axis = semi_major + cf_var_grid.semi_minor_axis = semi_minor + if ellipsoid.datum is not None: + cf_var_grid.horizontal_datum_name = ellipsoid.datum + + # latlon + if isinstance(cs, iris.coord_systems.GeogCS): + add_ellipsoid(cs) + + # rotated latlon + elif isinstance(cs, iris.coord_systems.RotatedGeogCS): + if cs.ellipsoid: + add_ellipsoid(cs.ellipsoid) + cf_var_grid.grid_north_pole_latitude = cs.grid_north_pole_latitude + cf_var_grid.grid_north_pole_longitude = cs.grid_north_pole_longitude + cf_var_grid.north_pole_grid_longitude = cs.north_pole_grid_longitude + + # tmerc + elif isinstance(cs, iris.coord_systems.TransverseMercator): + if cs.ellipsoid: + add_ellipsoid(cs.ellipsoid) + cf_var_grid.longitude_of_central_meridian = cs.longitude_of_central_meridian + cf_var_grid.latitude_of_projection_origin = cs.latitude_of_projection_origin + cf_var_grid.false_easting = cs.false_easting + cf_var_grid.false_northing = cs.false_northing + cf_var_grid.scale_factor_at_central_meridian = ( + cs.scale_factor_at_central_meridian + ) + + # merc + elif isinstance(cs, iris.coord_systems.Mercator): + if cs.ellipsoid: + add_ellipsoid(cs.ellipsoid) + cf_var_grid.longitude_of_projection_origin = ( + cs.longitude_of_projection_origin + ) + cf_var_grid.false_easting = cs.false_easting + cf_var_grid.false_northing = cs.false_northing + # Only one of these should be set + if cs.standard_parallel is not None: + cf_var_grid.standard_parallel = cs.standard_parallel + elif cs.scale_factor_at_projection_origin is not None: + cf_var_grid.scale_factor_at_projection_origin = ( + cs.scale_factor_at_projection_origin + ) + + # lcc + elif isinstance(cs, iris.coord_systems.LambertConformal): + if cs.ellipsoid: + add_ellipsoid(cs.ellipsoid) + cf_var_grid.standard_parallel = cs.secant_latitudes + cf_var_grid.latitude_of_projection_origin = cs.central_lat + cf_var_grid.longitude_of_central_meridian = cs.central_lon + cf_var_grid.false_easting = cs.false_easting + cf_var_grid.false_northing = cs.false_northing + + # polar stereo (have to do this before Stereographic because it subclasses it) + elif isinstance(cs, iris.coord_systems.PolarStereographic): + if cs.ellipsoid: + add_ellipsoid(cs.ellipsoid) + cf_var_grid.latitude_of_projection_origin = cs.central_lat + cf_var_grid.straight_vertical_longitude_from_pole = cs.central_lon + cf_var_grid.false_easting = cs.false_easting + cf_var_grid.false_northing = cs.false_northing + # Only one of these should be set + if cs.true_scale_lat is not None: + cf_var_grid.true_scale_lat = cs.true_scale_lat + elif cs.scale_factor_at_projection_origin is not None: + cf_var_grid.scale_factor_at_projection_origin = ( + cs.scale_factor_at_projection_origin + ) + else: + cf_var_grid.scale_factor_at_projection_origin = 1.0 + + # stereo + elif isinstance(cs, iris.coord_systems.Stereographic): + if cs.ellipsoid: + add_ellipsoid(cs.ellipsoid) + cf_var_grid.longitude_of_projection_origin = cs.central_lon + cf_var_grid.latitude_of_projection_origin = cs.central_lat + cf_var_grid.false_easting = cs.false_easting + cf_var_grid.false_northing = cs.false_northing + # Only one of these should be set + if cs.true_scale_lat is not None: + msg = ( + "It is not valid CF to save a true_scale_lat for " + "a Stereographic grid mapping." + ) + raise ValueError(msg) + elif cs.scale_factor_at_projection_origin is not None: + cf_var_grid.scale_factor_at_projection_origin = ( + cs.scale_factor_at_projection_origin + ) + else: + cf_var_grid.scale_factor_at_projection_origin = 1.0 + + # osgb (a specific tmerc) + elif isinstance(cs, iris.coord_systems.OSGB): + warnings.warn( + "OSGB coordinate system not yet handled", + category=iris.warnings.IrisSaveWarning, + ) + + # lambert azimuthal equal area + elif isinstance(cs, iris.coord_systems.LambertAzimuthalEqualArea): + if cs.ellipsoid: + add_ellipsoid(cs.ellipsoid) + cf_var_grid.longitude_of_projection_origin = ( + cs.longitude_of_projection_origin + ) + cf_var_grid.latitude_of_projection_origin = cs.latitude_of_projection_origin + cf_var_grid.false_easting = cs.false_easting + cf_var_grid.false_northing = cs.false_northing + + # albers conical equal area + elif isinstance(cs, iris.coord_systems.AlbersEqualArea): + if cs.ellipsoid: + add_ellipsoid(cs.ellipsoid) + cf_var_grid.longitude_of_central_meridian = cs.longitude_of_central_meridian + cf_var_grid.latitude_of_projection_origin = cs.latitude_of_projection_origin + cf_var_grid.false_easting = cs.false_easting + cf_var_grid.false_northing = cs.false_northing + cf_var_grid.standard_parallel = cs.standard_parallels + + # vertical perspective + elif isinstance(cs, iris.coord_systems.VerticalPerspective): + if cs.ellipsoid: + add_ellipsoid(cs.ellipsoid) + cf_var_grid.longitude_of_projection_origin = ( + cs.longitude_of_projection_origin + ) + cf_var_grid.latitude_of_projection_origin = cs.latitude_of_projection_origin + cf_var_grid.false_easting = cs.false_easting + cf_var_grid.false_northing = cs.false_northing + cf_var_grid.perspective_point_height = cs.perspective_point_height + + # geostationary + elif isinstance(cs, iris.coord_systems.Geostationary): + if cs.ellipsoid: + add_ellipsoid(cs.ellipsoid) + cf_var_grid.longitude_of_projection_origin = ( + cs.longitude_of_projection_origin + ) + cf_var_grid.latitude_of_projection_origin = cs.latitude_of_projection_origin + cf_var_grid.false_easting = cs.false_easting + cf_var_grid.false_northing = cs.false_northing + cf_var_grid.perspective_point_height = cs.perspective_point_height + cf_var_grid.sweep_angle_axis = cs.sweep_angle_axis + + # oblique mercator (and rotated variant) + # Use duck-typing over isinstance() - subclasses (i.e. + # RotatedMercator) upset mock tests. + elif getattr(cs, "grid_mapping_name", None) == "oblique_mercator": + # RotatedMercator subclasses ObliqueMercator, and RM + # instances are implicitly saved as OM due to inherited + # properties. This is correct because CF 1.11 is removing + # all mention of RM. + if cs.ellipsoid: + add_ellipsoid(cs.ellipsoid) + cf_var_grid.azimuth_of_central_line = cs.azimuth_of_central_line + cf_var_grid.latitude_of_projection_origin = cs.latitude_of_projection_origin + cf_var_grid.longitude_of_projection_origin = ( + cs.longitude_of_projection_origin + ) + cf_var_grid.false_easting = cs.false_easting + cf_var_grid.false_northing = cs.false_northing + cf_var_grid.scale_factor_at_projection_origin = ( + cs.scale_factor_at_projection_origin + ) + + # other + else: + warnings.warn( + "Unable to represent the horizontal " + "coordinate system. The coordinate system " + "type %r is not yet implemented." % type(cs), + category=iris.warnings.IrisSaveWarning, + ) + + # add WKT string + cf_var_grid.crs_wkt = cs.as_cartopy_crs().to_wkt() + def _create_cf_grid_mapping(self, cube, cf_var_cube): """Create CF-netCDF grid mapping and associated CF-netCDF variable. @@ -1864,265 +2071,100 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube): None """ - # TODO(CB): We need ALL coord systems here, not just the fist one found. - # cs = cube.coord_system("CoordSystem") - - # TODO(CB): This is workaround code (gets _unique_ coord systems): - # TODO(CB): Put this code in new cube.coord_systems() function, or modify cube.coord_system() - # coord_systems = [coord.coord_system for coord in cube.coords() if coord.coord_system] coord_systems = [] - for coord in cube.coords(): - if coord.coord_system and coord.coord_system not in coord_systems: - coord_systems.append(coord.coord_system) + if iris.FUTURE.extended_grid_mapping: + # get unique list of all coord_systems on cube coords: + for coord in cube.coords(): + if coord.coord_system and coord.coord_system not in coord_systems: + coord_systems.append(coord.coord_system) + else: + # will only return a single coord system (if one exists): + if cs := cube.coord_system("CoordSystem"): + coord_systems.append(cs) grid_mappings = [] + matched_all_coords = True for cs in coord_systems: # Grid var not yet created? if cs not in self._coord_systems: + # handle potential duplicate netCDF variable names: while cs.grid_mapping_name in self._dataset.variables: aname = self._increment_name(cs.grid_mapping_name) cs.grid_mapping_name = aname - cf_var_grid = self._dataset.createVariable( - cs.grid_mapping_name, np.int32 - ) - _setncattr(cf_var_grid, "grid_mapping_name", cs.grid_mapping_name) - - def add_ellipsoid(ellipsoid): - cf_var_grid.longitude_of_prime_meridian = ( - ellipsoid.longitude_of_prime_meridian - ) - semi_major = ellipsoid.semi_major_axis - semi_minor = ellipsoid.semi_minor_axis - if semi_minor == semi_major: - cf_var_grid.earth_radius = semi_major - else: - cf_var_grid.semi_major_axis = semi_major - cf_var_grid.semi_minor_axis = semi_minor - if ellipsoid.datum is not None: - cf_var_grid.horizontal_datum_name = ellipsoid.datum - - # latlon - if isinstance(cs, iris.coord_systems.GeogCS): - add_ellipsoid(cs) - - # rotated latlon - elif isinstance(cs, iris.coord_systems.RotatedGeogCS): - if cs.ellipsoid: - add_ellipsoid(cs.ellipsoid) - cf_var_grid.grid_north_pole_latitude = cs.grid_north_pole_latitude - cf_var_grid.grid_north_pole_longitude = cs.grid_north_pole_longitude - cf_var_grid.north_pole_grid_longitude = cs.north_pole_grid_longitude - - # tmerc - elif isinstance(cs, iris.coord_systems.TransverseMercator): - if cs.ellipsoid: - add_ellipsoid(cs.ellipsoid) - cf_var_grid.longitude_of_central_meridian = ( - cs.longitude_of_central_meridian - ) - cf_var_grid.latitude_of_projection_origin = ( - cs.latitude_of_projection_origin - ) - cf_var_grid.false_easting = cs.false_easting - cf_var_grid.false_northing = cs.false_northing - cf_var_grid.scale_factor_at_central_meridian = ( - cs.scale_factor_at_central_meridian - ) - - # merc - elif isinstance(cs, iris.coord_systems.Mercator): - if cs.ellipsoid: - add_ellipsoid(cs.ellipsoid) - cf_var_grid.longitude_of_projection_origin = ( - cs.longitude_of_projection_origin - ) - cf_var_grid.false_easting = cs.false_easting - cf_var_grid.false_northing = cs.false_northing - # Only one of these should be set - if cs.standard_parallel is not None: - cf_var_grid.standard_parallel = cs.standard_parallel - elif cs.scale_factor_at_projection_origin is not None: - cf_var_grid.scale_factor_at_projection_origin = ( - cs.scale_factor_at_projection_origin - ) + # create grid mapping variable on dataset for this coordinate system: + self._add_grid_mapping_to_dataset(cs) + self._coord_systems.append(cs) - # lcc - elif isinstance(cs, iris.coord_systems.LambertConformal): - if cs.ellipsoid: - add_ellipsoid(cs.ellipsoid) - cf_var_grid.standard_parallel = cs.secant_latitudes - cf_var_grid.latitude_of_projection_origin = cs.central_lat - cf_var_grid.longitude_of_central_meridian = cs.central_lon - cf_var_grid.false_easting = cs.false_easting - cf_var_grid.false_northing = cs.false_northing - - # polar stereo (have to do this before Stereographic because it subclasses it) - elif isinstance(cs, iris.coord_systems.PolarStereographic): - if cs.ellipsoid: - add_ellipsoid(cs.ellipsoid) - cf_var_grid.latitude_of_projection_origin = cs.central_lat - cf_var_grid.straight_vertical_longitude_from_pole = cs.central_lon - cf_var_grid.false_easting = cs.false_easting - cf_var_grid.false_northing = cs.false_northing - # Only one of these should be set - if cs.true_scale_lat is not None: - cf_var_grid.true_scale_lat = cs.true_scale_lat - elif cs.scale_factor_at_projection_origin is not None: - cf_var_grid.scale_factor_at_projection_origin = ( - cs.scale_factor_at_projection_origin - ) - else: - cf_var_grid.scale_factor_at_projection_origin = 1.0 - - # stereo - elif isinstance(cs, iris.coord_systems.Stereographic): - if cs.ellipsoid: - add_ellipsoid(cs.ellipsoid) - cf_var_grid.longitude_of_projection_origin = cs.central_lon - cf_var_grid.latitude_of_projection_origin = cs.central_lat - cf_var_grid.false_easting = cs.false_easting - cf_var_grid.false_northing = cs.false_northing - # Only one of these should be set - if cs.true_scale_lat is not None: + # create the `grid_mapping` attribute for the data variable: + if iris.FUTURE.extended_grid_mapping: + # Order the coordinates as per the order in the CRS/WKT string. + # (We should only ever have a coordinate system for horizontal + # spatial coords, so check for east/north directions) + ordered_coords = [] + for ax_info in cs.as_cartopy_crs().axis_info: + try: + match ax_info.direction.lower(): + case "east": + ordered_coords.append( + cube.coord(axis="X", coord_system=cs) + ) + case "north": + ordered_coords.append( + cube.coord(axis="Y", coord_system=cs) + ) + case _: + msg = ( + f"Can't handle axis direction {ax_info.direction!r}" + ) + raise Exception(msg) + except iris.exceptions.CoordinateNotFoundError as e: msg = ( - "It is not valid CF to save a true_scale_lat for " - "a Stereographic grid mapping." + f"Failed to assign coordinate for {ax_info.name} axis of " + f"coordinate system {cs.grid_mapping_name}: {str(e)}" ) - raise ValueError(msg) - elif cs.scale_factor_at_projection_origin is not None: - cf_var_grid.scale_factor_at_projection_origin = ( - cs.scale_factor_at_projection_origin - ) - else: - cf_var_grid.scale_factor_at_projection_origin = 1.0 + warnings.warn(msg, iris.warnings.IrisSaveWarning) - # osgb (a specific tmerc) - elif isinstance(cs, iris.coord_systems.OSGB): - warnings.warn( - "OSGB coordinate system not yet handled", - category=iris.warnings.IrisSaveWarning, - ) + # fall back to simple grid mapping (single crs entry for DimCoord only) + matched_all_coords = False - # lambert azimuthal equal area - elif isinstance(cs, iris.coord_systems.LambertAzimuthalEqualArea): - if cs.ellipsoid: - add_ellipsoid(cs.ellipsoid) - cf_var_grid.longitude_of_projection_origin = ( - cs.longitude_of_projection_origin - ) - cf_var_grid.latitude_of_projection_origin = ( - cs.latitude_of_projection_origin - ) - cf_var_grid.false_easting = cs.false_easting - cf_var_grid.false_northing = cs.false_northing - - # albers conical equal area - elif isinstance(cs, iris.coord_systems.AlbersEqualArea): - if cs.ellipsoid: - add_ellipsoid(cs.ellipsoid) - cf_var_grid.longitude_of_central_meridian = ( - cs.longitude_of_central_meridian - ) - cf_var_grid.latitude_of_projection_origin = ( - cs.latitude_of_projection_origin - ) - cf_var_grid.false_easting = cs.false_easting - cf_var_grid.false_northing = cs.false_northing - cf_var_grid.standard_parallel = cs.standard_parallels - - # vertical perspective - elif isinstance(cs, iris.coord_systems.VerticalPerspective): - if cs.ellipsoid: - add_ellipsoid(cs.ellipsoid) - cf_var_grid.longitude_of_projection_origin = ( - cs.longitude_of_projection_origin - ) - cf_var_grid.latitude_of_projection_origin = ( - cs.latitude_of_projection_origin - ) - cf_var_grid.false_easting = cs.false_easting - cf_var_grid.false_northing = cs.false_northing - cf_var_grid.perspective_point_height = cs.perspective_point_height - - # geostationary - elif isinstance(cs, iris.coord_systems.Geostationary): - if cs.ellipsoid: - add_ellipsoid(cs.ellipsoid) - cf_var_grid.longitude_of_projection_origin = ( - cs.longitude_of_projection_origin - ) - cf_var_grid.latitude_of_projection_origin = ( - cs.latitude_of_projection_origin - ) - cf_var_grid.false_easting = cs.false_easting - cf_var_grid.false_northing = cs.false_northing - cf_var_grid.perspective_point_height = cs.perspective_point_height - cf_var_grid.sweep_angle_axis = cs.sweep_angle_axis - - # oblique mercator (and rotated variant) - # Use duck-typing over isinstance() - subclasses (i.e. - # RotatedMercator) upset mock tests. - elif getattr(cs, "grid_mapping_name", None) == "oblique_mercator": - # RotatedMercator subclasses ObliqueMercator, and RM - # instances are implicitly saved as OM due to inherited - # properties. This is correct because CF 1.11 is removing - # all mention of RM. - if cs.ellipsoid: - add_ellipsoid(cs.ellipsoid) - cf_var_grid.azimuth_of_central_line = cs.azimuth_of_central_line - cf_var_grid.latitude_of_projection_origin = ( - cs.latitude_of_projection_origin - ) - cf_var_grid.longitude_of_projection_origin = ( - cs.longitude_of_projection_origin - ) - cf_var_grid.false_easting = cs.false_easting - cf_var_grid.false_northing = cs.false_northing - cf_var_grid.scale_factor_at_projection_origin = ( - cs.scale_factor_at_projection_origin - ) - - # other - else: - warnings.warn( - "Unable to represent the horizontal " - "coordinate system. The coordinate system " - "type %r is not yet implemented." % type(cs), - category=iris.warnings.IrisSaveWarning, - ) - - cf_var_grid.crs_wkt = cs.as_cartopy_crs().to_wkt() - - self._coord_systems.append(cs) - - # create grid mapping string: - coords = cube.coords(coord_system=cs) - # TODO: How do we sort these coords? - # For DimCoords, we can use the dimension order, but what - # about AuxCoords that could be 2D? - - # prefer netCDF variable name, if exists, else default ro coord.name() - coord_string = " ".join( - [coord.var_name if coord.var_name else coord.name() for coord in coords] - ) - grid_mappings.append((cs.grid_mapping_name, coord_string)) + # prefer netCDF variable name, if exists, else default to coord.name() + coord_string = " ".join( + [ + coord.var_name if coord.var_name else coord.name() + for coord in ordered_coords + ] + ) + grid_mappings.append((cs.grid_mapping_name, coord_string)) # Refer to grid var - # TODO: Future flag for extended grid_mapping syntax? - # For now, if only one coord_system, write out in simple form. - # Use extended form for multiple coord_systems - if len(grid_mappings): - if len(grid_mappings) > 1: - # TODO: Check future flag? Warn if not set? - grid_mapping = " ".join( - f"{cs_name}: {cs_coords}" for cs_name, cs_coords in grid_mappings - ) + if len(coord_systems): + grid_mapping = None + if iris.FUTURE.extended_grid_mapping: + if matched_all_coords: + grid_mapping = " ".join( + f"{cs_name}: {cs_coords}" + for cs_name, cs_coords in grid_mappings + ) + else: + # We didn't match all coords required for grid mapping, default to + # single coordinate system for DimCoords, if set. + for ax in ["x", "y"]: + try: + if dim_cs := cube.coord( + axis=ax, dim_coords=True + ).coord_system: + grid_mapping = dim_cs.grid_mapping_name + break + except iris.exceptions.CoordinateNotFoundError: + pass else: - grid_mapping = grid_mappings[0][0] # just the cs_name + # Not using extended_grid_mapping: only ever be on cs in this case: + grid_mapping = coord_systems[0].grid_mapping_name - _setncattr(cf_var_cube, "grid_mapping", grid_mapping) + if grid_mapping: + _setncattr(cf_var_cube, "grid_mapping", grid_mapping) def _create_cf_data_variable( self, diff --git a/lib/iris/tests/unit/test_Future.py b/lib/iris/tests/unit/test_Future.py index cd923fd9e2..c9e8787c10 100644 --- a/lib/iris/tests/unit/test_Future.py +++ b/lib/iris/tests/unit/test_Future.py @@ -124,7 +124,8 @@ class Test__str_repr: def _check_content(self, future, text): assert text == ( "Future(datum_support=False, pandas_ndim=False, save_split_attrs=False, " - "date_microseconds=False, derived_bounds=False, lam_pole_offset=False)" + "date_microseconds=False, derived_bounds=False, lam_pole_offset=False, " + "extended_grid_mapping=False)" ) # Also just check that all the property elements are included for propname in future.__dict__.keys(): From 46edf1ac47a0a8a8b1117ed91bb8a508aba0df7d Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 14:14:15 +0100 Subject: [PATCH 16/58] Fixed typo --- lib/iris/fileformats/cf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 24770e4f4f..768a285d26 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -1447,7 +1447,7 @@ def _translate(self, variables): cs_mappings = hh._parse_extended_grid_mapping(grid_mapping_attr) self._coord_system_mappings[nc_var.name] = cs_mappings except iris.exceptions.IrisError as e: - msg = f"Error parsing grid_grid mapping attribute for {nc_var.name}: {str(e)}" + msg = f"Error parsing `grid_mapping` attribute for {nc_var.name}: {str(e)}" warnings.warn(msg, category=iris.warnings.IrisCfWarning) continue From ed0e7036652f26e2b8cd015d3a7c651ae159c375 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 14:15:12 +0100 Subject: [PATCH 17/58] Removed redundant line --- lib/iris/fileformats/cf.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 768a285d26..170593debd 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -668,8 +668,6 @@ def identify( nc_var_att = getattr(nc_var, cls.cf_identity, None) if nc_var_att is not None: - name = nc_var_att.strip() - # All `grid_mapping` attributes will already have been parsed prior # to `identify` being called and passed in as an argument. We can # ignore the attribute here (it's just used to identify that a grid From 7af31edc1e07ba58d7e21f4a2d91941c458d0f23 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 14:18:55 +0100 Subject: [PATCH 18/58] Inverted conditional for clarity. --- lib/iris/fileformats/cf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 170593debd..7cdc9c2e1d 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -1623,13 +1623,13 @@ def _span_check( if not is_mesh_var: ignore += ugrid_coord_names # Prevent grid mapping variables being mis-identified as CF coordinate variables. - if not issubclass(variable_type, CFGridMappingVariable): - ignore += coordinate_names - else: + if issubclass(variable_type, CFGridMappingVariable): # pass parsed grid_mappings to CFGridMappingVariable types kwargs.update( {"coord_system_mappings": self._coord_system_mappings} ) + else: + ignore += coordinate_names match = variable_type.identify( variables, From 2a5466947dbde7c9301dab6cfc9e0b8d5d7b7965 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 14:25:17 +0100 Subject: [PATCH 19/58] MpPy type hinting. --- lib/iris/fileformats/_nc_load_rules/helpers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index 3b7e99211c..6a53490536 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -19,7 +19,7 @@ import contextlib from functools import partial import re -from typing import TYPE_CHECKING, Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, List, Optional import warnings import cf_units @@ -1977,14 +1977,14 @@ def is_grid_mapping(engine, cf_name, grid_mapping): ################################################################################ -def _parse_extended_grid_mapping(grid_mapping: str) -> Dict[Any, str]: +def _parse_extended_grid_mapping(grid_mapping: str) -> dict[None | str, str]: """Parse `grid_mapping` attribute and return list of coordinate system variables and associated coords.""" # Handles extended grid_mapping too. Possibilities: # grid_mapping = "crs" : simple mapping; a single variable name with no coords # grid_mapping = "crs: lat lon" : extended mapping; a variable name and list of coords # grid_mapping = "crs: lat lon other: var1 var2" : multiple extended mappings - # TODO(ChrisB): TESTS!! + mappings: dict[None | str, str] # try simple mapping first if _GRID_MAPPING_PARSE_SIMPLE.match(grid_mapping): From e80a6b4f8d40f846c1bae37c163dc2af6c1b6566 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 14:28:07 +0100 Subject: [PATCH 20/58] Use .get rather than indexing on engine.cube_parts --- lib/iris/fileformats/_nc_load_rules/actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/fileformats/_nc_load_rules/actions.py b/lib/iris/fileformats/_nc_load_rules/actions.py index 8239d72f65..db4d35bed3 100644 --- a/lib/iris/fileformats/_nc_load_rules/actions.py +++ b/lib/iris/fileformats/_nc_load_rules/actions.py @@ -357,7 +357,7 @@ def action_build_dimension_coordinate(engine, providescoord_fact): if len(coord_systems): # Find which coord system applies to this coordinate. - cs_mappings = engine.cube_parts["coordinate_system_mappings"] + cs_mappings = engine.cube_parts.get("coordinate_system_mappings") if cs_mappings and coord_systems: if len(coord_systems) == 1 and None in cs_mappings: # Simple grid mapping (a single coord_system with no explicit coords) From a7b8817f76ac6c28caeef55b67fa44a3247b3289 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 14:47:35 +0100 Subject: [PATCH 21/58] Fix broken function signature --- lib/iris/fileformats/netcdf/saver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index a4dec6ff7b..b8a49e4be8 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -1845,7 +1845,7 @@ def _create_cf_cell_methods(self, cube, dimension_names): return " ".join(cell_methods) - def _add_grid_mapping_to_dataset(self, cs, cf_var_grid): + def _add_grid_mapping_to_dataset(self, cs): """Create a CF-netCDF grid mapping variable and add to the dataset. Parameters From 09952c8394072facabe2292b81b12c6b08b0e3a4 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 14:48:12 +0100 Subject: [PATCH 22/58] Fix Mocks for Saver.py --- lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py index a8d61d38a8..428262cdc0 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py +++ b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py @@ -816,7 +816,9 @@ def setncattr(self, name, attr): saver = mock.Mock(spec=Saver, _coord_systems=[], _dataset=dataset) variable = NCMock() - # This is the method we're actually testing! + saver._add_grid_mapping_to_dataset = ( + lambda x: Saver._add_grid_mapping_to_dataset(saver, x) + ) Saver._create_cf_grid_mapping(saver, cube, variable) self.assertEqual(create_var_fn.call_count, 1) From fea5d70484d75fe8d56efe9c205358a932e78c35 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 15:10:04 +0100 Subject: [PATCH 23/58] Changed grid_mappings to dict in saver.py --- lib/iris/fileformats/netcdf/saver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index b8a49e4be8..f33e6d0c04 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2082,7 +2082,7 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube): if cs := cube.coord_system("CoordSystem"): coord_systems.append(cs) - grid_mappings = [] + grid_mappings = {} matched_all_coords = True for cs in coord_systems: @@ -2136,7 +2136,7 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube): for coord in ordered_coords ] ) - grid_mappings.append((cs.grid_mapping_name, coord_string)) + grid_mappings[cs.grid_mapping_name] = coord_string # Refer to grid var if len(coord_systems): @@ -2145,7 +2145,7 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube): if matched_all_coords: grid_mapping = " ".join( f"{cs_name}: {cs_coords}" - for cs_name, cs_coords in grid_mappings + for cs_name, cs_coords in grid_mappings.items() ) else: # We didn't match all coords required for grid mapping, default to From 4791e844e2481f9cd05226d3997925657e4ecc9f Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 15:45:34 +0100 Subject: [PATCH 24/58] Missing category keyword on warning. --- lib/iris/fileformats/netcdf/saver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index f33e6d0c04..4a4c21f1d6 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2124,7 +2124,7 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube): f"Failed to assign coordinate for {ax_info.name} axis of " f"coordinate system {cs.grid_mapping_name}: {str(e)}" ) - warnings.warn(msg, iris.warnings.IrisSaveWarning) + warnings.warn(msg, category=iris.warnings.IrisSaveWarning) # fall back to simple grid mapping (single crs entry for DimCoord only) matched_all_coords = False From 564c37cfb73a4ca8e66eacca64bffb22a931bf2b Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 16:02:37 +0100 Subject: [PATCH 25/58] Removed Future flag and added Cube.ordered_axes property instead. --- lib/iris/__init__.py | 10 ---------- lib/iris/cube.py | 30 ++++++++++++++++++++++++++++ lib/iris/fileformats/netcdf/saver.py | 6 +++--- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/lib/iris/__init__.py b/lib/iris/__init__.py index 6480761b03..be113bc912 100644 --- a/lib/iris/__init__.py +++ b/lib/iris/__init__.py @@ -183,7 +183,6 @@ def __init__( date_microseconds=False, derived_bounds=False, lam_pole_offset=False, - extended_grid_mapping=False, ): """Container for run-time options controls. @@ -224,14 +223,6 @@ def __init__( to a PP file will set the pole longitude (PP field ``bplon``) to 180.0 degrees if the grid is defined on a standard pole. Does not affect global or rotated-pole domains. - extended_grid_mapping : bool, default=False - When True, Iris will use the extended grid mapping syntax for the - `grid_mapping` attribute of a data variable. This allows for multiple - coordinate systems to be associated with a data variable and explicitly - defines an ordered mapping between coordinate systems and coordinates. - See: - https://cfconventions.org/Data/cf-conventions/cf-conventions-1.9/cf-conventions.html#grid-mappings-and-projections - for more information on extended grid mapping. """ # The flag 'example_future_flag' is provided as a reference for the @@ -247,7 +238,6 @@ def __init__( self.__dict__["date_microseconds"] = date_microseconds self.__dict__["derived_bounds"] = derived_bounds self.__dict__["lam_pole_offset"] = lam_pole_offset - self.__dict__["extended_grid_mapping"] = extended_grid_mapping # TODO: next major release: set IrisDeprecation to subclass # DeprecationWarning instead of UserWarning. diff --git a/lib/iris/cube.py b/lib/iris/cube.py index d75b43b6c8..e65ea6868a 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1332,6 +1332,10 @@ def __init__( for ancillary_variable, avdims in ancillary_variables_and_dims: self.add_ancillary_variable(ancillary_variable, avdims) + # Set ordered_axis property - if we have more that one coord system then + # it should be True. + self._ordered_axes = len(self.coord_systems()) > 1 + @property def _names(self) -> tuple[str | None, str | None, str | None, str | None]: """Tuple containing the value of each name participating in the identity of a :class:`iris.cube.Cube`. @@ -2451,6 +2455,32 @@ def coord_system( return result + def coord_systems(self) -> list[iris.coord_systems.CoordSystem]: + """Return a list of all coordinate systems used in cube coordinates.""" + # Gather list of our unique CoordSystems on cube: + coord_systems = ClassDict(iris.coord_systems.CoordSystem) + for coord in self.coords(): + if coord.coord_system: + coord_systems.add(coord.coord_system, replace=True) + + return list(coord_systems.values()) + + @property + def ordered_axes(self) -> bool: + """Return True if a cube will use extended grid mapping syntax to write axes order in grid_mapping. + + Only relevant when saving a cube to NetCDF file format. + + For more details see: + https://cfconventions.org/Data/cf-conventions/cf-conventions-1.9/cf-conventions.html#grid-mappings-and-projections + """ + return self._ordered_axes + + @ordered_axes.setter + def ordered_axes(self, ordered: bool) -> None: + """Set to True to enable extended grid mapping syntax.""" + self._ordered_axes = ordered + def _any_meshcoord(self) -> MeshCoord | None: """Return a MeshCoord if there are any, else None.""" mesh_coords = self.coords(mesh_coords=True) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 4a4c21f1d6..589a9b71d9 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2072,7 +2072,7 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube): """ coord_systems = [] - if iris.FUTURE.extended_grid_mapping: + if cube.ordered_axes: # get unique list of all coord_systems on cube coords: for coord in cube.coords(): if coord.coord_system and coord.coord_system not in coord_systems: @@ -2098,7 +2098,7 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube): self._coord_systems.append(cs) # create the `grid_mapping` attribute for the data variable: - if iris.FUTURE.extended_grid_mapping: + if cube.ordered_axes: # Order the coordinates as per the order in the CRS/WKT string. # (We should only ever have a coordinate system for horizontal # spatial coords, so check for east/north directions) @@ -2141,7 +2141,7 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube): # Refer to grid var if len(coord_systems): grid_mapping = None - if iris.FUTURE.extended_grid_mapping: + if cube.ordered_axes: if matched_all_coords: grid_mapping = " ".join( f"{cs_name}: {cs_coords}" From 0499eec5114dc12ce644b337a684e17dd7cd87e6 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 16:30:33 +0100 Subject: [PATCH 26/58] Prefer use of `_get_coord_variable_name` over --- lib/iris/fileformats/netcdf/saver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 589a9b71d9..ec7152862d 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2118,7 +2118,7 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube): msg = ( f"Can't handle axis direction {ax_info.direction!r}" ) - raise Exception(msg) + raise NotImplementedError(msg) except iris.exceptions.CoordinateNotFoundError as e: msg = ( f"Failed to assign coordinate for {ax_info.name} axis of " @@ -2132,7 +2132,7 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube): # prefer netCDF variable name, if exists, else default to coord.name() coord_string = " ".join( [ - coord.var_name if coord.var_name else coord.name() + self._get_coord_variable_name(cube, coord) for coord in ordered_coords ] ) From 3bdb1004b92c0231cbea078efcb4e1ab6b08b612 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 16:40:18 +0100 Subject: [PATCH 27/58] Updated URL to CF Conventions document --- lib/iris/cube.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index e65ea6868a..88bdd44996 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -2471,8 +2471,8 @@ def ordered_axes(self) -> bool: Only relevant when saving a cube to NetCDF file format. - For more details see: - https://cfconventions.org/Data/cf-conventions/cf-conventions-1.9/cf-conventions.html#grid-mappings-and-projections + For more details see "Grid Mappings and Projections" in the CF Conventions document: + https://cfconventions.org/cf-conventions/conformance.html """ return self._ordered_axes From 113aaf3351faef81cafd6b3733a310cc379b59f5 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 16:56:53 +0100 Subject: [PATCH 28/58] Fixed spurious match group in extended grid mapping regex --- lib/iris/fileformats/_nc_load_rules/helpers.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index 6a53490536..fccaeccc5f 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -145,14 +145,14 @@ # # Regex for parsing grid_mapping (extended format) -# Link to online regex101 playground: https://regex101.com/r/jFbFLG/1 +# Link to online regex101 playground: https://regex101.com/r/NcKzkQ/1 # # (\w+): # Matches ':' and stores in CAPTURE GROUP 1 # ( # CAPTURE GROUP 2 for capturing multiple coords # (?: # Non-capturing group for composing match # \s+ # Matches one or more blank characters # (?!\w+:) # Negative look-ahead: don't match followed by colon -# (\w+) # Matches a +# \w+ # Matches a # )+ # Repeats non-capturing group at least once. # ) # End of CAPTURE GROUP 2 _GRID_MAPPING_PARSE_EXTENDED = re.compile( @@ -162,7 +162,7 @@ (?: \s+ (?!\w+:) - (\w+) + \w+ )+ )+ """, @@ -2007,10 +2007,8 @@ def _parse_extended_grid_mapping(grid_mapping: str) -> dict[None | str, str]: mappings = {} # TODO: below could possibly be a nested list/dict comprehension, but wold # likely be overly complicated? - for r in result: - cs = r[0] - coords = r[1].split() - mappings.update({coord: cs for coord in coords}) + for cs, coords in result: + mappings.update({coord: cs for coord in coords.split()}) return mappings From cd5c9b6009aa26a61d24081f1a5ab599e0160d89 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 22:57:43 +0100 Subject: [PATCH 29/58] Ensure WKT is only written out if cube.ordered_axes=True --- lib/iris/fileformats/netcdf/saver.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index ec7152862d..d8ee2a32f1 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -1845,7 +1845,7 @@ def _create_cf_cell_methods(self, cube, dimension_names): return " ".join(cell_methods) - def _add_grid_mapping_to_dataset(self, cs): + def _add_grid_mapping_to_dataset(self, cs, extended_grid_mapping=False): """Create a CF-netCDF grid mapping variable and add to the dataset. Parameters @@ -2050,7 +2050,8 @@ def add_ellipsoid(ellipsoid): ) # add WKT string - cf_var_grid.crs_wkt = cs.as_cartopy_crs().to_wkt() + if extended_grid_mapping: + cf_var_grid.crs_wkt = cs.as_cartopy_crs().to_wkt() def _create_cf_grid_mapping(self, cube, cf_var_cube): """Create CF-netCDF grid mapping and associated CF-netCDF variable. @@ -2094,7 +2095,9 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube): cs.grid_mapping_name = aname # create grid mapping variable on dataset for this coordinate system: - self._add_grid_mapping_to_dataset(cs) + self._add_grid_mapping_to_dataset( + cs, extended_grid_mapping=cube.ordered_axes + ) self._coord_systems.append(cs) # create the `grid_mapping` attribute for the data variable: From d501f13c9c2fb0a65fa20678bc9f700d53c62e66 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 23:05:30 +0100 Subject: [PATCH 30/58] Updated `Test_create_cf_grid_mapping` to test grid_mapping generation with and without ordered coordinates. --- .../fileformats/netcdf/saver/test_Saver.py | 105 ++++++++++++------ 1 file changed, 72 insertions(+), 33 deletions(-) diff --git a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py index 428262cdc0..ceb8ad6594 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py +++ b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py @@ -6,7 +6,7 @@ # Import iris.tests first so that some things can be initialised before # importing anything else. -from types import ModuleType +from types import MethodType, ModuleType import iris.tests as tests # isort:skip @@ -16,6 +16,7 @@ import numpy as np from numpy import ma +import pytest import iris from iris.coord_systems import ( @@ -789,12 +790,24 @@ def test_passthrough_units(self): ) -class Test__create_cf_grid_mapping(tests.IrisTest): +class Test_create_cf_grid_mapping: + """Tests correct generation of CF grid_mapping variable attributes. + + Note: The firtst 3 tests are run with the "extended grid" mapping + both enabled (the default for all these tests) and disabled. This + controls the output of the WKT attribute. + """ + + @pytest.fixture(autouse=True) + def _setup(self): + self._extended_grid_mapping = True # forces WKT strings to be written + def _cube_with_cs(self, coord_system): """Return a simple 2D cube that uses the given coordinate system.""" cube = stock.lat_lon_cube() x, y = cube.coord("longitude"), cube.coord("latitude") x.coord_system = y.coord_system = coord_system + cube.ordered_axes = self._extended_grid_mapping return cube def _grid_mapping_variable(self, coord_system): @@ -816,13 +829,20 @@ def setncattr(self, name, attr): saver = mock.Mock(spec=Saver, _coord_systems=[], _dataset=dataset) variable = NCMock() - saver._add_grid_mapping_to_dataset = ( - lambda x: Saver._add_grid_mapping_to_dataset(saver, x) + # Bind required methods on Saver called by `_create_cf_grid_mapping`: + saver._add_grid_mapping_to_dataset = MethodType( + Saver._add_grid_mapping_to_dataset, saver + ) + saver._get_coord_variable_name = MethodType( + Saver._get_coord_variable_name, saver ) + saver.cf_valid_var_name = Saver.cf_valid_var_name # simpler for static methods + + # The method we want to test: Saver._create_cf_grid_mapping(saver, cube, variable) - self.assertEqual(create_var_fn.call_count, 1) - self.assertEqual(variable.grid_mapping, grid_variable.grid_mapping_name) + assert create_var_fn.call_count == 1 + assert variable.grid_mapping, grid_variable.grid_mapping_name return grid_variable def _variable_attributes(self, coord_system): @@ -842,14 +862,24 @@ def _test(self, coord_system, expected): actual = self._variable_attributes(coord_system) # To see obvious differences, check that they keys are the same. - self.assertEqual(sorted(actual.keys()), sorted(expected.keys())) + assert sorted(actual.keys()) == sorted(expected.keys()) # Now check that the values are equivalent. - self.assertEqual(actual, expected) + assert actual == expected - def test_rotated_geog_cs(self): + def test_rotated_geog_cs(self, extended_grid_mapping): + self._extended_grid_mapping = extended_grid_mapping coord_system = RotatedGeogCS(37.5, 177.5, ellipsoid=GeogCS(6371229.0)) + expected = { - "crs_wkt": ( + "grid_mapping_name": b"rotated_latitude_longitude", + "north_pole_grid_longitude": 0.0, + "grid_north_pole_longitude": 177.5, + "grid_north_pole_latitude": 37.5, + "longitude_of_prime_meridian": 0.0, + "earth_radius": 6371229.0, + } + if extended_grid_mapping: + expected["crs_wkt"] = ( 'GEOGCRS["unnamed",BASEGEOGCRS["unknown",DATUM["unknown",' 'ELLIPSOID["unknown",6371229,0,LENGTHUNIT["metre",1,ID[' '"EPSG",9001]]]],PRIMEM["Greenwich",0,ANGLEUNIT["degree",' @@ -863,20 +893,20 @@ def test_rotated_geog_cs(self): '"degree",0.0174532925199433,ID["EPSG",9122]]],AXIS["latitude"' ',north,ORDER[2],ANGLEUNIT["degree",0.0174532925199433,ID[' '"EPSG",9122]]]]' - ), - "grid_mapping_name": b"rotated_latitude_longitude", - "north_pole_grid_longitude": 0.0, - "grid_north_pole_longitude": 177.5, - "grid_north_pole_latitude": 37.5, - "longitude_of_prime_meridian": 0.0, - "earth_radius": 6371229.0, - } + ) + self._test(coord_system, expected) - def test_spherical_geog_cs(self): + def test_spherical_geog_cs(self, extended_grid_mapping): + self._extended_grid_mapping = extended_grid_mapping coord_system = GeogCS(6371229.0) expected = { - "crs_wkt": ( + "grid_mapping_name": b"latitude_longitude", + "longitude_of_prime_meridian": 0.0, + "earth_radius": 6371229.0, + } + if extended_grid_mapping: + expected["crs_wkt"] = ( 'GEOGCRS["unknown",DATUM["unknown",ELLIPSOID["unknown",6371229' ',0,LENGTHUNIT["metre",1,ID["EPSG",9001]]]],PRIMEM["Greenwich"' ',0,ANGLEUNIT["degree",0.0174532925199433],ID["EPSG",8901]],CS' @@ -884,17 +914,20 @@ def test_spherical_geog_cs(self): '"degree",0.0174532925199433,ID["EPSG",9122]]],AXIS["latitude"' ',north,ORDER[2],ANGLEUNIT["degree",0.0174532925199433,ID[' '"EPSG",9122]]]]' - ), - "grid_mapping_name": b"latitude_longitude", - "longitude_of_prime_meridian": 0.0, - "earth_radius": 6371229.0, - } + ) self._test(coord_system, expected) - def test_elliptic_geog_cs(self): + def test_elliptic_geog_cs(self, extended_grid_mapping): + self._extended_grid_mapping = extended_grid_mapping coord_system = GeogCS(637, 600) expected = { - "crs_wkt": ( + "grid_mapping_name": b"latitude_longitude", + "longitude_of_prime_meridian": 0.0, + "semi_minor_axis": 600.0, + "semi_major_axis": 637.0, + } + if extended_grid_mapping: + expected["crs_wkt"] = ( 'GEOGCRS["unknown",DATUM["unknown",ELLIPSOID["unknown",637,' '17.2162162162162,LENGTHUNIT["metre",1,ID["EPSG",9001]]]],' 'PRIMEM["Reference meridian",0,ANGLEUNIT["degree",' @@ -903,12 +936,7 @@ def test_elliptic_geog_cs(self): '0.0174532925199433,ID["EPSG",9122]]],AXIS["latitude",north,' 'ORDER[2],ANGLEUNIT["degree",0.0174532925199433,ID["EPSG",' "9122]]]]" - ), - "grid_mapping_name": b"latitude_longitude", - "longitude_of_prime_meridian": 0.0, - "semi_minor_axis": 600.0, - "semi_major_axis": 637.0, - } + ) self._test(coord_system, expected) def test_lambert_conformal(self): @@ -1198,5 +1226,16 @@ def test_oblique_cs(self): self._test(coord_system, expected) +@pytest.fixture( + params=[ + pytest.param(True, id="extended_grid_mapping"), + pytest.param(False, id="no_extended_grid_mapping"), + ] +) +def extended_grid_mapping(request): + """Fixture for enabling/disabling extended grid mapping.""" + return request.param + + if __name__ == "__main__": tests.main() From b9e64ec25c31a7e1b0aa77c4067df7efb753019b Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 23:13:03 +0100 Subject: [PATCH 31/58] Revert Future tests. --- lib/iris/tests/unit/test_Future.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/iris/tests/unit/test_Future.py b/lib/iris/tests/unit/test_Future.py index c9e8787c10..cd923fd9e2 100644 --- a/lib/iris/tests/unit/test_Future.py +++ b/lib/iris/tests/unit/test_Future.py @@ -124,8 +124,7 @@ class Test__str_repr: def _check_content(self, future, text): assert text == ( "Future(datum_support=False, pandas_ndim=False, save_split_attrs=False, " - "date_microseconds=False, derived_bounds=False, lam_pole_offset=False, " - "extended_grid_mapping=False)" + "date_microseconds=False, derived_bounds=False, lam_pole_offset=False)" ) # Also just check that all the property elements are included for propname in future.__dict__.keys(): From bd361ec2ffba5c9547f378092c9b70115eef6cdd Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 4 Jul 2025 23:20:57 +0100 Subject: [PATCH 32/58] Use `_name_coord_map` to get coord var name --- lib/iris/fileformats/netcdf/saver.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index d8ee2a32f1..afab8511a7 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2134,10 +2134,7 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube): # prefer netCDF variable name, if exists, else default to coord.name() coord_string = " ".join( - [ - self._get_coord_variable_name(cube, coord) - for coord in ordered_coords - ] + [self._name_coord_map.name(coord) for coord in ordered_coords] ) grid_mappings[cs.grid_mapping_name] = coord_string From 2c5901013521ee3f44a08b328be580db5ce2baf1 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Sat, 5 Jul 2025 13:13:00 +0100 Subject: [PATCH 33/58] Update _name_coord_map if no cfvar name found for coord --- lib/iris/fileformats/netcdf/saver.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index afab8511a7..52c1ef2347 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -2132,10 +2132,19 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube): # fall back to simple grid mapping (single crs entry for DimCoord only) matched_all_coords = False - # prefer netCDF variable name, if exists, else default to coord.name() - coord_string = " ".join( - [self._name_coord_map.name(coord) for coord in ordered_coords] - ) + # Get variable names, and store them if necessary: + cfvar_names = [] + for coord in ordered_coords: + cfvar = self._name_coord_map.name(coord) + if not cfvar: + # not found - create and store it: + cfvar = self._get_coord_variable_name(cube, coord) + self._name_coord_map.append( + cfvar, self._get_coord_variable_name(cube, coord) + ) + cfvar_names.append(cfvar) + + coord_string = " ".join(cfvar_names) grid_mappings[cs.grid_mapping_name] = coord_string # Refer to grid var From c37d1fbacd6d76c47ceedb63e7a409205d4499e5 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Sat, 5 Jul 2025 13:21:46 +0100 Subject: [PATCH 34/58] Added some tests for multi-coordinate system saving --- .../write_extended_grid_mapping/multi_cs.cdl | 40 ++++++ .../multi_cs_missing_coord.cdl | 37 ++++++ .../write_extended_grid_mapping/no_aux_cs.cdl | 34 ++++++ .../write_extended_grid_mapping/no_cs.cdl | 22 ++++ .../fileformats/netcdf/saver/test_Saver.py | 114 ++++++++++++++++-- 5 files changed, 235 insertions(+), 12 deletions(-) create mode 100644 lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write_extended_grid_mapping/multi_cs.cdl create mode 100644 lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write_extended_grid_mapping/multi_cs_missing_coord.cdl create mode 100644 lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write_extended_grid_mapping/no_aux_cs.cdl create mode 100644 lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write_extended_grid_mapping/no_cs.cdl diff --git a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write_extended_grid_mapping/multi_cs.cdl b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write_extended_grid_mapping/multi_cs.cdl new file mode 100644 index 0000000000..547813fdf1 --- /dev/null +++ b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write_extended_grid_mapping/multi_cs.cdl @@ -0,0 +1,40 @@ +dimensions: + projection_x_coordinate = 4 ; + projection_y_coordinate = 3 ; +variables: + int64 air_pressure_anomaly(projection_y_coordinate, projection_x_coordinate) ; + air_pressure_anomaly:standard_name = "air_pressure_anomaly" ; + air_pressure_anomaly:grid_mapping = "transverse_mercator: projection_x_coordinate projection_y_coordinate latitude_longitude: longitude latitude" ; + air_pressure_anomaly:coordinates = "latitude longitude" ; + int transverse_mercator ; + transverse_mercator:grid_mapping_name = "transverse_mercator" ; + transverse_mercator:longitude_of_prime_meridian = 0. ; + transverse_mercator:semi_major_axis = 6377563.396 ; + transverse_mercator:semi_minor_axis = 6356256.909 ; + transverse_mercator:longitude_of_central_meridian = -2. ; + transverse_mercator:latitude_of_projection_origin = 49. ; + transverse_mercator:false_easting = -400000. ; + transverse_mercator:false_northing = 100000. ; + transverse_mercator:scale_factor_at_central_meridian = 0.9996012717 ; + transverse_mercator:crs_wkt = "PROJCRS[\"unknown\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6377563.396,299.324961266495,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],CONVERSION[\"unknown\",METHOD[\"Transverse Mercator\",ID[\"EPSG\",9807]],PARAMETER[\"Latitude of natural origin\",49,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8801]],PARAMETER[\"Longitude of natural origin\",-2,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8802]],PARAMETER[\"Scale factor at natural origin\",0.9996012717,SCALEUNIT[\"unity\",1],ID[\"EPSG\",8805]],PARAMETER[\"False easting\",-400000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8806]],PARAMETER[\"False northing\",100000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8807]]],CS[Cartesian,2],AXIS[\"(E)\",east,ORDER[1],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]],AXIS[\"(N)\",north,ORDER[2],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]]" ; + int latitude_longitude ; + latitude_longitude:grid_mapping_name = "latitude_longitude" ; + latitude_longitude:longitude_of_prime_meridian = 0. ; + latitude_longitude:semi_major_axis = 6377563.396 ; + latitude_longitude:semi_minor_axis = 6356256.909 ; + latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6377563.396,299.324961266495,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; + int64 projection_y_coordinate(projection_y_coordinate) ; + projection_y_coordinate:axis = "Y" ; + projection_y_coordinate:units = "m" ; + projection_y_coordinate:standard_name = "projection_y_coordinate" ; + int64 projection_x_coordinate(projection_x_coordinate) ; + projection_x_coordinate:axis = "X" ; + projection_x_coordinate:units = "m" ; + projection_x_coordinate:standard_name = "projection_x_coordinate" ; + int64 latitude(projection_y_coordinate, projection_x_coordinate) ; + latitude:units = "degrees_north" ; + latitude:standard_name = "latitude" ; + int64 longitude(projection_y_coordinate, projection_x_coordinate) ; + longitude:units = "degrees_east" ; + longitude:standard_name = "longitude" ; +} diff --git a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write_extended_grid_mapping/multi_cs_missing_coord.cdl b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write_extended_grid_mapping/multi_cs_missing_coord.cdl new file mode 100644 index 0000000000..34c7061c9b --- /dev/null +++ b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write_extended_grid_mapping/multi_cs_missing_coord.cdl @@ -0,0 +1,37 @@ +dimensions: + projection_x_coordinate = 4 ; + projection_y_coordinate = 3 ; +variables: + int64 air_pressure_anomaly(projection_y_coordinate, projection_x_coordinate) ; + air_pressure_anomaly:standard_name = "air_pressure_anomaly" ; + air_pressure_anomaly:grid_mapping = "transverse_mercator" ; + air_pressure_anomaly:coordinates = "longitude" ; + int transverse_mercator ; + transverse_mercator:grid_mapping_name = "transverse_mercator" ; + transverse_mercator:longitude_of_prime_meridian = 0. ; + transverse_mercator:semi_major_axis = 6377563.396 ; + transverse_mercator:semi_minor_axis = 6356256.909 ; + transverse_mercator:longitude_of_central_meridian = -2. ; + transverse_mercator:latitude_of_projection_origin = 49. ; + transverse_mercator:false_easting = -400000. ; + transverse_mercator:false_northing = 100000. ; + transverse_mercator:scale_factor_at_central_meridian = 0.9996012717 ; + transverse_mercator:crs_wkt = "PROJCRS[\"unknown\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6377563.396,299.324961266495,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],CONVERSION[\"unknown\",METHOD[\"Transverse Mercator\",ID[\"EPSG\",9807]],PARAMETER[\"Latitude of natural origin\",49,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8801]],PARAMETER[\"Longitude of natural origin\",-2,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8802]],PARAMETER[\"Scale factor at natural origin\",0.9996012717,SCALEUNIT[\"unity\",1],ID[\"EPSG\",8805]],PARAMETER[\"False easting\",-400000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8806]],PARAMETER[\"False northing\",100000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8807]]],CS[Cartesian,2],AXIS[\"(E)\",east,ORDER[1],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]],AXIS[\"(N)\",north,ORDER[2],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]]" ; + int latitude_longitude ; + latitude_longitude:grid_mapping_name = "latitude_longitude" ; + latitude_longitude:longitude_of_prime_meridian = 0. ; + latitude_longitude:semi_major_axis = 6377563.396 ; + latitude_longitude:semi_minor_axis = 6356256.909 ; + latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6377563.396,299.324961266495,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; + int64 projection_y_coordinate(projection_y_coordinate) ; + projection_y_coordinate:axis = "Y" ; + projection_y_coordinate:units = "m" ; + projection_y_coordinate:standard_name = "projection_y_coordinate" ; + int64 projection_x_coordinate(projection_x_coordinate) ; + projection_x_coordinate:axis = "X" ; + projection_x_coordinate:units = "m" ; + projection_x_coordinate:standard_name = "projection_x_coordinate" ; + int64 longitude(projection_y_coordinate, projection_x_coordinate) ; + longitude:units = "degrees_east" ; + longitude:standard_name = "longitude" ; +} diff --git a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write_extended_grid_mapping/no_aux_cs.cdl b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write_extended_grid_mapping/no_aux_cs.cdl new file mode 100644 index 0000000000..9b5aeb2dfb --- /dev/null +++ b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write_extended_grid_mapping/no_aux_cs.cdl @@ -0,0 +1,34 @@ +dimensions: + projection_x_coordinate = 4 ; + projection_y_coordinate = 3 ; +variables: + int64 air_pressure_anomaly(projection_y_coordinate, projection_x_coordinate) ; + air_pressure_anomaly:standard_name = "air_pressure_anomaly" ; + air_pressure_anomaly:grid_mapping = "transverse_mercator: projection_x_coordinate projection_y_coordinate" ; + air_pressure_anomaly:coordinates = "latitude longitude" ; + int transverse_mercator ; + transverse_mercator:grid_mapping_name = "transverse_mercator" ; + transverse_mercator:longitude_of_prime_meridian = 0. ; + transverse_mercator:semi_major_axis = 6377563.396 ; + transverse_mercator:semi_minor_axis = 6356256.909 ; + transverse_mercator:longitude_of_central_meridian = -2. ; + transverse_mercator:latitude_of_projection_origin = 49. ; + transverse_mercator:false_easting = -400000. ; + transverse_mercator:false_northing = 100000. ; + transverse_mercator:scale_factor_at_central_meridian = 0.9996012717 ; + transverse_mercator:crs_wkt = "PROJCRS[\"unknown\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6377563.396,299.324961266495,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],CONVERSION[\"unknown\",METHOD[\"Transverse Mercator\",ID[\"EPSG\",9807]],PARAMETER[\"Latitude of natural origin\",49,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8801]],PARAMETER[\"Longitude of natural origin\",-2,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8802]],PARAMETER[\"Scale factor at natural origin\",0.9996012717,SCALEUNIT[\"unity\",1],ID[\"EPSG\",8805]],PARAMETER[\"False easting\",-400000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8806]],PARAMETER[\"False northing\",100000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8807]]],CS[Cartesian,2],AXIS[\"(E)\",east,ORDER[1],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]],AXIS[\"(N)\",north,ORDER[2],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]]" ; + int64 projection_y_coordinate(projection_y_coordinate) ; + projection_y_coordinate:axis = "Y" ; + projection_y_coordinate:units = "m" ; + projection_y_coordinate:standard_name = "projection_y_coordinate" ; + int64 projection_x_coordinate(projection_x_coordinate) ; + projection_x_coordinate:axis = "X" ; + projection_x_coordinate:units = "m" ; + projection_x_coordinate:standard_name = "projection_x_coordinate" ; + int64 latitude(projection_y_coordinate, projection_x_coordinate) ; + latitude:units = "degrees_north" ; + latitude:standard_name = "latitude" ; + int64 longitude(projection_y_coordinate, projection_x_coordinate) ; + longitude:units = "degrees_east" ; + longitude:standard_name = "longitude" ; +} diff --git a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write_extended_grid_mapping/no_cs.cdl b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write_extended_grid_mapping/no_cs.cdl new file mode 100644 index 0000000000..ce8825b16d --- /dev/null +++ b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write_extended_grid_mapping/no_cs.cdl @@ -0,0 +1,22 @@ +dimensions: + projection_x_coordinate = 4 ; + projection_y_coordinate = 3 ; +variables: + int64 air_pressure_anomaly(projection_y_coordinate, projection_x_coordinate) ; + air_pressure_anomaly:standard_name = "air_pressure_anomaly" ; + air_pressure_anomaly:coordinates = "latitude longitude" ; + int64 projection_y_coordinate(projection_y_coordinate) ; + projection_y_coordinate:axis = "Y" ; + projection_y_coordinate:units = "m" ; + projection_y_coordinate:standard_name = "projection_y_coordinate" ; + int64 projection_x_coordinate(projection_x_coordinate) ; + projection_x_coordinate:axis = "X" ; + projection_x_coordinate:units = "m" ; + projection_x_coordinate:standard_name = "projection_x_coordinate" ; + int64 latitude(projection_y_coordinate, projection_x_coordinate) ; + latitude:units = "degrees_north" ; + latitude:standard_name = "latitude" ; + int64 longitude(projection_y_coordinate, projection_x_coordinate) ; + longitude:units = "degrees_east" ; + longitude:standard_name = "longitude" ; +} diff --git a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py index ceb8ad6594..60fcceb7d0 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py +++ b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py @@ -6,7 +6,7 @@ # Import iris.tests first so that some things can be initialised before # importing anything else. -from types import MethodType, ModuleType +from types import ModuleType import iris.tests as tests # isort:skip @@ -36,6 +36,7 @@ from iris.coords import AuxCoord, DimCoord from iris.cube import Cube from iris.fileformats.netcdf import Saver, _thread_safe_nc +from iris.tests._shared_utils import assert_CDL import iris.tests.stock as stock @@ -790,10 +791,107 @@ def test_passthrough_units(self): ) +@pytest.fixture +def transverse_mercator_cube_ordered_axes(): + data = np.arange(12).reshape(3, 4) + cube = Cube(data, "air_pressure_anomaly") + cube.ordered_axes = True + + geog_cs = GeogCS(6377563.396, 6356256.909) + trans_merc = TransverseMercator( + 49.0, -2.0, -400000.0, 100000.0, 0.9996012717, geog_cs + ) + coord = DimCoord( + np.arange(3), + "projection_y_coordinate", + units="m", + coord_system=trans_merc, + ) + cube.add_dim_coord(coord, 0) + coord = DimCoord( + np.arange(4), + "projection_x_coordinate", + units="m", + coord_system=trans_merc, + ) + cube.add_dim_coord(coord, 1) + + # Add auxiliary lat/lon coords with a GeogCS coord system + coord = AuxCoord( + np.arange(3 * 4).reshape((3, 4)), + "longitude", + units="degrees", + coord_system=geog_cs, + ) + cube.add_aux_coord(coord, (0, 1)) + + coord = AuxCoord( + np.arange(3 * 4).reshape((3, 4)), + "latitude", + units="degrees", + coord_system=geog_cs, + ) + cube.add_aux_coord(coord, (0, 1)) + + return cube + + +class Test_write_extended_grid_mapping: + def test_multi_cs(self, transverse_mercator_cube_ordered_axes, tmp_path, request): + """Test writing a cube with multiple coordinate systems. + Should generate a grid mapping using extended syntax that references + both coordinate systems and the coords. + """ + cube = transverse_mercator_cube_ordered_axes + nc_path = tmp_path / "tmp.nc" + with Saver(nc_path, "NETCDF4") as saver: + saver.write(cube) + assert_CDL(request, nc_path) + + def test_no_aux_cs(self, transverse_mercator_cube_ordered_axes, tmp_path, request): + """Test when DimCoords have coord system, but AuxCoords do not. + Should write extended grid mapping for just DimCoords. + """ + cube = transverse_mercator_cube_ordered_axes + cube.coord("latitude").coord_system = None + cube.coord("longitude").coord_system = None + + nc_path = tmp_path / "tmp.nc" + with Saver(nc_path, "NETCDF4") as saver: + saver.write(cube) + assert_CDL(request, nc_path) + + def test_multi_cs_missing_coord( + self, transverse_mercator_cube_ordered_axes, tmp_path, request + ): + """Test when we have a missing coordinate. + Grid mapping will fall back to simple mapping to DimCoord CS (no coords referenced). + """ + cube = transverse_mercator_cube_ordered_axes + cube.remove_coord("latitude") + nc_path = tmp_path / "tmp.nc" + with Saver(nc_path, "NETCDF4") as saver: + saver.write(cube) + assert_CDL(request, nc_path) + + def test_no_cs(self, transverse_mercator_cube_ordered_axes, tmp_path, request): + """Test when no coordinate systems associated with cube coords. + Grid mapping will not be generated at all. + """ + cube = transverse_mercator_cube_ordered_axes + for coord in cube.coords(): + coord.coord_system = None + + nc_path = tmp_path / "tmp.nc" + with Saver(nc_path, "NETCDF4") as saver: + saver.write(cube) + assert_CDL(request, nc_path) + + class Test_create_cf_grid_mapping: """Tests correct generation of CF grid_mapping variable attributes. - Note: The firtst 3 tests are run with the "extended grid" mapping + Note: The first 3 tests are run with the "extended grid" mapping both enabled (the default for all these tests) and disabled. This controls the output of the WKT attribute. """ @@ -826,20 +924,12 @@ def setncattr(self, name, attr): grid_variable = NCMock(name="NetCDFVariable") create_var_fn = mock.Mock(side_effect=[grid_variable]) dataset = mock.Mock(variables=[], createVariable=create_var_fn) - saver = mock.Mock(spec=Saver, _coord_systems=[], _dataset=dataset) variable = NCMock() - # Bind required methods on Saver called by `_create_cf_grid_mapping`: - saver._add_grid_mapping_to_dataset = MethodType( - Saver._add_grid_mapping_to_dataset, saver - ) - saver._get_coord_variable_name = MethodType( - Saver._get_coord_variable_name, saver - ) - saver.cf_valid_var_name = Saver.cf_valid_var_name # simpler for static methods + saver = Saver(dataset, "NETCDF4", compute=False) # The method we want to test: - Saver._create_cf_grid_mapping(saver, cube, variable) + saver._create_cf_grid_mapping(cube, variable) assert create_var_fn.call_count == 1 assert variable.grid_mapping, grid_variable.grid_mapping_name From ff9041d2aa80e66fad3efefaabf344a5fb7ab6e9 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Sat, 5 Jul 2025 14:10:22 +0100 Subject: [PATCH 35/58] Added assert on existence of coord system in test__grid_mappings.py --- .../fileformats/nc_load_rules/actions/test__grid_mappings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py index 637af2210b..f5beafce6b 100644 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py @@ -326,6 +326,7 @@ def check_result( self.assertIsNone(yco_cs) self.assertIsNone(xco_cs) else: + self.assertIsNotNone(cube_cs) if cube_cstype is not None: self.assertIsInstance(cube_cs, cube_cstype) if xco_no_cs: From 54c439365260ecce8cc768c139cbf6491b4f4369 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Sat, 5 Jul 2025 22:03:02 +0100 Subject: [PATCH 36/58] Added multi coord system loading tests --- .../fileformats/_nc_load_rules/actions.py | 4 +- .../nc_load_rules/actions/__init__.py | 10 +- .../actions/test__grid_mappings.py | 236 ++++++++++++++++++ 3 files changed, 245 insertions(+), 5 deletions(-) diff --git a/lib/iris/fileformats/_nc_load_rules/actions.py b/lib/iris/fileformats/_nc_load_rules/actions.py index db4d35bed3..af84a84fa9 100644 --- a/lib/iris/fileformats/_nc_load_rules/actions.py +++ b/lib/iris/fileformats/_nc_load_rules/actions.py @@ -370,7 +370,7 @@ def action_build_dimension_coordinate(engine, providescoord_fact): # `grid_mapping = "crs: coord1 coord2 crs: coord3 coord4"` # We need to search for coord system that references our coordinate. if cs_name := cs_mappings.get(cf_var.cf_name): - coord_system = coord_systems[cs_name] + coord_system = coord_systems.get(cs_name, None) # Translate the specific grid-mapping type to a grid-class if coord_system is None: @@ -522,7 +522,7 @@ def action_build_auxiliary_coordinate(engine, auxcoord_fact): # `grid_mapping = "crs: coord1 coord2 crs: coord3 coord4"` # We need to search for coord system that references our coordinate. if cs_name := cs_mappings.get(cf_var.cf_name): - coord_system = coord_systems[cs_name] + coord_system = coord_systems.get(cs_name, None) cf_var = engine.cf_var.cf_group.auxiliary_coordinates[var_name] hh.build_and_add_auxiliary_coordinate( diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py index 7c79740023..296765f853 100644 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/__init__.py @@ -65,7 +65,7 @@ def tearDownClass(cls): # Destroy a temp directory for temp files. shutil.rmtree(cls.temp_dirpath) - def load_cube_from_cdl(self, cdl_string, cdl_path, nc_path): + def load_cube_from_cdl(self, cdl_string, cdl_path, nc_path, mocker=None): """Load the 'phenom' data variable in a CDL testcase, as a cube. Using ncgen, CFReader and the _load_cube call. @@ -85,8 +85,12 @@ def load_cube_from_cdl(self, cdl_string, cdl_path, nc_path): # If debug enabled, switch on the activation summary debug output. # Use 'patch' so it is restored after the test. - self.patch("iris.fileformats.netcdf.loader.DEBUG", self.debug_info) - + if mocker: + # If pytest mocker provided, use that to patch: + mocker.patch("iris.fileformats.netcdf.loader.DEBUG", self.debug_info) + elif hasattr(self, "patch"): + # keep old UnitTest patch working + self.patch("iris.fileformats.netcdf.loader.DEBUG", self.debug_info) with warnings.catch_warnings(): warnings.filterwarnings( "ignore", diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py index f5beafce6b..b52464d365 100644 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py @@ -9,8 +9,12 @@ """ +import iris.coord_systems + import iris.tests as tests # isort: skip +import pytest + import iris.coord_systems as ics import iris.fileformats._nc_load_rules.helpers as hh from iris.loading import LOAD_PROBLEMS @@ -959,5 +963,237 @@ def test_nondim_lats(self): self.check_result(result, yco_is_aux=True, load_problems_regex=error) +@pytest.fixture +def latlon_cs(): + return """ + int crsWGS84 ; + crsWGS84:grid_mapping_name = "latitude_longitude" ; + crsWGS84:longitude_of_prime_meridian = 0. ; + crsWGS84:semi_major_axis = 6378137. ; + crsWGS84:inverse_flattening = 298.257223563 ; + double lat(x,y) ; + lat:standard_name = "latitude" ; + lat:units = "degrees_north" ; + double lon(x,y) ; + lon:standard_name = "longitude" ; + lon:units = "degrees_east" ; + """ + + +@pytest.fixture +def latlon_cs_missing_coord(latlon_cs): + # Trin last 3 lines of latlon_cs to remove longitude coord + return "\n".join(latlon_cs.splitlines()[:-4]) + + +@pytest.fixture +def osgb_cs(): + return """ + int crsOSGB ; + crsOSGB:grid_mapping_name = "transverse_mercator" ; + crsOSGB:semi_major_axis = 6377563.396 ; + crsOSGB:inverse_flattening = 299.3249646 ; + crsOSGB:longitude_of_prime_meridian = 0. ; + crsOSGB:latitude_of_projection_origin = 49. ; + crsOSGB:longitude_of_central_meridian = -2. ; + crsOSGB:scale_factor_at_central_meridian = 0.9996012717 ; + crsOSGB:false_easting = 400000. ; + crsOSGB:false_northing = -100000. ; + crsOSGB:unit = "metre" ; + double x(x) ; + x:standard_name = "projection_x_coordinate" ; + x:long_name = "Easting" ; + x:units = "m" ; + double y(y) ; + y:standard_name = "projection_y_coordinate" ; + y:long_name = "Northing" ; + y:units = "m" ; + """ + + +class Test_multi_coordinate_system_grid_mapping(Mixin__nc_load_actions): + def test_two_coord_systems(self, osgb_cs, latlon_cs, mocker, tmp_path): + """Test load a well described multi coordinate system variable.""" + cdl = f""" + netcdf tmp {{ + dimensions: + x = 4 ; + y = 3 ; + variables: + float phenom(y, x) ; + phenom:standard_name = "air_pressure" ; + phenom:units = "Pa" ; + phenom:coordinates = "lat lon" ; + phenom:grid_mapping = "crsOSGB: x y crsWGS84: lat lon" ; + {osgb_cs} + {latlon_cs} + }} + """ + nc_path = str(tmp_path / "tmp.nc") + cube = self.load_cube_from_cdl(cdl, None, nc_path, mocker) + + assert len(cube.coord_systems()) == 2 + + for coord_name in ["projection_x_coordinate", "projection_x_coordinate"]: + assert isinstance( + cube.coord(coord_name).coord_system, + iris.coord_systems.TransverseMercator, + ) + for coord_name in ["longitude", "latitude"]: + assert isinstance( + cube.coord(coord_name).coord_system, iris.coord_systems.GeogCS + ) + + # TODO: assert cube.ordered_axes + + def test_two_coord_systems_missing_coord( + self, osgb_cs, latlon_cs_missing_coord, mocker, tmp_path + ): + """Test missing coord in grid_mapping raises warning.""" + cdl = f""" + netcdf tmp {{ + dimensions: + x = 4 ; + y = 3 ; + variables: + float phenom(y, x) ; + phenom:standard_name = "air_pressure" ; + phenom:units = "Pa" ; + phenom:coordinates = "lat" ; + phenom:grid_mapping = "crsOSGB: x y crsWGS84: lat lon" ; + {osgb_cs} + {latlon_cs_missing_coord} + }} + """ + nc_path = str(tmp_path / "tmp.nc") + + # loader will warn that it can't find the longitude coord + with pytest.warns( + iris.warnings.IrisCfWarning, + match="Missing CF-netCDF coordinate variable 'lon'", + ): + cube = self.load_cube_from_cdl(cdl, None, nc_path, mocker) + + assert len(cube.coords()) == 3 + assert len(cube.coord_systems()) == 2 + + for coord_name in ["projection_x_coordinate", "projection_x_coordinate"]: + assert isinstance( + cube.coord(coord_name).coord_system, + iris.coord_systems.TransverseMercator, + ) + for coord_name in ["latitude"]: + assert isinstance( + cube.coord(coord_name).coord_system, iris.coord_systems.GeogCS + ) + + def test_two_coord_systems_missing_aux_crs( + self, osgb_cs, latlon_cs, mocker, tmp_path + ): + """Test invalid coordinate system mapping for Aux coords.""" + cdl = f""" + netcdf tmp {{ + dimensions: + x = 4 ; + y = 3 ; + variables: + float phenom(y, x) ; + phenom:standard_name = "air_pressure" ; + phenom:units = "Pa" ; + phenom:coordinates = "lat lon" ; + phenom:grid_mapping = "crsOSGB: x y non_existent_crs: lat lon" ; + {osgb_cs} + {latlon_cs} + }} + """ + nc_path = str(tmp_path / "tmp.nc") + + # loader will warn that it can't find the longitude coord + with pytest.warns( + iris.warnings.IrisCfWarning, + match="Missing CF-netCDF grid mapping variable 'non_existent_crs'", + ): + cube = self.load_cube_from_cdl(cdl, None, nc_path, mocker) + + assert len(cube.coords()) == 4 + assert len(cube.coord_systems()) == 1 + + for coord_name in ["projection_x_coordinate", "projection_x_coordinate"]: + assert isinstance( + cube.coord(coord_name).coord_system, + iris.coord_systems.TransverseMercator, + ) + for coord_name in ["latitude", "longitude"]: + assert cube.coord(coord_name).coord_system is None + + def test_two_coord_systems_missing_dim_crs( + self, osgb_cs, latlon_cs, mocker, tmp_path + ): + """Test invalid coordinate system mapping for Dim coords.""" + cdl = f""" + netcdf tmp {{ + dimensions: + x = 4 ; + y = 3 ; + variables: + float phenom(y, x) ; + phenom:standard_name = "air_pressure" ; + phenom:units = "Pa" ; + phenom:coordinates = "lat lon" ; + phenom:grid_mapping = "non_existent_crs: x y crsWGS84: lat lon" ; + {osgb_cs} + {latlon_cs} + }} + """ + nc_path = str(tmp_path / "tmp.nc") + + # loader will warn that it can't find the longitude coord + with pytest.warns( + iris.warnings.IrisCfWarning, + match="Missing CF-netCDF grid mapping variable 'non_existent_crs'", + ): + cube = self.load_cube_from_cdl(cdl, None, nc_path, mocker) + + assert len(cube.coords()) == 2 # DimCoords won't be found + assert len(cube.coord_systems()) == 1 + + for coord_name in ["latitude", "longitude"]: + assert isinstance( + cube.coord(coord_name).coord_system, iris.coord_systems.GeogCS + ) + + def test_two_coord_systems_invalid_grid_mapping( + self, osgb_cs, latlon_cs, mocker, tmp_path + ): + """Test invalid grid mapping doesn't load any coord systems and warns.""" + cdl = f""" + netcdf tmp {{ + dimensions: + x = 4 ; + y = 3 ; + variables: + float phenom(y, x) ; + phenom:standard_name = "air_pressure" ; + phenom:units = "Pa" ; + phenom:coordinates = "lat lon" ; + phenom:grid_mapping = "crsOSGB: crsWGS84:" ; + {osgb_cs} + {latlon_cs} + }} + """ + nc_path = str(tmp_path / "tmp.nc") + + # loader will warn that grid_mapping is invalid + with pytest.warns( + iris.warnings.IrisCfWarning, match="Error parsing `grid_mapping` attribute" + ): + cube = self.load_cube_from_cdl(cdl, None, nc_path, mocker) + + assert len(cube.coords()) == 2 + assert len(cube.coord_systems()) == 0 + for coord in cube.coords(): + assert coord.coord_system is None + + if __name__ == "__main__": tests.main() From 4c693c0ae5e1201264ced63c65c367dff00f8253 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Tue, 8 Jul 2025 12:39:27 +0100 Subject: [PATCH 37/58] Only create CFGridMappingVariable if at least one referenced coord exists --- lib/iris/fileformats/cf.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 7cdc9c2e1d..09e995e235 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -704,18 +704,25 @@ def identify( ) else: # For extended grid_mapping, also check coord references exist: + has_a_valid_coord = False if coords: for coord_name in coords: # coord_name could be None if simple grid_mapping is used. - if coord_name and coord_name not in variables: + if coord_name is None or ( + coord_name and coord_name in variables + ): + has_a_valid_coord = True + else: message = "Missing CF-netCDF coordinate variable %r (associated with grid mapping variable %r), referenced by netCDF variable %r" warnings.warn( message % (coord_name, name, nc_var_name), category=iris.warnings.IrisCfMissingVarWarning, ) - # TODO: Question: A missing coord reference will not stop the coord_system from - # being added as a CFGridMappingVariable. Is this ok? - result[name] = CFGridMappingVariable(name, variables[name]) + # Only add as a CFGridMappingVariable if at least one of its referenced coords exists: + if has_a_valid_coord: + result[name] = CFGridMappingVariable( + name, variables[name] + ) return result From bf6dbd63b54431735af9da485ab0ab094c36fe65 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Tue, 8 Jul 2025 12:49:17 +0100 Subject: [PATCH 38/58] New CFParseError exception --- lib/iris/exceptions.py | 6 ++++++ lib/iris/fileformats/_nc_load_rules/helpers.py | 4 ++-- lib/iris/fileformats/cf.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/iris/exceptions.py b/lib/iris/exceptions.py index 450afce3a6..870a64901f 100644 --- a/lib/iris/exceptions.py +++ b/lib/iris/exceptions.py @@ -172,3 +172,9 @@ def __str__(self): "operations are not currently supported." ) return msg.format(super().__str__()) + + +class CFParseError(IrisError): + """Raised when a string associated with a CF defined syntax could not be parsed.""" + + pass diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index fccaeccc5f..49ef35d901 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -1995,13 +1995,13 @@ def _parse_extended_grid_mapping(grid_mapping: str) -> dict[None | str, str]: for v_re, v_msg in _GRID_MAPPING_VALIDATORS: if len(match := v_re.findall(grid_mapping)): msg = f"Invalid syntax in extended grid_mapping: {grid_mapping!r}\n{v_msg} : {match}" - raise iris.exceptions.IrisError(msg) # TODO: Better Exception type + raise iris.exceptions.CFParseError(msg) # 2. Parse grid_mapping into list of [cs, (coords, ...)]: result = _GRID_MAPPING_PARSE_EXTENDED.findall(grid_mapping) if len(result) == 0: msg = f"Failed to parse grid_mapping: {grid_mapping!r}" - raise iris.exceptions.IrisError(msg) # TODO: Better exception type + raise iris.exceptions.CFParseError(msg) # split second match group into list of coordinates: mappings = {} diff --git a/lib/iris/fileformats/cf.py b/lib/iris/fileformats/cf.py index 09e995e235..23186072a4 100644 --- a/lib/iris/fileformats/cf.py +++ b/lib/iris/fileformats/cf.py @@ -1451,7 +1451,7 @@ def _translate(self, variables): try: cs_mappings = hh._parse_extended_grid_mapping(grid_mapping_attr) self._coord_system_mappings[nc_var.name] = cs_mappings - except iris.exceptions.IrisError as e: + except iris.exceptions.CFParseError as e: msg = f"Error parsing `grid_mapping` attribute for {nc_var.name}: {str(e)}" warnings.warn(msg, category=iris.warnings.IrisCfWarning) continue From 3839b01c7457999b5693476d1f8155bff27f5909 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Tue, 8 Jul 2025 14:54:38 +0100 Subject: [PATCH 39/58] Move setting of ordered_axes property to loader.py + added tests. --- lib/iris/cube.py | 7 ++- .../fileformats/_nc_load_rules/helpers.py | 2 - lib/iris/fileformats/netcdf/loader.py | 5 +++ .../actions/test__grid_mappings.py | 44 ++++++++++++++++++- 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 88bdd44996..1850766bbb 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1291,6 +1291,9 @@ def __init__( ] = [] self._aux_factories: list[AuxCoordFactory] = [] + # Default ordered_axes property to False; requires explicit opt-in + self._ordered_axes = False + # Cell Measures self._cell_measures_and_dims: list[tuple[CellMeasure, tuple[int, ...]]] = [] @@ -1332,10 +1335,6 @@ def __init__( for ancillary_variable, avdims in ancillary_variables_and_dims: self.add_ancillary_variable(ancillary_variable, avdims) - # Set ordered_axis property - if we have more that one coord system then - # it should be True. - self._ordered_axes = len(self.coord_systems()) > 1 - @property def _names(self) -> tuple[str | None, str | None, str | None, str | None]: """Tuple containing the value of each name participating in the identity of a :class:`iris.cube.Cube`. diff --git a/lib/iris/fileformats/_nc_load_rules/helpers.py b/lib/iris/fileformats/_nc_load_rules/helpers.py index 49ef35d901..35c2e96924 100644 --- a/lib/iris/fileformats/_nc_load_rules/helpers.py +++ b/lib/iris/fileformats/_nc_load_rules/helpers.py @@ -2005,8 +2005,6 @@ def _parse_extended_grid_mapping(grid_mapping: str) -> dict[None | str, str]: # split second match group into list of coordinates: mappings = {} - # TODO: below could possibly be a nested list/dict comprehension, but wold - # likely be overly complicated? for cs, coords in result: mappings.update({coord: cs for coord in coords.split()}) diff --git a/lib/iris/fileformats/netcdf/loader.py b/lib/iris/fileformats/netcdf/loader.py index 254cc3fa06..985ce902ed 100644 --- a/lib/iris/fileformats/netcdf/loader.py +++ b/lib/iris/fileformats/netcdf/loader.py @@ -451,6 +451,11 @@ def fix_attributes_all_elements(role_name): for method in cube.cell_methods ] + # Set ordered_axis property if extended grid_mapping was used + if cs_mappings := engine.cube_parts.get("coordinate_system_mappings", None): + # None as a mapping key implies simple mapping syntax (single coord system) + cube.ordered_axes = None not in cs_mappings + if DEBUG: # Show activation statistics for this data-var (i.e. cube). _actions_activation_stats(engine, cf_var.cf_name) diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py index b52464d365..b6210a2392 100644 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py @@ -1044,7 +1044,8 @@ def test_two_coord_systems(self, osgb_cs, latlon_cs, mocker, tmp_path): cube.coord(coord_name).coord_system, iris.coord_systems.GeogCS ) - # TODO: assert cube.ordered_axes + # Loading multiple coord systems or using extended grid mapping implies ordered axes: + assert cube.ordered_axes is True def test_two_coord_systems_missing_coord( self, osgb_cs, latlon_cs_missing_coord, mocker, tmp_path @@ -1087,6 +1088,8 @@ def test_two_coord_systems_missing_coord( cube.coord(coord_name).coord_system, iris.coord_systems.GeogCS ) + assert cube.ordered_axes is True + def test_two_coord_systems_missing_aux_crs( self, osgb_cs, latlon_cs, mocker, tmp_path ): @@ -1126,6 +1129,8 @@ def test_two_coord_systems_missing_aux_crs( for coord_name in ["latitude", "longitude"]: assert cube.coord(coord_name).coord_system is None + assert cube.ordered_axes is True + def test_two_coord_systems_missing_dim_crs( self, osgb_cs, latlon_cs, mocker, tmp_path ): @@ -1162,6 +1167,8 @@ def test_two_coord_systems_missing_dim_crs( cube.coord(coord_name).coord_system, iris.coord_systems.GeogCS ) + assert cube.ordered_axes is True + def test_two_coord_systems_invalid_grid_mapping( self, osgb_cs, latlon_cs, mocker, tmp_path ): @@ -1194,6 +1201,41 @@ def test_two_coord_systems_invalid_grid_mapping( for coord in cube.coords(): assert coord.coord_system is None + assert cube.ordered_axes is False + + def test_one_coord_system_simple(self, osgb_cs, latlon_cs, mocker, tmp_path): + """Make sure the simple coord system syntax still works.""" + cdl = f""" + netcdf tmp {{ + dimensions: + x = 4 ; + y = 3 ; + variables: + float phenom(y, x) ; + phenom:standard_name = "air_pressure" ; + phenom:units = "Pa" ; + phenom:coordinates = "lat lon" ; + phenom:grid_mapping = "crsOSGB" ; + {osgb_cs} + {latlon_cs} + }} + """ + nc_path = str(tmp_path / "tmp.nc") + cube = self.load_cube_from_cdl(cdl, None, nc_path, mocker) + + assert len(cube.coord_systems()) == 1 + + for coord_name in ["projection_x_coordinate", "projection_x_coordinate"]: + assert isinstance( + cube.coord(coord_name).coord_system, + iris.coord_systems.TransverseMercator, + ) + for coord_name in ["longitude", "latitude"]: + assert cube.coord(coord_name).coord_system is None + + # Loading multiple coord systems or using extended grid mapping implies ordered axes: + assert cube.ordered_axes is False + if __name__ == "__main__": tests.main() From 418549f7d337981f07779e12c9bb2d221f97d0e3 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Tue, 8 Jul 2025 16:00:45 +0100 Subject: [PATCH 40/58] Removed the WKT attr from the CDL of some tests --- .../unit/fileformats/netcdf/saver/Saver/write/mercator.cdl | 1 - .../netcdf/saver/Saver/write/mercator_no_ellipsoid.cdl | 1 - .../unit/fileformats/netcdf/saver/Saver/write/stereographic.cdl | 1 - .../netcdf/saver/Saver/write/stereographic_no_ellipsoid.cdl | 1 - .../netcdf/saver/Saver/write/stereographic_scale_factor.cdl | 1 - .../fileformats/netcdf/saver/Saver/write/transverse_mercator.cdl | 1 - .../saver/Saver/write/transverse_mercator_no_ellipsoid.cdl | 1 - 7 files changed, 7 deletions(-) diff --git a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/mercator.cdl b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/mercator.cdl index e45acd8ef0..ea9a1c283b 100644 --- a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/mercator.cdl +++ b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/mercator.cdl @@ -14,7 +14,6 @@ variables: mercator:false_easting = 0. ; mercator:false_northing = 0. ; mercator:standard_parallel = 0. ; - mercator:crs_wkt = "PROJCRS[\"unknown\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6377563.396,299.324961266495,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],CONVERSION[\"unknown\",METHOD[\"Mercator (variant B)\",ID[\"EPSG\",9805]],PARAMETER[\"Latitude of 1st standard parallel\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8823]],PARAMETER[\"Longitude of natural origin\",49,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8802]],PARAMETER[\"False easting\",0,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8806]],PARAMETER[\"False northing\",0,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8807]]],CS[Cartesian,2],AXIS[\"(E)\",east,ORDER[1],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]],AXIS[\"(N)\",north,ORDER[2],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]]" ; int64 projection_y_coordinate(projection_y_coordinate) ; projection_y_coordinate:axis = "Y" ; projection_y_coordinate:units = "m" ; diff --git a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/mercator_no_ellipsoid.cdl b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/mercator_no_ellipsoid.cdl index e7651e2c00..73b692ed63 100644 --- a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/mercator_no_ellipsoid.cdl +++ b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/mercator_no_ellipsoid.cdl @@ -11,7 +11,6 @@ variables: mercator:false_easting = 0. ; mercator:false_northing = 0. ; mercator:standard_parallel = 0. ; - mercator:crs_wkt = "PROJCRS[\"unknown\",BASEGEOGCRS[\"unknown\",DATUM[\"Unknown based on WGS 84 ellipsoid\",ELLIPSOID[\"WGS 84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",7030]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],CONVERSION[\"unknown\",METHOD[\"Mercator (variant B)\",ID[\"EPSG\",9805]],PARAMETER[\"Latitude of 1st standard parallel\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8823]],PARAMETER[\"Longitude of natural origin\",49,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8802]],PARAMETER[\"False easting\",0,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8806]],PARAMETER[\"False northing\",0,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8807]]],CS[Cartesian,2],AXIS[\"(E)\",east,ORDER[1],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]],AXIS[\"(N)\",north,ORDER[2],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]]" ; int64 projection_y_coordinate(projection_y_coordinate) ; projection_y_coordinate:axis = "Y" ; projection_y_coordinate:units = "m" ; diff --git a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/stereographic.cdl b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/stereographic.cdl index 76c30e178c..c021859121 100644 --- a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/stereographic.cdl +++ b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/stereographic.cdl @@ -15,7 +15,6 @@ variables: stereographic:false_easting = 500000. ; stereographic:false_northing = -200000. ; stereographic:scale_factor_at_projection_origin = 1. ; - stereographic:crs_wkt = "PROJCRS[\"unknown\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6377563.396,299.324961266495,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],CONVERSION[\"unknown\",METHOD[\"Stereographic\"],PARAMETER[\"Latitude of natural origin\",-10,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8801]],PARAMETER[\"Longitude of natural origin\",20,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8802]],PARAMETER[\"Scale factor at natural origin\",1,SCALEUNIT[\"unity\",1],ID[\"EPSG\",8805]],PARAMETER[\"False easting\",500000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8806]],PARAMETER[\"False northing\",-200000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8807]]],CS[Cartesian,2],AXIS[\"(E)\",east,ORDER[1],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]],AXIS[\"(N)\",north,ORDER[2],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]]" ; int64 projection_y_coordinate(projection_y_coordinate) ; projection_y_coordinate:axis = "Y" ; projection_y_coordinate:units = "m" ; diff --git a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/stereographic_no_ellipsoid.cdl b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/stereographic_no_ellipsoid.cdl index 2eca37488d..7b21325fbb 100644 --- a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/stereographic_no_ellipsoid.cdl +++ b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/stereographic_no_ellipsoid.cdl @@ -12,7 +12,6 @@ variables: stereographic:false_easting = 500000. ; stereographic:false_northing = -200000. ; stereographic:scale_factor_at_projection_origin = 1. ; - stereographic:crs_wkt = "PROJCRS[\"unknown\",BASEGEOGCRS[\"unknown\",DATUM[\"Unknown based on WGS 84 ellipsoid\",ELLIPSOID[\"WGS 84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",7030]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],CONVERSION[\"unknown\",METHOD[\"Stereographic\"],PARAMETER[\"Latitude of natural origin\",-10,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8801]],PARAMETER[\"Longitude of natural origin\",20,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8802]],PARAMETER[\"Scale factor at natural origin\",1,SCALEUNIT[\"unity\",1],ID[\"EPSG\",8805]],PARAMETER[\"False easting\",500000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8806]],PARAMETER[\"False northing\",-200000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8807]]],CS[Cartesian,2],AXIS[\"(E)\",east,ORDER[1],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]],AXIS[\"(N)\",north,ORDER[2],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]]" ; int64 projection_y_coordinate(projection_y_coordinate) ; projection_y_coordinate:axis = "Y" ; projection_y_coordinate:units = "m" ; diff --git a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/stereographic_scale_factor.cdl b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/stereographic_scale_factor.cdl index 4824aadf64..a11dc60c30 100644 --- a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/stereographic_scale_factor.cdl +++ b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/stereographic_scale_factor.cdl @@ -12,7 +12,6 @@ variables: stereographic:false_easting = 500000. ; stereographic:false_northing = -200000. ; stereographic:scale_factor_at_projection_origin = 1.3 ; - stereographic:crs_wkt = "PROJCRS[\"unknown\",BASEGEOGCRS[\"unknown\",DATUM[\"Unknown based on WGS 84 ellipsoid\",ELLIPSOID[\"WGS 84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",7030]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],CONVERSION[\"unknown\",METHOD[\"Stereographic\"],PARAMETER[\"Latitude of natural origin\",-10,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8801]],PARAMETER[\"Longitude of natural origin\",20,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8802]],PARAMETER[\"Scale factor at natural origin\",1.3,SCALEUNIT[\"unity\",1],ID[\"EPSG\",8805]],PARAMETER[\"False easting\",500000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8806]],PARAMETER[\"False northing\",-200000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8807]]],CS[Cartesian,2],AXIS[\"(E)\",east,ORDER[1],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]],AXIS[\"(N)\",north,ORDER[2],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]]" ; int64 projection_y_coordinate(projection_y_coordinate) ; projection_y_coordinate:axis = "Y" ; projection_y_coordinate:units = "m" ; diff --git a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/transverse_mercator.cdl b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/transverse_mercator.cdl index d80173d6a2..9a1d8e0431 100644 --- a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/transverse_mercator.cdl +++ b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/transverse_mercator.cdl @@ -15,7 +15,6 @@ variables: transverse_mercator:false_easting = -400000. ; transverse_mercator:false_northing = 100000. ; transverse_mercator:scale_factor_at_central_meridian = 0.9996012717 ; - transverse_mercator:crs_wkt = "PROJCRS[\"unknown\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6377563.396,299.324961266495,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],CONVERSION[\"unknown\",METHOD[\"Transverse Mercator\",ID[\"EPSG\",9807]],PARAMETER[\"Latitude of natural origin\",49,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8801]],PARAMETER[\"Longitude of natural origin\",-2,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8802]],PARAMETER[\"Scale factor at natural origin\",0.9996012717,SCALEUNIT[\"unity\",1],ID[\"EPSG\",8805]],PARAMETER[\"False easting\",-400000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8806]],PARAMETER[\"False northing\",100000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8807]]],CS[Cartesian,2],AXIS[\"(E)\",east,ORDER[1],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]],AXIS[\"(N)\",north,ORDER[2],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]]" ; int64 projection_y_coordinate(projection_y_coordinate) ; projection_y_coordinate:axis = "Y" ; projection_y_coordinate:units = "m" ; diff --git a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/transverse_mercator_no_ellipsoid.cdl b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/transverse_mercator_no_ellipsoid.cdl index df3a95d463..a7aee0a177 100644 --- a/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/transverse_mercator_no_ellipsoid.cdl +++ b/lib/iris/tests/results/unit/fileformats/netcdf/saver/Saver/write/transverse_mercator_no_ellipsoid.cdl @@ -12,7 +12,6 @@ variables: transverse_mercator:false_easting = -400000. ; transverse_mercator:false_northing = 100000. ; transverse_mercator:scale_factor_at_central_meridian = 0.9996012717 ; - transverse_mercator:crs_wkt = "PROJCRS[\"unknown\",BASEGEOGCRS[\"unknown\",DATUM[\"Unknown based on WGS 84 ellipsoid\",ELLIPSOID[\"WGS 84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",7030]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],CONVERSION[\"unknown\",METHOD[\"Transverse Mercator\",ID[\"EPSG\",9807]],PARAMETER[\"Latitude of natural origin\",49,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8801]],PARAMETER[\"Longitude of natural origin\",-2,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8802]],PARAMETER[\"Scale factor at natural origin\",0.9996012717,SCALEUNIT[\"unity\",1],ID[\"EPSG\",8805]],PARAMETER[\"False easting\",-400000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8806]],PARAMETER[\"False northing\",100000,LENGTHUNIT[\"metre\",1],ID[\"EPSG\",8807]]],CS[Cartesian,2],AXIS[\"(E)\",east,ORDER[1],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]],AXIS[\"(N)\",north,ORDER[2],LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]]" ; int64 projection_y_coordinate(projection_y_coordinate) ; projection_y_coordinate:axis = "Y" ; projection_y_coordinate:units = "m" ; From 8776d2c4f10ecc2c4a345a005a2d908234b1a7c6 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Tue, 8 Jul 2025 17:05:49 +0100 Subject: [PATCH 41/58] Remove expected WKT output from CDL of most tests. --- .../netcdf/aux_factories/TestAtmosphereSigma/save.cdl | 1 - .../netcdf/aux_factories/TestHybridPressure/save.cdl | 1 - .../hybrid_height_and_pressure.cdl | 1 - .../netcdf/general/TestPackedData/multi_packed_multi_dtype.cdl | 1 - .../general/TestPackedData/multi_packed_single_dtype.cdl | 1 - .../netcdf/general/TestPackedData/single_packed_manual.cdl | 1 - .../netcdf/general/TestPackedData/single_packed_signed.cdl | 1 - .../netcdf/general/TestPackedData/single_packed_unsigned.cdl | 1 - .../results/netcdf/TestNetCDFSave__ancillaries/aliases.cdl | 1 - .../tests/results/netcdf/TestNetCDFSave__ancillaries/flag.cdl | 1 - .../results/netcdf/TestNetCDFSave__ancillaries/fulldims.cdl | 1 - .../results/netcdf/TestNetCDFSave__ancillaries/multiple.cdl | 1 - .../results/netcdf/TestNetCDFSave__ancillaries/partialdims.cdl | 1 - .../results/netcdf/TestNetCDFSave__ancillaries/shared.cdl | 1 - lib/iris/tests/results/netcdf/netcdf_save_gridmapmulti.cdl | 3 --- lib/iris/tests/results/netcdf/netcdf_save_hybrid_height.cdl | 1 - lib/iris/tests/results/netcdf/netcdf_save_multi_0.cdl | 1 - lib/iris/tests/results/netcdf/netcdf_save_multi_1.cdl | 1 - lib/iris/tests/results/netcdf/netcdf_save_multi_2.cdl | 1 - lib/iris/tests/results/netcdf/netcdf_save_multiple.cdl | 1 - lib/iris/tests/results/netcdf/netcdf_save_ndim_auxiliary.cdl | 1 - lib/iris/tests/results/netcdf/netcdf_save_realistic_0d.cdl | 1 - lib/iris/tests/results/netcdf/netcdf_save_realistic_4d.cdl | 1 - .../results/netcdf/netcdf_save_realistic_4d_no_hybrid.cdl | 1 - lib/iris/tests/results/netcdf/netcdf_save_single.cdl | 1 - .../000003000000.03.236.000128.1990.12.01.00.00.b_0.cdl | 1 - .../000003000000.03.236.004224.1990.12.01.00.00.b_0.cdl | 1 - .../000003000000.03.236.008320.1990.12.01.00.00.b_0.cdl | 1 - .../000003000000.16.202.000128.1860.09.01.00.00.b_0.cdl | 1 - .../001000000000.00.000.000000.1860.01.01.00.00.f.b_0.cdl | 1 - .../002000000000.44.101.131200.1920.09.01.00.00.b_0.cdl | 1 - .../usecases/pp_to_cf_conversion/to_netcdf/12187.b_0.cdl | 1 - .../to_netcdf/HadCM2_ts_SAT_ann_18602100.b_0.cdl | 1 - .../pp_to_cf_conversion/to_netcdf/aaxzc_level_lat_orig.b_0.cdl | 1 - .../to_netcdf/aaxzc_lon_lat_press_orig.b_0.cdl | 1 - .../to_netcdf/aaxzc_lon_lat_several.b_0.cdl | 1 - .../pp_to_cf_conversion/to_netcdf/aaxzc_n10r13xy.b_0.cdl | 1 - .../to_netcdf/abcza_pa19591997_daily_29.b_0.cdl | 1 - .../to_netcdf/abcza_pa19591997_daily_29.b_1.cdl | 1 - .../to_netcdf/abcza_pa19591997_daily_29.b_2.cdl | 1 - .../pp_to_cf_conversion/to_netcdf/abxpa_press_lat.b_0.cdl | 1 - .../usecases/pp_to_cf_conversion/to_netcdf/integer.b_0.cdl | 1 - .../usecases/pp_to_cf_conversion/to_netcdf/model.b_0.cdl | 1 - .../usecases/pp_to_cf_conversion/to_netcdf/ocean_xsect.b_0.cdl | 1 - .../usecases/pp_to_cf_conversion/to_netcdf/st0fc699.b_0.cdl | 1 - .../usecases/pp_to_cf_conversion/to_netcdf/st0fc942.b_0.cdl | 1 - .../usecases/pp_to_cf_conversion/to_netcdf/st30211.b_0.cdl | 1 - 47 files changed, 49 deletions(-) diff --git a/lib/iris/tests/results/integration/netcdf/aux_factories/TestAtmosphereSigma/save.cdl b/lib/iris/tests/results/integration/netcdf/aux_factories/TestAtmosphereSigma/save.cdl index 29c6a6ef6e..762226192c 100644 --- a/lib/iris/tests/results/integration/netcdf/aux_factories/TestAtmosphereSigma/save.cdl +++ b/lib/iris/tests/results/integration/netcdf/aux_factories/TestAtmosphereSigma/save.cdl @@ -17,7 +17,6 @@ variables: rotated_latitude_longitude:grid_north_pole_latitude = 37.5 ; rotated_latitude_longitude:grid_north_pole_longitude = 177.5 ; rotated_latitude_longitude:north_pole_grid_longitude = 0. ; - rotated_latitude_longitude:crs_wkt = "GEOGCRS[\"unnamed\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],DERIVINGCONVERSION[\"unknown\",METHOD[\"PROJ ob_tran o_proj=latlon\"],PARAMETER[\"o_lon_p\",0,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"o_lat_p\",37.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"lon_0\",357.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:units = "hours since 1970-01-01 00:00:00" ; diff --git a/lib/iris/tests/results/integration/netcdf/aux_factories/TestHybridPressure/save.cdl b/lib/iris/tests/results/integration/netcdf/aux_factories/TestHybridPressure/save.cdl index f0ae5dc036..6fed33430a 100644 --- a/lib/iris/tests/results/integration/netcdf/aux_factories/TestHybridPressure/save.cdl +++ b/lib/iris/tests/results/integration/netcdf/aux_factories/TestHybridPressure/save.cdl @@ -17,7 +17,6 @@ variables: rotated_latitude_longitude:grid_north_pole_latitude = 37.5 ; rotated_latitude_longitude:grid_north_pole_longitude = 177.5 ; rotated_latitude_longitude:north_pole_grid_longitude = 0. ; - rotated_latitude_longitude:crs_wkt = "GEOGCRS[\"unnamed\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],DERIVINGCONVERSION[\"unknown\",METHOD[\"PROJ ob_tran o_proj=latlon\"],PARAMETER[\"o_lon_p\",0,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"o_lat_p\",37.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"lon_0\",357.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:units = "hours since 1970-01-01 00:00:00" ; diff --git a/lib/iris/tests/results/integration/netcdf/aux_factories/TestSaveMultipleAuxFactories/hybrid_height_and_pressure.cdl b/lib/iris/tests/results/integration/netcdf/aux_factories/TestSaveMultipleAuxFactories/hybrid_height_and_pressure.cdl index df92f1fa38..d813ab98dc 100644 --- a/lib/iris/tests/results/integration/netcdf/aux_factories/TestSaveMultipleAuxFactories/hybrid_height_and_pressure.cdl +++ b/lib/iris/tests/results/integration/netcdf/aux_factories/TestSaveMultipleAuxFactories/hybrid_height_and_pressure.cdl @@ -17,7 +17,6 @@ variables: rotated_latitude_longitude:grid_north_pole_latitude = 37.5 ; rotated_latitude_longitude:grid_north_pole_longitude = 177.5 ; rotated_latitude_longitude:north_pole_grid_longitude = 0. ; - rotated_latitude_longitude:crs_wkt = "GEOGCRS[\"unnamed\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],DERIVINGCONVERSION[\"unknown\",METHOD[\"PROJ ob_tran o_proj=latlon\"],PARAMETER[\"o_lon_p\",0,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"o_lat_p\",37.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"lon_0\",357.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:units = "hours since 1970-01-01 00:00:00" ; diff --git a/lib/iris/tests/results/integration/netcdf/general/TestPackedData/multi_packed_multi_dtype.cdl b/lib/iris/tests/results/integration/netcdf/general/TestPackedData/multi_packed_multi_dtype.cdl index 367472c127..8a8f481492 100644 --- a/lib/iris/tests/results/integration/netcdf/general/TestPackedData/multi_packed_multi_dtype.cdl +++ b/lib/iris/tests/results/integration/netcdf/general/TestPackedData/multi_packed_multi_dtype.cdl @@ -17,7 +17,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:bounds = "time_bnds" ; diff --git a/lib/iris/tests/results/integration/netcdf/general/TestPackedData/multi_packed_single_dtype.cdl b/lib/iris/tests/results/integration/netcdf/general/TestPackedData/multi_packed_single_dtype.cdl index 8f3b1b5084..3f2c909ce8 100644 --- a/lib/iris/tests/results/integration/netcdf/general/TestPackedData/multi_packed_single_dtype.cdl +++ b/lib/iris/tests/results/integration/netcdf/general/TestPackedData/multi_packed_single_dtype.cdl @@ -17,7 +17,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:bounds = "time_bnds" ; diff --git a/lib/iris/tests/results/integration/netcdf/general/TestPackedData/single_packed_manual.cdl b/lib/iris/tests/results/integration/netcdf/general/TestPackedData/single_packed_manual.cdl index 3dacf82e5f..83e7329575 100644 --- a/lib/iris/tests/results/integration/netcdf/general/TestPackedData/single_packed_manual.cdl +++ b/lib/iris/tests/results/integration/netcdf/general/TestPackedData/single_packed_manual.cdl @@ -16,7 +16,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; float latitude(latitude) ; latitude:axis = "Y" ; latitude:units = "degrees_north" ; diff --git a/lib/iris/tests/results/integration/netcdf/general/TestPackedData/single_packed_signed.cdl b/lib/iris/tests/results/integration/netcdf/general/TestPackedData/single_packed_signed.cdl index 3dacf82e5f..83e7329575 100644 --- a/lib/iris/tests/results/integration/netcdf/general/TestPackedData/single_packed_signed.cdl +++ b/lib/iris/tests/results/integration/netcdf/general/TestPackedData/single_packed_signed.cdl @@ -16,7 +16,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; float latitude(latitude) ; latitude:axis = "Y" ; latitude:units = "degrees_north" ; diff --git a/lib/iris/tests/results/integration/netcdf/general/TestPackedData/single_packed_unsigned.cdl b/lib/iris/tests/results/integration/netcdf/general/TestPackedData/single_packed_unsigned.cdl index da89e90094..7b9114309e 100644 --- a/lib/iris/tests/results/integration/netcdf/general/TestPackedData/single_packed_unsigned.cdl +++ b/lib/iris/tests/results/integration/netcdf/general/TestPackedData/single_packed_unsigned.cdl @@ -16,7 +16,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; float latitude(latitude) ; latitude:axis = "Y" ; latitude:units = "degrees_north" ; diff --git a/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/aliases.cdl b/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/aliases.cdl index 5d7ccbfe54..da0d1d10db 100644 --- a/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/aliases.cdl +++ b/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/aliases.cdl @@ -16,7 +16,6 @@ variables: rotated_latitude_longitude:grid_north_pole_latitude = 37.5 ; rotated_latitude_longitude:grid_north_pole_longitude = 177.5 ; rotated_latitude_longitude:north_pole_grid_longitude = 0. ; - rotated_latitude_longitude:crs_wkt = "GEOGCRS[\"unnamed\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],DERIVINGCONVERSION[\"unknown\",METHOD[\"PROJ ob_tran o_proj=latlon\"],PARAMETER[\"o_lon_p\",0,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"o_lat_p\",37.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"lon_0\",357.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:units = "hours since 1970-01-01 00:00:00" ; diff --git a/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/flag.cdl b/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/flag.cdl index 09760efbaa..ef1ef973e2 100644 --- a/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/flag.cdl +++ b/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/flag.cdl @@ -16,7 +16,6 @@ variables: rotated_latitude_longitude:grid_north_pole_latitude = 37.5 ; rotated_latitude_longitude:grid_north_pole_longitude = 177.5 ; rotated_latitude_longitude:north_pole_grid_longitude = 0. ; - rotated_latitude_longitude:crs_wkt = "GEOGCRS[\"unnamed\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],DERIVINGCONVERSION[\"unknown\",METHOD[\"PROJ ob_tran o_proj=latlon\"],PARAMETER[\"o_lon_p\",0,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"o_lat_p\",37.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"lon_0\",357.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:units = "hours since 1970-01-01 00:00:00" ; diff --git a/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/fulldims.cdl b/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/fulldims.cdl index 4ce8cc11b8..1d33942464 100644 --- a/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/fulldims.cdl +++ b/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/fulldims.cdl @@ -16,7 +16,6 @@ variables: rotated_latitude_longitude:grid_north_pole_latitude = 37.5 ; rotated_latitude_longitude:grid_north_pole_longitude = 177.5 ; rotated_latitude_longitude:north_pole_grid_longitude = 0. ; - rotated_latitude_longitude:crs_wkt = "GEOGCRS[\"unnamed\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],DERIVINGCONVERSION[\"unknown\",METHOD[\"PROJ ob_tran o_proj=latlon\"],PARAMETER[\"o_lon_p\",0,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"o_lat_p\",37.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"lon_0\",357.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:units = "hours since 1970-01-01 00:00:00" ; diff --git a/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/multiple.cdl b/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/multiple.cdl index 44bb7976cb..5a0edc7528 100644 --- a/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/multiple.cdl +++ b/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/multiple.cdl @@ -16,7 +16,6 @@ variables: rotated_latitude_longitude:grid_north_pole_latitude = 37.5 ; rotated_latitude_longitude:grid_north_pole_longitude = 177.5 ; rotated_latitude_longitude:north_pole_grid_longitude = 0. ; - rotated_latitude_longitude:crs_wkt = "GEOGCRS[\"unnamed\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],DERIVINGCONVERSION[\"unknown\",METHOD[\"PROJ ob_tran o_proj=latlon\"],PARAMETER[\"o_lon_p\",0,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"o_lat_p\",37.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"lon_0\",357.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:units = "hours since 1970-01-01 00:00:00" ; diff --git a/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/partialdims.cdl b/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/partialdims.cdl index 80eaac153f..81d32bf80c 100644 --- a/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/partialdims.cdl +++ b/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/partialdims.cdl @@ -16,7 +16,6 @@ variables: rotated_latitude_longitude:grid_north_pole_latitude = 37.5 ; rotated_latitude_longitude:grid_north_pole_longitude = 177.5 ; rotated_latitude_longitude:north_pole_grid_longitude = 0. ; - rotated_latitude_longitude:crs_wkt = "GEOGCRS[\"unnamed\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],DERIVINGCONVERSION[\"unknown\",METHOD[\"PROJ ob_tran o_proj=latlon\"],PARAMETER[\"o_lon_p\",0,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"o_lat_p\",37.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"lon_0\",357.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:units = "hours since 1970-01-01 00:00:00" ; diff --git a/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/shared.cdl b/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/shared.cdl index e11d921131..c6b29c5bda 100644 --- a/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/shared.cdl +++ b/lib/iris/tests/results/netcdf/TestNetCDFSave__ancillaries/shared.cdl @@ -16,7 +16,6 @@ variables: rotated_latitude_longitude:grid_north_pole_latitude = 37.5 ; rotated_latitude_longitude:grid_north_pole_longitude = 177.5 ; rotated_latitude_longitude:north_pole_grid_longitude = 0. ; - rotated_latitude_longitude:crs_wkt = "GEOGCRS[\"unnamed\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],DERIVINGCONVERSION[\"unknown\",METHOD[\"PROJ ob_tran o_proj=latlon\"],PARAMETER[\"o_lon_p\",0,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"o_lat_p\",37.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"lon_0\",357.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:units = "hours since 1970-01-01 00:00:00" ; diff --git a/lib/iris/tests/results/netcdf/netcdf_save_gridmapmulti.cdl b/lib/iris/tests/results/netcdf/netcdf_save_gridmapmulti.cdl index c8dc831bf2..c998f129d3 100644 --- a/lib/iris/tests/results/netcdf/netcdf_save_gridmapmulti.cdl +++ b/lib/iris/tests/results/netcdf/netcdf_save_gridmapmulti.cdl @@ -13,7 +13,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; int64 longitude(longitude) ; longitude:axis = "X" ; longitude:units = "degrees_east" ; @@ -31,7 +30,6 @@ variables: latitude_longitude_0:grid_mapping_name = "latitude_longitude_0" ; latitude_longitude_0:longitude_of_prime_meridian = 0. ; latitude_longitude_0:earth_radius = 6371228. ; - latitude_longitude_0:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371228,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; int64 longitude_0(longitude_0) ; longitude_0:axis = "X" ; longitude_0:units = "degrees_east" ; @@ -50,7 +48,6 @@ variables: rotated_latitude_longitude:grid_north_pole_latitude = 30. ; rotated_latitude_longitude:grid_north_pole_longitude = 30. ; rotated_latitude_longitude:north_pole_grid_longitude = 0. ; - rotated_latitude_longitude:crs_wkt = "GEOGCRS[\"unnamed\",BASEGEOGCRS[\"unknown\",DATUM[\"World Geodetic System 1984\",ELLIPSOID[\"WGS 84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]],ID[\"EPSG\",6326]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],DERIVINGCONVERSION[\"unknown\",METHOD[\"PROJ ob_tran o_proj=latlon\"],PARAMETER[\"o_lon_p\",0,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"o_lat_p\",30,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"lon_0\",210,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; int64 grid_longitude(grid_longitude) ; grid_longitude:axis = "X" ; grid_longitude:units = "degrees" ; diff --git a/lib/iris/tests/results/netcdf/netcdf_save_hybrid_height.cdl b/lib/iris/tests/results/netcdf/netcdf_save_hybrid_height.cdl index b1327794d0..74a83c9714 100644 --- a/lib/iris/tests/results/netcdf/netcdf_save_hybrid_height.cdl +++ b/lib/iris/tests/results/netcdf/netcdf_save_hybrid_height.cdl @@ -18,7 +18,6 @@ variables: rotated_latitude_longitude:grid_north_pole_latitude = 37.5 ; rotated_latitude_longitude:grid_north_pole_longitude = 177.5 ; rotated_latitude_longitude:north_pole_grid_longitude = 0. ; - rotated_latitude_longitude:crs_wkt = "GEOGCRS[\"unnamed\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],DERIVINGCONVERSION[\"unknown\",METHOD[\"PROJ ob_tran o_proj=latlon\"],PARAMETER[\"o_lon_p\",0,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"o_lat_p\",37.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"lon_0\",357.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:units = "hours since 1970-01-01 00:00:00" ; diff --git a/lib/iris/tests/results/netcdf/netcdf_save_multi_0.cdl b/lib/iris/tests/results/netcdf/netcdf_save_multi_0.cdl index cf2b4b1bef..f4f8d6e88a 100644 --- a/lib/iris/tests/results/netcdf/netcdf_save_multi_0.cdl +++ b/lib/iris/tests/results/netcdf/netcdf_save_multi_0.cdl @@ -15,7 +15,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:bounds = "time_bnds" ; diff --git a/lib/iris/tests/results/netcdf/netcdf_save_multi_1.cdl b/lib/iris/tests/results/netcdf/netcdf_save_multi_1.cdl index 75319d17cc..50222b796e 100644 --- a/lib/iris/tests/results/netcdf/netcdf_save_multi_1.cdl +++ b/lib/iris/tests/results/netcdf/netcdf_save_multi_1.cdl @@ -15,7 +15,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:bounds = "time_bnds" ; diff --git a/lib/iris/tests/results/netcdf/netcdf_save_multi_2.cdl b/lib/iris/tests/results/netcdf/netcdf_save_multi_2.cdl index c9cc153daf..7761a3c45d 100644 --- a/lib/iris/tests/results/netcdf/netcdf_save_multi_2.cdl +++ b/lib/iris/tests/results/netcdf/netcdf_save_multi_2.cdl @@ -15,7 +15,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:bounds = "time_bnds" ; diff --git a/lib/iris/tests/results/netcdf/netcdf_save_multiple.cdl b/lib/iris/tests/results/netcdf/netcdf_save_multiple.cdl index 3aafd266a5..7ad1818bb6 100644 --- a/lib/iris/tests/results/netcdf/netcdf_save_multiple.cdl +++ b/lib/iris/tests/results/netcdf/netcdf_save_multiple.cdl @@ -15,7 +15,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:bounds = "time_bnds" ; diff --git a/lib/iris/tests/results/netcdf/netcdf_save_ndim_auxiliary.cdl b/lib/iris/tests/results/netcdf/netcdf_save_ndim_auxiliary.cdl index ea5a8009e6..f8180d4ea8 100644 --- a/lib/iris/tests/results/netcdf/netcdf_save_ndim_auxiliary.cdl +++ b/lib/iris/tests/results/netcdf/netcdf_save_ndim_auxiliary.cdl @@ -16,7 +16,6 @@ variables: rotated_latitude_longitude:grid_north_pole_latitude = 18. ; rotated_latitude_longitude:grid_north_pole_longitude = -140.75 ; rotated_latitude_longitude:north_pole_grid_longitude = 0. ; - rotated_latitude_longitude:crs_wkt = "GEOGCRS[\"unnamed\",BASEGEOGCRS[\"unknown\",DATUM[\"World Geodetic System 1984\",ELLIPSOID[\"WGS 84\",6378137,298.257223563,LENGTHUNIT[\"metre\",1]],ID[\"EPSG\",6326]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],DERIVINGCONVERSION[\"unknown\",METHOD[\"PROJ ob_tran o_proj=latlon\"],PARAMETER[\"o_lon_p\",0,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"o_lat_p\",18,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"lon_0\",39.25,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; float time(time) ; time:axis = "T" ; time:bounds = "time_bnds" ; diff --git a/lib/iris/tests/results/netcdf/netcdf_save_realistic_0d.cdl b/lib/iris/tests/results/netcdf/netcdf_save_realistic_0d.cdl index a8e9b315bd..642e46a905 100644 --- a/lib/iris/tests/results/netcdf/netcdf_save_realistic_0d.cdl +++ b/lib/iris/tests/results/netcdf/netcdf_save_realistic_0d.cdl @@ -13,7 +13,6 @@ variables: rotated_latitude_longitude:grid_north_pole_latitude = 37.5 ; rotated_latitude_longitude:grid_north_pole_longitude = 177.5 ; rotated_latitude_longitude:north_pole_grid_longitude = 0. ; - rotated_latitude_longitude:crs_wkt = "GEOGCRS[\"unnamed\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],DERIVINGCONVERSION[\"unknown\",METHOD[\"PROJ ob_tran o_proj=latlon\"],PARAMETER[\"o_lon_p\",0,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"o_lat_p\",37.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"lon_0\",357.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double forecast_period ; forecast_period:units = "hours" ; forecast_period:standard_name = "forecast_period" ; diff --git a/lib/iris/tests/results/netcdf/netcdf_save_realistic_4d.cdl b/lib/iris/tests/results/netcdf/netcdf_save_realistic_4d.cdl index 186d39a53a..d49e775024 100644 --- a/lib/iris/tests/results/netcdf/netcdf_save_realistic_4d.cdl +++ b/lib/iris/tests/results/netcdf/netcdf_save_realistic_4d.cdl @@ -17,7 +17,6 @@ variables: rotated_latitude_longitude:grid_north_pole_latitude = 37.5 ; rotated_latitude_longitude:grid_north_pole_longitude = 177.5 ; rotated_latitude_longitude:north_pole_grid_longitude = 0. ; - rotated_latitude_longitude:crs_wkt = "GEOGCRS[\"unnamed\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],DERIVINGCONVERSION[\"unknown\",METHOD[\"PROJ ob_tran o_proj=latlon\"],PARAMETER[\"o_lon_p\",0,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"o_lat_p\",37.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"lon_0\",357.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:units = "hours since 1970-01-01 00:00:00" ; diff --git a/lib/iris/tests/results/netcdf/netcdf_save_realistic_4d_no_hybrid.cdl b/lib/iris/tests/results/netcdf/netcdf_save_realistic_4d_no_hybrid.cdl index 731e4cf786..8353df60e9 100644 --- a/lib/iris/tests/results/netcdf/netcdf_save_realistic_4d_no_hybrid.cdl +++ b/lib/iris/tests/results/netcdf/netcdf_save_realistic_4d_no_hybrid.cdl @@ -17,7 +17,6 @@ variables: rotated_latitude_longitude:grid_north_pole_latitude = 37.5 ; rotated_latitude_longitude:grid_north_pole_longitude = 177.5 ; rotated_latitude_longitude:north_pole_grid_longitude = 0. ; - rotated_latitude_longitude:crs_wkt = "GEOGCRS[\"unnamed\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],DERIVINGCONVERSION[\"unknown\",METHOD[\"PROJ ob_tran o_proj=latlon\"],PARAMETER[\"o_lon_p\",0,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"o_lat_p\",37.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"lon_0\",357.5,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:units = "hours since 1970-01-01 00:00:00" ; diff --git a/lib/iris/tests/results/netcdf/netcdf_save_single.cdl b/lib/iris/tests/results/netcdf/netcdf_save_single.cdl index df2d3017ea..9847532001 100644 --- a/lib/iris/tests/results/netcdf/netcdf_save_single.cdl +++ b/lib/iris/tests/results/netcdf/netcdf_save_single.cdl @@ -14,7 +14,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; float latitude(latitude) ; latitude:axis = "Y" ; latitude:units = "degrees_north" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/000003000000.03.236.000128.1990.12.01.00.00.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/000003000000.03.236.000128.1990.12.01.00.00.b_0.cdl index 3742a3fce7..ddbbee5d34 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/000003000000.03.236.000128.1990.12.01.00.00.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/000003000000.03.236.000128.1990.12.01.00.00.b_0.cdl @@ -15,7 +15,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; float latitude(latitude) ; latitude:axis = "Y" ; latitude:units = "degrees_north" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/000003000000.03.236.004224.1990.12.01.00.00.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/000003000000.03.236.004224.1990.12.01.00.00.b_0.cdl index 3742a3fce7..ddbbee5d34 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/000003000000.03.236.004224.1990.12.01.00.00.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/000003000000.03.236.004224.1990.12.01.00.00.b_0.cdl @@ -15,7 +15,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; float latitude(latitude) ; latitude:axis = "Y" ; latitude:units = "degrees_north" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/000003000000.03.236.008320.1990.12.01.00.00.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/000003000000.03.236.008320.1990.12.01.00.00.b_0.cdl index 545792aa98..cb026fd7ae 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/000003000000.03.236.008320.1990.12.01.00.00.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/000003000000.03.236.008320.1990.12.01.00.00.b_0.cdl @@ -15,7 +15,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; float latitude(latitude) ; latitude:axis = "Y" ; latitude:units = "degrees_north" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/000003000000.16.202.000128.1860.09.01.00.00.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/000003000000.16.202.000128.1860.09.01.00.00.b_0.cdl index b95869f98b..e8f3f04d7d 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/000003000000.16.202.000128.1860.09.01.00.00.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/000003000000.16.202.000128.1860.09.01.00.00.b_0.cdl @@ -16,7 +16,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; float pressure(pressure) ; pressure:axis = "Z" ; pressure:units = "hPa" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/001000000000.00.000.000000.1860.01.01.00.00.f.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/001000000000.00.000.000000.1860.01.01.00.00.f.b_0.cdl index 9ffd7ae035..cb3a3bc2eb 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/001000000000.00.000.000000.1860.01.01.00.00.f.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/001000000000.00.000.000000.1860.01.01.00.00.f.b_0.cdl @@ -13,7 +13,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; float latitude(latitude) ; latitude:axis = "Y" ; latitude:units = "degrees_north" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/002000000000.44.101.131200.1920.09.01.00.00.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/002000000000.44.101.131200.1920.09.01.00.00.b_0.cdl index 1ecd199a2f..40ea329140 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/002000000000.44.101.131200.1920.09.01.00.00.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/002000000000.44.101.131200.1920.09.01.00.00.b_0.cdl @@ -13,7 +13,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; float depth(depth) ; depth:axis = "Z" ; depth:units = "m" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/12187.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/12187.b_0.cdl index dc3dfcb657..20607d69ba 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/12187.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/12187.b_0.cdl @@ -16,7 +16,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; int model_level_number(model_level_number) ; model_level_number:axis = "Z" ; model_level_number:units = "1" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/HadCM2_ts_SAT_ann_18602100.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/HadCM2_ts_SAT_ann_18602100.b_0.cdl index cd421fab8b..8a9498abce 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/HadCM2_ts_SAT_ann_18602100.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/HadCM2_ts_SAT_ann_18602100.b_0.cdl @@ -15,7 +15,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; float time(time) ; time:axis = "T" ; time:units = "days since 0000-01-01 00:00:00" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/aaxzc_level_lat_orig.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/aaxzc_level_lat_orig.b_0.cdl index 94d7cbd3bf..f9450c58ff 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/aaxzc_level_lat_orig.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/aaxzc_level_lat_orig.b_0.cdl @@ -16,7 +16,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:bounds = "time_bnds" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/aaxzc_lon_lat_press_orig.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/aaxzc_lon_lat_press_orig.b_0.cdl index 28f1d310fd..8d513798fd 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/aaxzc_lon_lat_press_orig.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/aaxzc_lon_lat_press_orig.b_0.cdl @@ -17,7 +17,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:bounds = "time_bnds" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/aaxzc_lon_lat_several.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/aaxzc_lon_lat_several.b_0.cdl index d50d412c98..de372487af 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/aaxzc_lon_lat_several.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/aaxzc_lon_lat_several.b_0.cdl @@ -16,7 +16,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:bounds = "time_bnds" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/aaxzc_n10r13xy.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/aaxzc_n10r13xy.b_0.cdl index 1991586855..105f25201f 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/aaxzc_n10r13xy.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/aaxzc_n10r13xy.b_0.cdl @@ -15,7 +15,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:bounds = "time_bnds" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/abcza_pa19591997_daily_29.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/abcza_pa19591997_daily_29.b_0.cdl index d1a57c4dbd..d3beb8f273 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/abcza_pa19591997_daily_29.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/abcza_pa19591997_daily_29.b_0.cdl @@ -16,7 +16,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:bounds = "time_bnds" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/abcza_pa19591997_daily_29.b_1.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/abcza_pa19591997_daily_29.b_1.cdl index 347b0689dc..5e6e974110 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/abcza_pa19591997_daily_29.b_1.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/abcza_pa19591997_daily_29.b_1.cdl @@ -16,7 +16,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:bounds = "time_bnds" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/abcza_pa19591997_daily_29.b_2.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/abcza_pa19591997_daily_29.b_2.cdl index f993ecc953..01ca9c1493 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/abcza_pa19591997_daily_29.b_2.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/abcza_pa19591997_daily_29.b_2.cdl @@ -16,7 +16,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:bounds = "time_bnds" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/abxpa_press_lat.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/abxpa_press_lat.b_0.cdl index c1ecec16df..6f6cf00d82 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/abxpa_press_lat.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/abxpa_press_lat.b_0.cdl @@ -15,7 +15,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; float pressure(pressure) ; pressure:axis = "Z" ; pressure:units = "hPa" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/integer.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/integer.b_0.cdl index b4cd781460..443b44e9bf 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/integer.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/integer.b_0.cdl @@ -14,7 +14,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; float latitude(latitude) ; latitude:axis = "Y" ; latitude:units = "degrees_north" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/model.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/model.b_0.cdl index 18224f6ea2..bf505b032a 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/model.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/model.b_0.cdl @@ -17,7 +17,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; double time(time) ; time:axis = "T" ; time:bounds = "time_bnds" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/ocean_xsect.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/ocean_xsect.b_0.cdl index 5a09c2a446..ff46155154 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/ocean_xsect.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/ocean_xsect.b_0.cdl @@ -15,7 +15,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; float depth(depth) ; depth:axis = "Z" ; depth:bounds = "depth_bnds" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/st0fc699.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/st0fc699.b_0.cdl index b7b23e5823..48f6f0c835 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/st0fc699.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/st0fc699.b_0.cdl @@ -13,7 +13,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; float latitude(latitude) ; latitude:axis = "Y" ; latitude:units = "degrees_north" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/st0fc942.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/st0fc942.b_0.cdl index 188d8f1644..4514f23858 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/st0fc942.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/st0fc942.b_0.cdl @@ -18,7 +18,6 @@ variables: rotated_latitude_longitude:grid_north_pole_latitude = 0. ; rotated_latitude_longitude:grid_north_pole_longitude = 0. ; rotated_latitude_longitude:north_pole_grid_longitude = 0. ; - rotated_latitude_longitude:crs_wkt = "GEOGCRS[\"unnamed\",BASEGEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]]],DERIVINGCONVERSION[\"unknown\",METHOD[\"PROJ ob_tran o_proj=latlon\"],PARAMETER[\"o_lon_p\",0,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"o_lat_p\",0,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],PARAMETER[\"lon_0\",180,ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; int pseudo_level(pseudo_level) ; pseudo_level:units = "1" ; pseudo_level:long_name = "pseudo_level" ; diff --git a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/st30211.b_0.cdl b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/st30211.b_0.cdl index 5e92035268..cefed0f94c 100644 --- a/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/st30211.b_0.cdl +++ b/lib/iris/tests/results/usecases/pp_to_cf_conversion/to_netcdf/st30211.b_0.cdl @@ -17,7 +17,6 @@ variables: latitude_longitude:grid_mapping_name = "latitude_longitude" ; latitude_longitude:longitude_of_prime_meridian = 0. ; latitude_longitude:earth_radius = 6371229. ; - latitude_longitude:crs_wkt = "GEOGCRS[\"unknown\",DATUM[\"unknown\",ELLIPSOID[\"unknown\",6371229,0,LENGTHUNIT[\"metre\",1,ID[\"EPSG\",9001]]]],PRIMEM[\"Greenwich\",0,ANGLEUNIT[\"degree\",0.0174532925199433],ID[\"EPSG\",8901]],CS[ellipsoidal,2],AXIS[\"longitude\",east,ORDER[1],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]],AXIS[\"latitude\",north,ORDER[2],ANGLEUNIT[\"degree\",0.0174532925199433,ID[\"EPSG\",9122]]]]" ; int pseudo_level(pseudo_level) ; pseudo_level:units = "1" ; pseudo_level:long_name = "pseudo_level" ; From fd1ffcd365dba39903876754e27359dbd8571dfa Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Tue, 8 Jul 2025 18:19:38 +0100 Subject: [PATCH 42/58] What's New --- docs/src/whatsnew/latest.rst | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index b14218134d..9a8b02c757 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -52,6 +52,18 @@ This document explains the changes made to Iris for this release needs by using the `Ncdata`_ package. See `CRS WKT in the CF Conventions`_ for more. (:issue:`3796`, :pull:`6519`) +#. `@ukmo-ccbunney` and `@trexfeathers` added support for **ordered coordinates** + when loading and saving NetCDF files. This allows for coordinates to be + explicitly associated with a coordinate system via an extended syntax in + the ``grid_mapping`` attribute of a NetCDF data variable. This extended + syntax also supports specification of multiple coordinate systems per + data variable. Setting the property ``cube.ordered_coords = True`` will + enable extended grid mapping syntax when saving a NetCDF file and also + enerated an associated well known text attribute (``crs_wks``; as + described in :issue:`3796`). + See `CRS Grid Mappings and Projections`_ for more information. + (:issue:3388:, :pull:6536:) + 🐛 Bugs Fixed ============= @@ -146,4 +158,5 @@ This document explains the changes made to Iris for this release Whatsnew resources in alphabetical order: .. _CRS WKT in the CF Conventions: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.12/cf-conventions.html#use-of-the-crs-well-known-text-format +.. _CRS Grid Mappings and Projections: https://cfconventions.org/Data/cf-conventions/cf-conventions-1.12/cf-conventions.html#grid-mappings-and-projections .. _Ncdata: https://github.com/pp-mo/ncdata From c60eb34d1d4cb29ae5af2c34324acbfa17cefebb Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Tue, 8 Jul 2025 19:31:10 +0100 Subject: [PATCH 43/58] Update Docs --- docs/src/further_topics/netcdf_io.rst | 67 +++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/docs/src/further_topics/netcdf_io.rst b/docs/src/further_topics/netcdf_io.rst index 682918d5f4..34b070a2b6 100644 --- a/docs/src/further_topics/netcdf_io.rst +++ b/docs/src/further_topics/netcdf_io.rst @@ -235,3 +235,70 @@ Worked example: >>> my_coord.ignore_axis = True >>> print(guess_coord_axis(my_coord)) None + +Multiple Coordinate Systems and Ordered Axes +-------------------------------------------- + +Traditionally, the coordinate system for coordinate of a data variable +is specified by variable a single name in the ``grid_mapping`` attribute, +e.g.: + +:: + + grid_mapping = 'latitude_longitude` + +Since version `1.8 of the CF Conventions +`_ +, there has been support for the concept of **ordered axes** in the definition +of a coordinate system which allows for more explicit specification of the +associated coordinate variables. + +This is achieved via use of an extended syntax in the ``grid_mapping`` +attribute of a data variable: + +:: + + : [ …] [: …] + +where each ``gridMappingVariable`` identifies a grid mapping variable +followed by the list of associated coordinate variables. Note that with +this syntax it is possible to specify multiple coordinate systems for a +data variable. + +The order of the axes in the extended grid mapping specification is +significant, but only when used in conjunction with a "well known text" +(WKT) representation of the coordinate system _REF_ where it should be +consistent with the ``AXES ORDER`` specified in the ``crs_wkt`` attribute. + + +Effect on loading +^^^^^^^^^^^^^^^^^ + +When Iris loads a NetCDF file that uses the extended grid mapping syntax +it will generate an :class:`iris.coord_systems.CoordSystem` for each +coordinate system listed and attempt to attach it to the associated +:class:`iris.coord.Coord` instances on the cube. Currently, Iris considers +the ``crs_wkt`` supplementary and builds coordinate systems exclusively +from the ``grid_mapping`` attribute. + +The :attr:`iris.cube.Cube.ordered_axes` property will be set to ``True`` +for cubes loaded from NetCDF data variables utilising the extended +``grid_mapping`` syntax. + +Effect on saving +^^^^^^^^^^^^^^^^ + +To maintain existing behaviour, saving an :class:`iris.cube.Cube` to +a netCDF file will default to the "simple" grid mapping syntax. If +the cube contains multiple coordinate systems, only the coordinate +system of the dimension coordinate(s) will be specified. + +To enable saving of multiple coordinate systems with ordered axes, +set the :attr:`iris.cube.Cube.ordered_axes` to ``True``. This will +generate a ``grid_mapping`` attribute using the extended syntax to +specify all coordinate systems on the cube. The axes ordering of the +associated coordinate variables will be consistent with that of the +generated ``crs_wkt`` attribute. + +Note, the ``crs_wkt`` attribute will only be generated when the +extended grid mapping is also written, i.e. when ``Cube.ordered_axes=True``. From bec761382961dcebabe869c624bc91ffb7ccff8a Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Wed, 16 Jul 2025 19:25:02 +0100 Subject: [PATCH 44/58] Renamed ordered_coords -> extended_grid_mapping and store as an attribute --- docs/src/further_topics/netcdf_io.rst | 13 +++++----- lib/iris/cube.py | 13 ++++------ lib/iris/fileformats/netcdf/loader.py | 8 ++++--- lib/iris/fileformats/netcdf/saver.py | 24 ++++++++++++------- .../integration/test_netcdf__loadsaveattrs.py | 4 ++++ .../actions/test__grid_mappings.py | 12 +++++----- .../fileformats/netcdf/saver/test_Saver.py | 23 +++++++++--------- 7 files changed, 55 insertions(+), 42 deletions(-) diff --git a/docs/src/further_topics/netcdf_io.rst b/docs/src/further_topics/netcdf_io.rst index 34b070a2b6..d4fc3f39b0 100644 --- a/docs/src/further_topics/netcdf_io.rst +++ b/docs/src/further_topics/netcdf_io.rst @@ -281,8 +281,8 @@ coordinate system listed and attempt to attach it to the associated the ``crs_wkt`` supplementary and builds coordinate systems exclusively from the ``grid_mapping`` attribute. -The :attr:`iris.cube.Cube.ordered_axes` property will be set to ``True`` -for cubes loaded from NetCDF data variables utilising the extended +The :attr:`iris.cube.Cube.extended_grid_mapping` property will be set to +``True`` for cubes loaded from NetCDF data variables utilising the extended ``grid_mapping`` syntax. Effect on saving @@ -294,11 +294,12 @@ the cube contains multiple coordinate systems, only the coordinate system of the dimension coordinate(s) will be specified. To enable saving of multiple coordinate systems with ordered axes, -set the :attr:`iris.cube.Cube.ordered_axes` to ``True``. This will -generate a ``grid_mapping`` attribute using the extended syntax to -specify all coordinate systems on the cube. The axes ordering of the +set the :attr:`iris.cube.Cube.extended_grid_mapping` to ``True``. +This will generate a ``grid_mapping`` attribute using the extended syntax +to specify all coordinate systems on the cube. The axes ordering of the associated coordinate variables will be consistent with that of the generated ``crs_wkt`` attribute. Note, the ``crs_wkt`` attribute will only be generated when the -extended grid mapping is also written, i.e. when ``Cube.ordered_axes=True``. +extended grid mapping is also written, i.e. when +``Cube.extended_grid_mapping=True``. diff --git a/lib/iris/cube.py b/lib/iris/cube.py index 1850766bbb..0eb0b87088 100644 --- a/lib/iris/cube.py +++ b/lib/iris/cube.py @@ -1291,9 +1291,6 @@ def __init__( ] = [] self._aux_factories: list[AuxCoordFactory] = [] - # Default ordered_axes property to False; requires explicit opt-in - self._ordered_axes = False - # Cell Measures self._cell_measures_and_dims: list[tuple[CellMeasure, tuple[int, ...]]] = [] @@ -2465,7 +2462,7 @@ def coord_systems(self) -> list[iris.coord_systems.CoordSystem]: return list(coord_systems.values()) @property - def ordered_axes(self) -> bool: + def extended_grid_mapping(self) -> bool: """Return True if a cube will use extended grid mapping syntax to write axes order in grid_mapping. Only relevant when saving a cube to NetCDF file format. @@ -2473,12 +2470,12 @@ def ordered_axes(self) -> bool: For more details see "Grid Mappings and Projections" in the CF Conventions document: https://cfconventions.org/cf-conventions/conformance.html """ - return self._ordered_axes + return self.attributes.get("iris_extended_grid_mapping", False) - @ordered_axes.setter - def ordered_axes(self, ordered: bool) -> None: + @extended_grid_mapping.setter + def extended_grid_mapping(self, ordered: bool) -> None: """Set to True to enable extended grid mapping syntax.""" - self._ordered_axes = ordered + self.attributes["iris_extended_grid_mapping"] = ordered def _any_meshcoord(self) -> MeshCoord | None: """Return a MeshCoord if there are any, else None.""" diff --git a/lib/iris/fileformats/netcdf/loader.py b/lib/iris/fileformats/netcdf/loader.py index 985ce902ed..216df67590 100644 --- a/lib/iris/fileformats/netcdf/loader.py +++ b/lib/iris/fileformats/netcdf/loader.py @@ -451,10 +451,12 @@ def fix_attributes_all_elements(role_name): for method in cube.cell_methods ] - # Set ordered_axis property if extended grid_mapping was used + # Set extended_grid_mapping property ONLY if extended grid_mapping was used. + # This avoids having an unnecessary `iris_extended_grid_mapping` attribute entry. if cs_mappings := engine.cube_parts.get("coordinate_system_mappings", None): - # None as a mapping key implies simple mapping syntax (single coord system) - cube.ordered_axes = None not in cs_mappings + # `None` as a mapping key implies simple mapping syntax (single coord system) + if None not in cs_mappings: + cube.extended_grid_mapping = True if DEBUG: # Show activation statistics for this data-var (i.e. cube). diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 52c1ef2347..60973fb0f4 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -103,7 +103,12 @@ _CF_GLOBAL_ATTRS = ["conventions", "featureType", "history", "title"] # UKMO specific attributes that should not be global. -_UKMO_DATA_ATTRS = ["STASH", "um_stash_source", "ukmo__process_flags"] +_UKMO_DATA_ATTRS = [ + "STASH", + "um_stash_source", + "ukmo__process_flags", + "iris_extended_grid_mapping", +] # TODO: whenever we advance to CF-1.11 we should then discuss a completion date # for the deprecation of Rotated Mercator in coord_systems.py and @@ -642,7 +647,10 @@ def write( global_attributes = { k: v for k, v in cube_attributes.items() - if (k not in local_keys and k.lower() != "conventions") + if ( + k not in local_keys + and k.lower() not in ["conventions", "iris_extended_grid_mapping"] + ) } self.update_global_attributes(global_attributes) @@ -2073,7 +2081,7 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube): """ coord_systems = [] - if cube.ordered_axes: + if cube.extended_grid_mapping: # get unique list of all coord_systems on cube coords: for coord in cube.coords(): if coord.coord_system and coord.coord_system not in coord_systems: @@ -2096,12 +2104,12 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube): # create grid mapping variable on dataset for this coordinate system: self._add_grid_mapping_to_dataset( - cs, extended_grid_mapping=cube.ordered_axes + cs, extended_grid_mapping=cube.extended_grid_mapping ) self._coord_systems.append(cs) # create the `grid_mapping` attribute for the data variable: - if cube.ordered_axes: + if cube.extended_grid_mapping: # Order the coordinates as per the order in the CRS/WKT string. # (We should only ever have a coordinate system for horizontal # spatial coords, so check for east/north directions) @@ -2150,7 +2158,7 @@ def _create_cf_grid_mapping(self, cube, cf_var_cube): # Refer to grid var if len(coord_systems): grid_mapping = None - if cube.ordered_axes: + if cube.extended_grid_mapping: if matched_all_coords: grid_mapping = " ".join( f"{cs_name}: {cs_coords}" @@ -2311,8 +2319,8 @@ def set_packing_ncattrs(cfvar): attr_names = set(cube.attributes).intersection(local_keys) for attr_name in sorted(attr_names): - # Do not output 'conventions' attribute. - if attr_name.lower() == "conventions": + # Do not output 'conventions' or extended grid mapping attribute. + if attr_name.lower() in ["conventions", "iris_extended_grid_mapping"]: continue value = cube.attributes[attr_name] diff --git a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py index b89477bfe9..a2d6e9c754 100644 --- a/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py +++ b/lib/iris/tests/integration/test_netcdf__loadsaveattrs.py @@ -69,6 +69,9 @@ def global_attr(request): iris.fileformats.netcdf.saver._CF_DATA_ATTRS + iris.fileformats.netcdf.saver._UKMO_DATA_ATTRS ) +# Don't test iris_extended_grid_mapping, as it is a special attribute that +# is not expected to always roundtrip. +_LOCAL_TEST_ATTRS = [a for a in _LOCAL_TEST_ATTRS if a != "iris_extended_grid_mapping"] # Define a fixture to parametrise over the 'local-style' test attributes. @@ -513,6 +516,7 @@ def fetch_results( "standard_error_multiplier", "STASH", "um_stash_source", + "iris_extended_grid_mapping", ] _MATRIX_ATTRNAMES = [attr for attr in _MATRIX_ATTRNAMES if attr not in _SPECIAL_ATTRS] diff --git a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py index b6210a2392..202fb0fa16 100644 --- a/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py +++ b/lib/iris/tests/unit/fileformats/nc_load_rules/actions/test__grid_mappings.py @@ -1045,7 +1045,7 @@ def test_two_coord_systems(self, osgb_cs, latlon_cs, mocker, tmp_path): ) # Loading multiple coord systems or using extended grid mapping implies ordered axes: - assert cube.ordered_axes is True + assert cube.extended_grid_mapping is True def test_two_coord_systems_missing_coord( self, osgb_cs, latlon_cs_missing_coord, mocker, tmp_path @@ -1088,7 +1088,7 @@ def test_two_coord_systems_missing_coord( cube.coord(coord_name).coord_system, iris.coord_systems.GeogCS ) - assert cube.ordered_axes is True + assert cube.extended_grid_mapping is True def test_two_coord_systems_missing_aux_crs( self, osgb_cs, latlon_cs, mocker, tmp_path @@ -1129,7 +1129,7 @@ def test_two_coord_systems_missing_aux_crs( for coord_name in ["latitude", "longitude"]: assert cube.coord(coord_name).coord_system is None - assert cube.ordered_axes is True + assert cube.extended_grid_mapping is True def test_two_coord_systems_missing_dim_crs( self, osgb_cs, latlon_cs, mocker, tmp_path @@ -1167,7 +1167,7 @@ def test_two_coord_systems_missing_dim_crs( cube.coord(coord_name).coord_system, iris.coord_systems.GeogCS ) - assert cube.ordered_axes is True + assert cube.extended_grid_mapping is True def test_two_coord_systems_invalid_grid_mapping( self, osgb_cs, latlon_cs, mocker, tmp_path @@ -1201,7 +1201,7 @@ def test_two_coord_systems_invalid_grid_mapping( for coord in cube.coords(): assert coord.coord_system is None - assert cube.ordered_axes is False + assert cube.extended_grid_mapping is False def test_one_coord_system_simple(self, osgb_cs, latlon_cs, mocker, tmp_path): """Make sure the simple coord system syntax still works.""" @@ -1234,7 +1234,7 @@ def test_one_coord_system_simple(self, osgb_cs, latlon_cs, mocker, tmp_path): assert cube.coord(coord_name).coord_system is None # Loading multiple coord systems or using extended grid mapping implies ordered axes: - assert cube.ordered_axes is False + assert cube.extended_grid_mapping is False if __name__ == "__main__": diff --git a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py index 60fcceb7d0..e43f882167 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py +++ b/lib/iris/tests/unit/fileformats/netcdf/saver/test_Saver.py @@ -792,10 +792,11 @@ def test_passthrough_units(self): @pytest.fixture -def transverse_mercator_cube_ordered_axes(): +def transverse_mercator_cube_multi_cs(): + """A transverse mercator cube with an auxiliary GeogGS coordinate system.""" data = np.arange(12).reshape(3, 4) cube = Cube(data, "air_pressure_anomaly") - cube.ordered_axes = True + cube.extended_grid_mapping = True geog_cs = GeogCS(6377563.396, 6356256.909) trans_merc = TransverseMercator( @@ -837,22 +838,22 @@ def transverse_mercator_cube_ordered_axes(): class Test_write_extended_grid_mapping: - def test_multi_cs(self, transverse_mercator_cube_ordered_axes, tmp_path, request): + def test_multi_cs(self, transverse_mercator_cube_multi_cs, tmp_path, request): """Test writing a cube with multiple coordinate systems. Should generate a grid mapping using extended syntax that references both coordinate systems and the coords. """ - cube = transverse_mercator_cube_ordered_axes + cube = transverse_mercator_cube_multi_cs nc_path = tmp_path / "tmp.nc" with Saver(nc_path, "NETCDF4") as saver: saver.write(cube) assert_CDL(request, nc_path) - def test_no_aux_cs(self, transverse_mercator_cube_ordered_axes, tmp_path, request): + def test_no_aux_cs(self, transverse_mercator_cube_multi_cs, tmp_path, request): """Test when DimCoords have coord system, but AuxCoords do not. Should write extended grid mapping for just DimCoords. """ - cube = transverse_mercator_cube_ordered_axes + cube = transverse_mercator_cube_multi_cs cube.coord("latitude").coord_system = None cube.coord("longitude").coord_system = None @@ -862,23 +863,23 @@ def test_no_aux_cs(self, transverse_mercator_cube_ordered_axes, tmp_path, reques assert_CDL(request, nc_path) def test_multi_cs_missing_coord( - self, transverse_mercator_cube_ordered_axes, tmp_path, request + self, transverse_mercator_cube_multi_cs, tmp_path, request ): """Test when we have a missing coordinate. Grid mapping will fall back to simple mapping to DimCoord CS (no coords referenced). """ - cube = transverse_mercator_cube_ordered_axes + cube = transverse_mercator_cube_multi_cs cube.remove_coord("latitude") nc_path = tmp_path / "tmp.nc" with Saver(nc_path, "NETCDF4") as saver: saver.write(cube) assert_CDL(request, nc_path) - def test_no_cs(self, transverse_mercator_cube_ordered_axes, tmp_path, request): + def test_no_cs(self, transverse_mercator_cube_multi_cs, tmp_path, request): """Test when no coordinate systems associated with cube coords. Grid mapping will not be generated at all. """ - cube = transverse_mercator_cube_ordered_axes + cube = transverse_mercator_cube_multi_cs for coord in cube.coords(): coord.coord_system = None @@ -905,7 +906,7 @@ def _cube_with_cs(self, coord_system): cube = stock.lat_lon_cube() x, y = cube.coord("longitude"), cube.coord("latitude") x.coord_system = y.coord_system = coord_system - cube.ordered_axes = self._extended_grid_mapping + cube.extended_grid_mapping = self._extended_grid_mapping return cube def _grid_mapping_variable(self, coord_system): From 6d36cb87edf8df399ae0fc87335c1ff10ddbc297 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Wed, 16 Jul 2025 20:26:34 +0100 Subject: [PATCH 45/58] Fixed some typos and references to ordered_coords in latest.rst --- docs/src/whatsnew/latest.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index 9a8b02c757..41888b0b4d 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -52,15 +52,15 @@ This document explains the changes made to Iris for this release needs by using the `Ncdata`_ package. See `CRS WKT in the CF Conventions`_ for more. (:issue:`3796`, :pull:`6519`) -#. `@ukmo-ccbunney` and `@trexfeathers` added support for **ordered coordinates** - when loading and saving NetCDF files. This allows for coordinates to be - explicitly associated with a coordinate system via an extended syntax in - the ``grid_mapping`` attribute of a NetCDF data variable. This extended - syntax also supports specification of multiple coordinate systems per - data variable. Setting the property ``cube.ordered_coords = True`` will - enable extended grid mapping syntax when saving a NetCDF file and also - enerated an associated well known text attribute (``crs_wks``; as - described in :issue:`3796`). +#. `@ukmo-ccbunney`_ and `@trexfeathers`_ added support for **multiple coordinate + systems** and **ordered coordinates** when loading and saving NetCDF files. + This allows for coordinates to be explicitly associated with a coordinate + system via an extended syntax in the ``grid_mapping`` attribute of a NetCDF + data variable. This extended syntax also supports specification of multiple + coordinate systems per data variable. Setting the property + ``cube.extended_grid_mapping = True`` will enable extended grid mapping + syntax when saving a NetCDF file and also generate an associated **well known + text** attribute (``crs_wkt``; as described in :issue:`3796`). See `CRS Grid Mappings and Projections`_ for more information. (:issue:3388:, :pull:6536:) From 3e3a758ac7cfad20448db04c8a020240abca1b5c Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Thu, 17 Jul 2025 11:51:43 +0100 Subject: [PATCH 46/58] Expanded section in docs on mutliple coord systems --- docs/src/further_topics/netcdf_io.rst | 130 ++++++++++++++++++++++---- 1 file changed, 110 insertions(+), 20 deletions(-) diff --git a/docs/src/further_topics/netcdf_io.rst b/docs/src/further_topics/netcdf_io.rst index d4fc3f39b0..37174a00ef 100644 --- a/docs/src/further_topics/netcdf_io.rst +++ b/docs/src/further_topics/netcdf_io.rst @@ -239,36 +239,122 @@ Worked example: Multiple Coordinate Systems and Ordered Axes -------------------------------------------- -Traditionally, the coordinate system for coordinate of a data variable -is specified by variable a single name in the ``grid_mapping`` attribute, -e.g.: +In a CF compliant NetCDF file, the coordinate variables associated with a +data variable can specify a specific *coordinate system* that defines how +the coordinate values relate to physical locations on the globe. For example, +a coordinate might have values with units of metres that should be referenced +against a *Transverse Mercator* projection with a specific origin. This +information is not stored on the coordinate itself, but in a separate +*grid mapping* variable. Furthermore, the grid mapping for a set of +coordinates is associated with the data variable (not the coordinates +variables) via the ``grid_mapping`` attribute. + +For example, a temperature variable defined on a *rotated pole* grid might +look like this in a NetCDF file (extract of relevant variables): + +.. code-block:: text + + float T(rlat,rlon) ; + T:long_name = "temperature" ; + T:units = "K" ; + T:coordinates = "lon lat" ; + T:grid_mapping = "rotated_pole" ; + + char rotated_pole ; + rotated_pole:grid_mapping_name = "rotated_latitude_longitude" ; + rotated_pole:grid_north_pole_latitude = 32.5 ; + rotated_pole:grid_north_pole_longitude = 170. ; + + float rlon(rlon) ; + rlon:long_name = "longitude in rotated pole grid" ; + rlon:units = "degrees" ; + rlon:standard_name = "grid_longitude"; + + float rlat(rlat) ; + rlat:long_name = "latitude in rotated pole grid" ; + rlat:units = "degrees" ; + rlat:standard_name = "grid_latitude"; -:: - grid_mapping = 'latitude_longitude` +Note how the ``rotated pole`` grid mapping (coordinate system) is referenced +from the data variable ``T:grid_mapping = "rotated_pole"`` and is implicitly +associated with the dimension coordinate variables ``rlat`` and ``rlon``. + Since version `1.8 of the CF Conventions `_ -, there has been support for the concept of **ordered axes** in the definition -of a coordinate system which allows for more explicit specification of the -associated coordinate variables. +, there has been support for a more explicit version of the ``grid_mapping`` +attribute. This allows for **multiple coordinate systems** to be defined for +a data variable and individual coordinates to be explicitly associated with +a coordinate system. This is achieved by use of an **extended syntax** in the +``grid_mapping`` variable of a data variable: -This is achieved via use of an extended syntax in the ``grid_mapping`` -attribute of a data variable: -:: +.. code-block:: text - : [ …] [: …] + : [] [: ...] -where each ``gridMappingVariable`` identifies a grid mapping variable -followed by the list of associated coordinate variables. Note that with +where each ``grid_mapping_var`` identifies a grid mapping variable followed by +the list of associated coordinate variables (``coord_var``). Note that with this syntax it is possible to specify multiple coordinate systems for a data variable. -The order of the axes in the extended grid mapping specification is -significant, but only when used in conjunction with a "well known text" -(WKT) representation of the coordinate system _REF_ where it should be -consistent with the ``AXES ORDER`` specified in the ``crs_wkt`` attribute. +For example, consider the following *air pressure* variable that is +defined on an *OSGB Transverse Mercator grid*: + +.. code-block:: text + + float pres(y, x) ; + pres:standard_name = "air_pressure" ; + pres:units = "Pa" ; + pres:coordinates = "lat lon" ; + pres:grid_mapping = "crsOSGB: x y crsWGS84: lat lon" ; + + double x(x) ; + x:standard_name = "projection_x_coordinate" ; + x:units = "m" ; + + double y(y) ; + y:standard_name = "projection_y_coordinate" ; + y:units = "m" ; + + double lat(y, x) ; + lat:standard_name = "latitude" ; + lat:units = "degrees_north" ; + + double lon(y, x) ; + lon:standard_name = "longitude" ; + lon:units = "degrees_east" ; + + int crsOSGB ; + crsOSGB:grid_mapping_name = "transverse_mercator" ; + crsOSGB:semi_major_axis = 6377563.396 ; + crsOSGB:inverse_flattening = 299.3249646 ; + + + int crsWGS84 ; + crsWGS84:grid_mapping_name = "latitude_longitude" ; + crsWGS84:longitude_of_prime_meridian = 0. ; + + + +The dimension coordinates ``x`` and ``y`` are explicitly defined on +an *transverse mercator`` grid via the ``crsOSGB`` variable. + +However, with the extended grid syntax, it is also possible to define +a second coordinate system on a standard **latitude_longitude** grid +and associate it with the auxiliary ``lat`` and ``lon`` coordinates: + +:: + + pres:grid_mapping = "crsOSGB: x y crsWGS84: lat lon" ; + + +Note, the *order* of the axes in the extended grid mapping specification is +significant, but only when used in conjunction with a +`CRS Well Known Text (WKT)`_ representation of the coordinate system where it +should be consistent with the ``AXES ORDER`` specified in the ``crs_wkt`` +attribute. Effect on loading @@ -289,8 +375,9 @@ Effect on saving ^^^^^^^^^^^^^^^^ To maintain existing behaviour, saving an :class:`iris.cube.Cube` to -a netCDF file will default to the "simple" grid mapping syntax. If -the cube contains multiple coordinate systems, only the coordinate +a netCDF file will default to the "simple" grid mapping syntax, unless +the cube was loaded from a file using the extended grid mapping syntax. +If the cube contains multiple coordinate systems, only the coordinate system of the dimension coordinate(s) will be specified. To enable saving of multiple coordinate systems with ordered axes, @@ -303,3 +390,6 @@ generated ``crs_wkt`` attribute. Note, the ``crs_wkt`` attribute will only be generated when the extended grid mapping is also written, i.e. when ``Cube.extended_grid_mapping=True``. + + +.. _CRS Well Known Text (WKT): https://cfconventions.org/Data/cf-conventions/cf-conventions-1.12/cf-conventions.html#use-of-the-crs-well-known-text-format \ No newline at end of file From e4d07fb8fa2546ffd0e89ead1240115889da9975 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Thu, 17 Jul 2025 12:09:11 +0100 Subject: [PATCH 47/58] Fixed emphasis split over two lines (sphinx doesn't like this) --- docs/src/whatsnew/latest.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index 41888b0b4d..e5129472c0 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -52,8 +52,9 @@ This document explains the changes made to Iris for this release needs by using the `Ncdata`_ package. See `CRS WKT in the CF Conventions`_ for more. (:issue:`3796`, :pull:`6519`) -#. `@ukmo-ccbunney`_ and `@trexfeathers`_ added support for **multiple coordinate - systems** and **ordered coordinates** when loading and saving NetCDF files. +#. `@ukmo-ccbunney`_ and `@trexfeathers`_ added support for + **multiple coordinate systems** and **ordered coordinates** when loading + and saving NetCDF files. This allows for coordinates to be explicitly associated with a coordinate system via an extended syntax in the ``grid_mapping`` attribute of a NetCDF data variable. This extended syntax also supports specification of multiple From 7a8ab8f456e408b2e5f2a0abd621edcf36b256dc Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Thu, 17 Jul 2025 12:17:30 +0100 Subject: [PATCH 48/58] Small typo and emphasis fix --- docs/src/further_topics/netcdf_io.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/further_topics/netcdf_io.rst b/docs/src/further_topics/netcdf_io.rst index 37174a00ef..081526f497 100644 --- a/docs/src/further_topics/netcdf_io.rst +++ b/docs/src/further_topics/netcdf_io.rst @@ -339,7 +339,7 @@ defined on an *OSGB Transverse Mercator grid*: The dimension coordinates ``x`` and ``y`` are explicitly defined on -an *transverse mercator`` grid via the ``crsOSGB`` variable. +an a *transverse mercator* grid via the ``crsOSGB`` variable. However, with the extended grid syntax, it is also possible to define a second coordinate system on a standard **latitude_longitude** grid From 9f3fb48a76b44512c03b267219bc33435c30c369 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Thu, 17 Jul 2025 14:19:52 +0100 Subject: [PATCH 49/58] Typo iris.coord.Coord => iris.coords.Coord --- docs/src/further_topics/netcdf_io.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/further_topics/netcdf_io.rst b/docs/src/further_topics/netcdf_io.rst index 081526f497..21ca45dcac 100644 --- a/docs/src/further_topics/netcdf_io.rst +++ b/docs/src/further_topics/netcdf_io.rst @@ -363,7 +363,7 @@ Effect on loading When Iris loads a NetCDF file that uses the extended grid mapping syntax it will generate an :class:`iris.coord_systems.CoordSystem` for each coordinate system listed and attempt to attach it to the associated -:class:`iris.coord.Coord` instances on the cube. Currently, Iris considers +:class:`iris.coords.Coord` instances on the cube. Currently, Iris considers the ``crs_wkt`` supplementary and builds coordinate systems exclusively from the ``grid_mapping`` attribute. From dda23fcc88289e451d71f08093939de1ee7af037 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Thu, 17 Jul 2025 15:22:12 +0100 Subject: [PATCH 50/58] Extended grid_mapping integration tests to test multi coord systems --- .../integration/netcdf/test_coord_systems.py | 122 +++++++++++++++++- 1 file changed, 121 insertions(+), 1 deletion(-) diff --git a/lib/iris/tests/integration/netcdf/test_coord_systems.py b/lib/iris/tests/integration/netcdf/test_coord_systems.py index 6c101d9024..984c5b318e 100644 --- a/lib/iris/tests/integration/netcdf/test_coord_systems.py +++ b/lib/iris/tests/integration/netcdf/test_coord_systems.py @@ -13,6 +13,7 @@ import tempfile import warnings +import numpy as np import pytest import iris @@ -93,7 +94,7 @@ def test_load_laea_grid(self): float data(y, x) ; data :standard_name = "toa_brightness_temperature" ; data :units = "K" ; - data :grid_mapping = "mercator" ; + data :grid_mapping = "mercator: x y" ; int mercator ; mercator:grid_mapping_name = "mercator" ; mercator:longitude_of_prime_meridian = 0. ; @@ -131,6 +132,55 @@ def test_load_laea_grid(self): } """ + multi_cs_osgb_wkt = """ +netcdf osgb { +dimensions: + y = 5 ; + x = 4 ; +variables: + double x(x) ; + x:standard_name = "projection_x_coordinate" ; + x:long_name = "Easting" ; + x:units = "m" ; + double y(y) ; + y:standard_name = "projection_y_coordinate" ; + y:long_name = "Northing" ; + y:units = "m" ; + double lat(y, x) ; + lat:standard_name = "latitude" ; + lat:units = "degrees_north" ; + double lon(y, x) ; + lon:standard_name = "longitude" ; + lon:units = "degrees_east" ; + float temp(y, x) ; + temp:standard_name = "air_temperature" ; + temp:units = "K" ; + temp:coordinates = "lat lon" ; + temp:grid_mapping = "crsOSGB: x y crsWGS84: lat lon" ; + int crsOSGB ; + crsOSGB:grid_mapping_name = "transverse_mercator" ; + crsOSGB:semi_major_axis = 6377563.396 ; + crsOSGB:inverse_flattening = 299.3249646 ; + crsOSGB:longitude_of_prime_meridian = 0. ; + crsOSGB:latitude_of_projection_origin = 49. ; + crsOSGB:longitude_of_central_meridian = -2. ; + crsOSGB:scale_factor_at_central_meridian = 0.9996012717 ; + crsOSGB:false_easting = 400000. ; + crsOSGB:false_northing = -100000. ; + crsOSGB:unit = "metre" ; + crsOSGB:crs_wkt = "PROJCRS[\\"unknown\\",BASEGEOGCRS[\\"unknown\\",DATUM[\\"Unknown based on Airy 1830 ellipsoid\\",ELLIPSOID[\\"Airy 1830\\",6377563.396,299.324964600004,LENGTHUNIT[\\"metre\\",1,ID[\\"EPSG\\",9001]]]],PRIMEM[\\"Greenwich\\",0,ANGLEUNIT[\\"degree\\",0.0174532925199433],ID[\\"EPSG\\",8901]]],CONVERSION[\\"unknown\\",METHOD[\\"Transverse Mercator\\",ID[\\"EPSG\\",9807]],PARAMETER[\\"Latitude of natural origin\\",49,ANGLEUNIT[\\"degree\\",0.0174532925199433],ID[\\"EPSG\\",8801]],PARAMETER[\\"Longitude of natural origin\\",-2,ANGLEUNIT[\\"degree\\",0.0174532925199433],ID[\\"EPSG\\",8802]],PARAMETER[\\"Scale factor at natural origin\\",0.9996012717,SCALEUNIT[\\"unity\\",1],ID[\\"EPSG\\",8805]],PARAMETER[\\"False easting\\",400000,LENGTHUNIT[\\"metre\\",1],ID[\\"EPSG\\",8806]],PARAMETER[\\"False northing\\",-100000,LENGTHUNIT[\\"metre\\",1],ID[\\"EPSG\\",8807]]],CS[Cartesian,2],AXIS[\\"(E)\\",east,ORDER[1],LENGTHUNIT[\\"metre\\",1,ID[\\"EPSG\\",9001]]],AXIS[\\"(N)\\",north,ORDER[2],LENGTHUNIT[\\"metre\\",1,ID[\\"EPSG\\",9001]]]]" ; + int crsWGS84 ; + crsWGS84:grid_mapping_name = "latitude_longitude" ; + crsWGS84:longitude_of_prime_meridian = 0. ; + crsWGS84:semi_major_axis = 6378137. ; + crsWGS84:inverse_flattening = 298.257223563 ; + crsWGS84: crs_wkt = "GEOGCRS[\\"unknown\\",DATUM[\\"Unknown based on WGS 84 ellipsoid\\",ELLIPSOID[\\"WGS 84\\",6378137,298.257223562997,LENGTHUNIT[\\"metre\\",1,ID[\\"EPSG\\",9001]]]],PRIMEM[\\"Greenwich\\",0,ANGLEUNIT[\\"degree\\",0.0174532925199433],ID[\\"EPSG\\",8901]],CS[ellipsoidal,2],AXIS[\\"longitude\\",east,ORDER[1],ANGLEUNIT[\\"degree\\",0.0174532925199433,ID[\\"EPSG\\",9122]]],AXIS[\\"latitude\\",north,ORDER[2],ANGLEUNIT[\\"degree\\",0.0174532925199433,ID[\\"EPSG\\",9122]]]]" ; +data: + x = 1,2,3,4,5 ; + y = 1,2,3,4 ; +} +""" + def test_load_datum_wkt(self): expected = "OSGB 1936" nc_path = tlc.cdl_to_nc(self.datum_wkt_cdl) @@ -175,6 +225,22 @@ def test_no_load_datum_cf_var(self): actual = str(test_crs.as_cartopy_crs().datum) assert actual == "unknown" + def test_load_multi_cs_wkt(self): + nc_path = tlc.cdl_to_nc(self.multi_cs_osgb_wkt) + with iris.FUTURE.context(datum_support=True): + cube = iris.load_cube(nc_path) + + assert len(cube.coord_systems()) == 2 + for name in ["projection_y_coordinate", "projection_y_coordinate"]: + assert ( + cube.coord(name).coord_system.grid_mapping_name == "transverse_mercator" + ) + for name in ["latitude", "longitude"]: + assert ( + cube.coord(name).coord_system.grid_mapping_name == "latitude_longitude" + ) + assert cube.extended_grid_mapping is True + def test_save_datum(self): expected = "OSGB 1936" saved_crs = iris.coord_systems.Mercator( @@ -214,6 +280,60 @@ def test_save_datum(self): actual = str(test_crs.as_cartopy_crs().datum) assert actual == expected + def test_save_multi_cs_wkt(self): + crsOSGB = iris.coord_systems.OSGB() + crsLatLon = iris.coord_systems.GeogCS(6e6) + + dimx_coord = iris.coords.DimCoord( + np.arange(4), "projection_x_coordinate", coord_system=crsOSGB + ) + dimy_coord = iris.coords.DimCoord( + np.arange(5), "projection_y_coordinate", coord_system=crsOSGB + ) + + auxlon_coord = iris.coords.AuxCoord( + np.arange(20).reshape((5, 4)), + standard_name="longitude", + coord_system=crsLatLon, + ) + auxlat_coord = iris.coords.AuxCoord( + np.arange(20).reshape((5, 4)), + standard_name="latitude", + coord_system=crsLatLon, + ) + + test_cube = Cube( + np.ones(20).reshape((5, 4)), + standard_name="air_pressure", + units="Pa", + dim_coords_and_dims=( + (dimy_coord, 0), + (dimx_coord, 1), + ), + aux_coords_and_dims=( + (auxlat_coord, (0, 1)), + (auxlon_coord, (0, 1)), + ), + ) + + test_cube.extended_grid_mapping = True + + with self.temp_filename(suffix=".nc") as filename: + iris.save(test_cube, filename) + with iris.FUTURE.context(datum_support=True): + cube = iris.load_cube(filename) + + assert len(cube.coord_systems()) == 2 + for name in ["projection_y_coordinate", "projection_y_coordinate"]: + assert ( + cube.coord(name).coord_system.grid_mapping_name == "transverse_mercator" + ) + for name in ["latitude", "longitude"]: + assert ( + cube.coord(name).coord_system.grid_mapping_name == "latitude_longitude" + ) + assert cube.extended_grid_mapping is True + class TestLoadMinimalGeostationary(tests.IrisTest): """Check we can load data with a geostationary grid-mapping, even when the From 5cd4da4858b588472f6f16cc94539a8435448ed0 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Thu, 17 Jul 2025 15:29:29 +0100 Subject: [PATCH 51/58] Revert unnecessary change to attr saving. --- lib/iris/fileformats/netcdf/saver.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/iris/fileformats/netcdf/saver.py b/lib/iris/fileformats/netcdf/saver.py index 60973fb0f4..8657780af2 100644 --- a/lib/iris/fileformats/netcdf/saver.py +++ b/lib/iris/fileformats/netcdf/saver.py @@ -647,10 +647,7 @@ def write( global_attributes = { k: v for k, v in cube_attributes.items() - if ( - k not in local_keys - and k.lower() not in ["conventions", "iris_extended_grid_mapping"] - ) + if (k not in local_keys and k.lower() != "conventions") } self.update_global_attributes(global_attributes) From 44d171d8884c1f337a09384eed3e27759538fad2 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Thu, 17 Jul 2025 15:33:16 +0100 Subject: [PATCH 52/58] Removed confusing coordinates entry in example CDL. --- docs/src/further_topics/netcdf_io.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/src/further_topics/netcdf_io.rst b/docs/src/further_topics/netcdf_io.rst index 21ca45dcac..47d85ffeab 100644 --- a/docs/src/further_topics/netcdf_io.rst +++ b/docs/src/further_topics/netcdf_io.rst @@ -257,7 +257,6 @@ look like this in a NetCDF file (extract of relevant variables): float T(rlat,rlon) ; T:long_name = "temperature" ; T:units = "K" ; - T:coordinates = "lon lat" ; T:grid_mapping = "rotated_pole" ; char rotated_pole ; From 3e8dd2421959f2f1608ef689a96f64a5ab682205 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Thu, 17 Jul 2025 19:43:14 +0100 Subject: [PATCH 53/58] Fixed rendering of latest.rst entry. --- docs/src/whatsnew/latest.rst | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/src/whatsnew/latest.rst b/docs/src/whatsnew/latest.rst index e5129472c0..de4834fb27 100644 --- a/docs/src/whatsnew/latest.rst +++ b/docs/src/whatsnew/latest.rst @@ -53,17 +53,17 @@ This document explains the changes made to Iris for this release See `CRS WKT in the CF Conventions`_ for more. (:issue:`3796`, :pull:`6519`) #. `@ukmo-ccbunney`_ and `@trexfeathers`_ added support for - **multiple coordinate systems** and **ordered coordinates** when loading - and saving NetCDF files. - This allows for coordinates to be explicitly associated with a coordinate - system via an extended syntax in the ``grid_mapping`` attribute of a NetCDF - data variable. This extended syntax also supports specification of multiple - coordinate systems per data variable. Setting the property - ``cube.extended_grid_mapping = True`` will enable extended grid mapping - syntax when saving a NetCDF file and also generate an associated **well known - text** attribute (``crs_wkt``; as described in :issue:`3796`). - See `CRS Grid Mappings and Projections`_ for more information. - (:issue:3388:, :pull:6536:) + **multiple coordinate systems** and **ordered coordinates** when loading + and saving NetCDF files. + This allows for coordinates to be explicitly associated with a coordinate + system via an extended syntax in the ``grid_mapping`` attribute of a NetCDF + data variable. This extended syntax also supports specification of multiple + coordinate systems per data variable. Setting the property + ``cube.extended_grid_mapping = True`` will enable extended grid mapping + syntax when saving a NetCDF file and also generate an associated **well known + text** attribute (``crs_wkt``; as described in :issue:`3796`). + See `CRS Grid Mappings and Projections`_ for more information. + (:issue:`3388`:, :pull:`6536`:) 🐛 Bugs Fixed From 8132357f16924acdc621b780d4b93494dc9acb03 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Thu, 17 Jul 2025 20:42:27 +0100 Subject: [PATCH 54/58] Converted test_coord_systes.py::TestCoordSystem to pytest --- .../integration/netcdf/test_coord_systems.py | 83 ++++++++++--------- 1 file changed, 46 insertions(+), 37 deletions(-) diff --git a/lib/iris/tests/integration/netcdf/test_coord_systems.py b/lib/iris/tests/integration/netcdf/test_coord_systems.py index 984c5b318e..e558e252a2 100644 --- a/lib/iris/tests/integration/netcdf/test_coord_systems.py +++ b/lib/iris/tests/integration/netcdf/test_coord_systems.py @@ -20,27 +20,14 @@ from iris.coords import DimCoord from iris.cube import Cube from iris.tests import stock as stock +from iris.tests._shared_utils import assert_CML from iris.tests.stock.netcdf import ncgen_from_cdl from iris.tests.unit.fileformats.netcdf.loader import test_load_cubes as tlc -@tests.skip_data -class TestCoordSystem(tests.IrisTest): - def setUp(self): - tlc.setUpModule() - - def tearDown(self): - tlc.tearDownModule() - - def test_load_laea_grid(self): - cube = iris.load_cube( - tests.get_data_path( - ("NetCDF", "lambert_azimuthal_equal_area", "euro_air_temp.nc") - ) - ) - self.assertCML(cube, ("netcdf", "netcdf_laea.cml")) - - datum_cf_var_cdl = """ +@pytest.fixture +def datum_cf_var_cdl(): + return """ netcdf output { dimensions: y = 4 ; @@ -85,7 +72,10 @@ def test_load_laea_grid(self): } """ - datum_wkt_cdl = """ + +@pytest.fixture +def datum_wkt_cdl(): + return """ netcdf output5 { dimensions: y = 4 ; @@ -132,7 +122,10 @@ def test_load_laea_grid(self): } """ - multi_cs_osgb_wkt = """ + +@pytest.fixture +def multi_cs_osgb_wkt(): + return """ netcdf osgb { dimensions: y = 5 ; @@ -179,27 +172,44 @@ def test_load_laea_grid(self): x = 1,2,3,4,5 ; y = 1,2,3,4 ; } -""" + """ + - def test_load_datum_wkt(self): +@tests.skip_data +class TestCoordSystem: + @pytest.fixture(autouse=True) + def _setup(self): + tlc.setUpModule() + yield + tlc.tearDownModule() + + def test_load_laea_grid(self, request): + cube = iris.load_cube( + tests.get_data_path( + ("NetCDF", "lambert_azimuthal_equal_area", "euro_air_temp.nc") + ) + ) + assert_CML(request, cube, ("netcdf", "netcdf_laea.cml")) + + def test_load_datum_wkt(self, datum_wkt_cdl): expected = "OSGB 1936" - nc_path = tlc.cdl_to_nc(self.datum_wkt_cdl) + nc_path = tlc.cdl_to_nc(datum_wkt_cdl) with iris.FUTURE.context(datum_support=True): cube = iris.load_cube(nc_path) test_crs = cube.coord("projection_y_coordinate").coord_system actual = str(test_crs.as_cartopy_crs().datum) assert actual == expected - def test_no_load_datum_wkt(self): - nc_path = tlc.cdl_to_nc(self.datum_wkt_cdl) + def test_no_load_datum_wkt(self, datum_wkt_cdl): + nc_path = tlc.cdl_to_nc(datum_wkt_cdl) with pytest.warns(FutureWarning, match="iris.FUTURE.datum_support"): cube = iris.load_cube(nc_path) test_crs = cube.coord("projection_y_coordinate").coord_system actual = str(test_crs.as_cartopy_crs().datum) assert actual == "unknown" - def test_no_datum_no_warn(self): - new_cdl = self.datum_wkt_cdl.splitlines() + def test_no_datum_no_warn(self, datum_wkt_cdl): + new_cdl = datum_wkt_cdl.splitlines() new_cdl = [line for line in new_cdl if "DATUM" not in line] new_cdl = "\n".join(new_cdl) nc_path = tlc.cdl_to_nc(new_cdl) @@ -208,25 +218,25 @@ def test_no_datum_no_warn(self): warnings.simplefilter("error", FutureWarning) _ = iris.load_cube(nc_path) - def test_load_datum_cf_var(self): + def test_load_datum_cf_var(self, datum_cf_var_cdl): expected = "OSGB 1936" - nc_path = tlc.cdl_to_nc(self.datum_cf_var_cdl) + nc_path = tlc.cdl_to_nc(datum_cf_var_cdl) with iris.FUTURE.context(datum_support=True): cube = iris.load_cube(nc_path) test_crs = cube.coord("projection_y_coordinate").coord_system actual = str(test_crs.as_cartopy_crs().datum) assert actual == expected - def test_no_load_datum_cf_var(self): - nc_path = tlc.cdl_to_nc(self.datum_cf_var_cdl) + def test_no_load_datum_cf_var(self, datum_cf_var_cdl): + nc_path = tlc.cdl_to_nc(datum_cf_var_cdl) with pytest.warns(FutureWarning, match="iris.FUTURE.datum_support"): cube = iris.load_cube(nc_path) test_crs = cube.coord("projection_y_coordinate").coord_system actual = str(test_crs.as_cartopy_crs().datum) assert actual == "unknown" - def test_load_multi_cs_wkt(self): - nc_path = tlc.cdl_to_nc(self.multi_cs_osgb_wkt) + def test_load_multi_cs_wkt(self, multi_cs_osgb_wkt): + nc_path = tlc.cdl_to_nc(multi_cs_osgb_wkt) with iris.FUTURE.context(datum_support=True): cube = iris.load_cube(nc_path) @@ -241,7 +251,7 @@ def test_load_multi_cs_wkt(self): ) assert cube.extended_grid_mapping is True - def test_save_datum(self): + def test_save_datum(self, tmp_path): expected = "OSGB 1936" saved_crs = iris.coord_systems.Mercator( ellipsoid=iris.coord_systems.GeogCS.from_datum("OSGB36") @@ -270,8 +280,7 @@ def test_save_datum(self): (test_lon_coord, 2), ), ) - - with self.temp_filename(suffix=".nc") as filename: + with tmp_path / "output.nc" as filename: iris.save(test_cube, filename) with iris.FUTURE.context(datum_support=True): cube = iris.load_cube(filename) @@ -280,7 +289,7 @@ def test_save_datum(self): actual = str(test_crs.as_cartopy_crs().datum) assert actual == expected - def test_save_multi_cs_wkt(self): + def test_save_multi_cs_wkt(self, tmp_path): crsOSGB = iris.coord_systems.OSGB() crsLatLon = iris.coord_systems.GeogCS(6e6) @@ -318,7 +327,7 @@ def test_save_multi_cs_wkt(self): test_cube.extended_grid_mapping = True - with self.temp_filename(suffix=".nc") as filename: + with tmp_path / "output.nc" as filename: iris.save(test_cube, filename) with iris.FUTURE.context(datum_support=True): cube = iris.load_cube(filename) From af5d10485c04f4ac1c86beb833e50ae6a531adc4 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Thu, 17 Jul 2025 21:05:26 +0100 Subject: [PATCH 55/58] Updated remainder of test_coord_systems.py to pytest --- .../integration/netcdf/test_coord_systems.py | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/lib/iris/tests/integration/netcdf/test_coord_systems.py b/lib/iris/tests/integration/netcdf/test_coord_systems.py index e558e252a2..18fbcd5ae6 100644 --- a/lib/iris/tests/integration/netcdf/test_coord_systems.py +++ b/lib/iris/tests/integration/netcdf/test_coord_systems.py @@ -10,7 +10,6 @@ from os.path import join as path_join import shutil -import tempfile import warnings import numpy as np @@ -344,13 +343,9 @@ def test_save_multi_cs_wkt(self, tmp_path): assert cube.extended_grid_mapping is True -class TestLoadMinimalGeostationary(tests.IrisTest): - """Check we can load data with a geostationary grid-mapping, even when the - 'false-easting' and 'false_northing' properties are missing. - - """ - - _geostationary_problem_cdl = """ +@pytest.fixture +def geostationary_problem_cdl(): + return """ netcdf geostationary_problem_case { dimensions: y = 2 ; @@ -387,23 +382,30 @@ class TestLoadMinimalGeostationary(tests.IrisTest): x = 0, 1, 2 ; } -""" + """ + + +class TestLoadMinimalGeostationary: + """Check we can load data with a geostationary grid-mapping, even when the + 'false-easting' and 'false_northing' properties are missing. + + """ @classmethod - def setUpClass(cls): + @pytest.fixture(autouse=True) + def _setup(cls, tmp_path, geostationary_problem_cdl): # Create a temp directory for transient test files. - cls.temp_dir = tempfile.mkdtemp() + cls.temp_dir = tmp_path cls.path_test_cdl = path_join(cls.temp_dir, "geos_problem.cdl") cls.path_test_nc = path_join(cls.temp_dir, "geos_problem.nc") # Create reference CDL and netcdf files from the CDL text. ncgen_from_cdl( - cdl_str=cls._geostationary_problem_cdl, + cdl_str=geostationary_problem_cdl, cdl_path=cls.path_test_cdl, nc_path=cls.path_test_nc, ) + yield - @classmethod - def tearDownClass(cls): # Destroy the temp directory. shutil.rmtree(cls.temp_dir) @@ -415,7 +417,3 @@ def test_geostationary_no_false_offsets(self): assert isinstance(cs, iris.coord_systems.Geostationary) assert cs.false_easting == 0.0 assert cs.false_northing == 0.0 - - -if __name__ == "__main__": - tests.main() From 12e575b3afa2c9669edde019c2953e9fc062c584 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Thu, 17 Jul 2025 22:28:48 +0100 Subject: [PATCH 56/58] Removed context manager for tmp_path filename - deprecated in python3.13 --- .../integration/netcdf/test_coord_systems.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/iris/tests/integration/netcdf/test_coord_systems.py b/lib/iris/tests/integration/netcdf/test_coord_systems.py index 18fbcd5ae6..0f9f8f369e 100644 --- a/lib/iris/tests/integration/netcdf/test_coord_systems.py +++ b/lib/iris/tests/integration/netcdf/test_coord_systems.py @@ -279,10 +279,10 @@ def test_save_datum(self, tmp_path): (test_lon_coord, 2), ), ) - with tmp_path / "output.nc" as filename: - iris.save(test_cube, filename) - with iris.FUTURE.context(datum_support=True): - cube = iris.load_cube(filename) + filename = tmp_path / "output.nc" + iris.save(test_cube, filename) + with iris.FUTURE.context(datum_support=True): + cube = iris.load_cube(filename) test_crs = cube.coord("projection_y_coordinate").coord_system actual = str(test_crs.as_cartopy_crs().datum) @@ -326,10 +326,10 @@ def test_save_multi_cs_wkt(self, tmp_path): test_cube.extended_grid_mapping = True - with tmp_path / "output.nc" as filename: - iris.save(test_cube, filename) - with iris.FUTURE.context(datum_support=True): - cube = iris.load_cube(filename) + filename = tmp_path / "output.nc" + iris.save(test_cube, filename) + with iris.FUTURE.context(datum_support=True): + cube = iris.load_cube(filename) assert len(cube.coord_systems()) == 2 for name in ["projection_y_coordinate", "projection_y_coordinate"]: From 4379b419e298d5ccb1cf3e28e9b1b33b5444e91c Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Thu, 17 Jul 2025 22:29:52 +0100 Subject: [PATCH 57/58] Fixed x and y variable data in multi_cs_osgb_wkt CDL. --- lib/iris/tests/integration/netcdf/test_coord_systems.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iris/tests/integration/netcdf/test_coord_systems.py b/lib/iris/tests/integration/netcdf/test_coord_systems.py index 0f9f8f369e..95a0ee1fb8 100644 --- a/lib/iris/tests/integration/netcdf/test_coord_systems.py +++ b/lib/iris/tests/integration/netcdf/test_coord_systems.py @@ -168,8 +168,8 @@ def multi_cs_osgb_wkt(): crsWGS84:inverse_flattening = 298.257223563 ; crsWGS84: crs_wkt = "GEOGCRS[\\"unknown\\",DATUM[\\"Unknown based on WGS 84 ellipsoid\\",ELLIPSOID[\\"WGS 84\\",6378137,298.257223562997,LENGTHUNIT[\\"metre\\",1,ID[\\"EPSG\\",9001]]]],PRIMEM[\\"Greenwich\\",0,ANGLEUNIT[\\"degree\\",0.0174532925199433],ID[\\"EPSG\\",8901]],CS[ellipsoidal,2],AXIS[\\"longitude\\",east,ORDER[1],ANGLEUNIT[\\"degree\\",0.0174532925199433,ID[\\"EPSG\\",9122]]],AXIS[\\"latitude\\",north,ORDER[2],ANGLEUNIT[\\"degree\\",0.0174532925199433,ID[\\"EPSG\\",9122]]]]" ; data: - x = 1,2,3,4,5 ; - y = 1,2,3,4 ; + x = 1,2,3,4 ; + y = 1,2,3,4,5 ; } """ From 8478bf5ec59b0dcd5872e754d3930476c99ed424 Mon Sep 17 00:00:00 2001 From: ukmo-ccbunney Date: Fri, 18 Jul 2025 09:06:36 +0100 Subject: [PATCH 58/58] Modernised TestLoadMinimalGeostationary tests --- .../integration/netcdf/test_coord_systems.py | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/lib/iris/tests/integration/netcdf/test_coord_systems.py b/lib/iris/tests/integration/netcdf/test_coord_systems.py index 95a0ee1fb8..9f4f272dd7 100644 --- a/lib/iris/tests/integration/netcdf/test_coord_systems.py +++ b/lib/iris/tests/integration/netcdf/test_coord_systems.py @@ -8,8 +8,6 @@ # importing anything else. import iris.tests as tests # isort:skip -from os.path import join as path_join -import shutil import warnings import numpy as np @@ -343,7 +341,7 @@ def test_save_multi_cs_wkt(self, tmp_path): assert cube.extended_grid_mapping is True -@pytest.fixture +@pytest.fixture(scope="module") def geostationary_problem_cdl(): return """ netcdf geostationary_problem_case { @@ -391,27 +389,23 @@ class TestLoadMinimalGeostationary: """ - @classmethod - @pytest.fixture(autouse=True) - def _setup(cls, tmp_path, geostationary_problem_cdl): - # Create a temp directory for transient test files. - cls.temp_dir = tmp_path - cls.path_test_cdl = path_join(cls.temp_dir, "geos_problem.cdl") - cls.path_test_nc = path_join(cls.temp_dir, "geos_problem.nc") - # Create reference CDL and netcdf files from the CDL text. + @pytest.fixture(scope="class") + def geostationary_problem_ncfile(self, tmp_path_factory, geostationary_problem_cdl): + tmp_path = tmp_path_factory.mktemp("geos") + cdl_path = tmp_path / "geos_problem.cdl" + nc_path = tmp_path / "geos_problem.nc" ncgen_from_cdl( cdl_str=geostationary_problem_cdl, - cdl_path=cls.path_test_cdl, - nc_path=cls.path_test_nc, + cdl_path=cdl_path, + nc_path=nc_path, ) - yield - - # Destroy the temp directory. - shutil.rmtree(cls.temp_dir) + return nc_path - def test_geostationary_no_false_offsets(self): + def test_geostationary_no_false_offsets( + self, tmp_path, geostationary_problem_ncfile + ): # Check we can load the test data and coordinate system properties are correct. - cube = iris.load_cube(self.path_test_nc) + cube = iris.load_cube(geostationary_problem_ncfile) # Check the coordinate system properties has the correct default properties. cs = cube.coord_system() assert isinstance(cs, iris.coord_systems.Geostationary)