diff --git a/lib/iris/_representation/cube_summary.py b/lib/iris/_representation/cube_summary.py index c7d0e15e59..68f86832f5 100644 --- a/lib/iris/_representation/cube_summary.py +++ b/lib/iris/_representation/cube_summary.py @@ -259,8 +259,17 @@ def __init__(self, cube, shorten=False, name_padding=35): vector_dim_coords = [ coord for coord in dim_coords if id(coord) not in scalar_coord_ids ] + if cube.mesh is None: + mesh_coords = [] + else: + mesh_coords = [ + coord for coord in aux_coords if hasattr(coord, "mesh") + ] + vector_aux_coords = [ - coord for coord in aux_coords if id(coord) not in scalar_coord_ids + coord + for coord in aux_coords + if (id(coord) not in scalar_coord_ids and coord not in mesh_coords) ] vector_derived_coords = [ coord @@ -300,6 +309,7 @@ def add_vector_section(title, contents, iscoord=True): ) add_vector_section("Dimension coordinates:", vector_dim_coords) + add_vector_section("Mesh coordinates:", mesh_coords) add_vector_section("Auxiliary coordinates:", vector_aux_coords) add_vector_section("Derived coordinates:", vector_derived_coords) add_vector_section("Cell measures:", vector_cell_measures, False) diff --git a/lib/iris/experimental/ugrid/__init__.py b/lib/iris/experimental/ugrid/__init__.py index bfc570fcfd..e31de8dd59 100644 --- a/lib/iris/experimental/ugrid/__init__.py +++ b/lib/iris/experimental/ugrid/__init__.py @@ -522,6 +522,8 @@ def __eq__(self, other): if hasattr(other, "metadata"): # metadata comparison eq = self.metadata == other.metadata + if eq: + eq = self.shape == other.shape if eq: eq = ( self.indices_by_src() == other.indices_by_src() diff --git a/lib/iris/fileformats/netcdf.py b/lib/iris/fileformats/netcdf.py index ef0100edd7..edd639cebd 100644 --- a/lib/iris/fileformats/netcdf.py +++ b/lib/iris/fileformats/netcdf.py @@ -976,6 +976,11 @@ def __setitem__(self, keys, arr): self.target[keys] = arr +# NOTE : this matches :class:`iris.experimental.ugrid.Mesh.LOCATIONS`, +# but in a specific preferred order. +MESH_LOCATIONS = ("node", "edge", "face") + + class Saver: """A manager for saving netcdf files.""" @@ -1017,12 +1022,15 @@ def __init__(self, filename, netcdf_format): # All persistent variables #: CF name mapping with iris coordinates self._name_coord_map = CFNameCoordMap() - #: List of dimension coordinates added to the file - self._dim_coords = [] + #: Map of dimensions to characteristic coordinates with which they are identified + self._dim_names_and_coords = CFNameCoordMap() #: List of grid mappings added to the file self._coord_systems = [] #: A dictionary, listing dimension names and corresponding length self._existing_dim = {} + #: A map from meshes to their actual file dimensions (names). + # NB: might not match those of the mesh, if they were 'incremented'. + self._mesh_dims = {} #: A dictionary, mapping formula terms to owner cf variable name self._formula_terms_cache = {} #: NetCDF dataset @@ -1191,15 +1199,25 @@ def write( self.check_attribute_compliance(coord, coord.points) # Get suitable dimension names. - dimension_names = self._get_dim_names(cube) + cube_dimensions, mesh_dimensions = self._get_dim_names(cube) + + # Create all the CF-netCDF data dimensions. + # Put mesh dims first, then non-mesh dims in cube-occurring order. + nonmesh_dimensions = [ + dim for dim in cube_dimensions if dim not in mesh_dimensions + ] + all_dimensions = mesh_dimensions + nonmesh_dimensions + self._create_cf_dimensions(cube, all_dimensions, unlimited_dimensions) - # Create the CF-netCDF data dimensions. - self._create_cf_dimensions(cube, dimension_names, unlimited_dimensions) + # Create the mesh components, if there is a mesh. + # We do this before creating the data-var, so that mesh vars precede + # data-vars in the file. + cf_mesh_name = self._add_mesh(cube) # Create the associated cube CF-netCDF data variable. cf_var_cube = self._create_cf_data_variable( cube, - dimension_names, + cube_dimensions, local_keys, zlib=zlib, complevel=complevel, @@ -1213,24 +1231,31 @@ def write( fill_value=fill_value, ) + # Associate any mesh with the data-variable. + # N.B. _add_mesh cannot do this, as we want to put mesh variables + # before data-variables in the file. + if cf_mesh_name is not None: + _setncattr(cf_var_cube, "mesh", cf_mesh_name) + _setncattr(cf_var_cube, "location", cube.location) + # Add coordinate variables. - self._add_dim_coords(cube, dimension_names) + self._add_dim_coords(cube, cube_dimensions) # Add the auxiliary coordinate variables and associate the data # variable to them - self._add_aux_coords(cube, cf_var_cube, dimension_names) + self._add_aux_coords(cube, cf_var_cube, cube_dimensions) # Add the cell_measures variables and associate the data # variable to them - self._add_cell_measures(cube, cf_var_cube, dimension_names) + self._add_cell_measures(cube, cf_var_cube, cube_dimensions) # Add the ancillary_variables variables and associate the data variable # to them - self._add_ancillary_variables(cube, cf_var_cube, dimension_names) + self._add_ancillary_variables(cube, cf_var_cube, cube_dimensions) # Add the formula terms to the appropriate cf variables for each # aux factory in the cube. - self._add_aux_factories(cube, cf_var_cube, dimension_names) + self._add_aux_factories(cube, cf_var_cube, cube_dimensions) # Add data variable-only attribute names to local_keys. if local_keys is None: @@ -1355,13 +1380,114 @@ def _create_cf_dimensions( size = self._existing_dim[dim_name] self._dataset.createDimension(dim_name, size) + def _add_mesh(self, cube): + """ + Add the cube's mesh, and all related variables to the dataset. + Includes all the mesh-element coordinate and connectivity variables. + + ..note:: + + Here, we do *not* add the relevant referencing attributes to the + data-variable, because we want to create the data-variable later. + + Args: + + * cube (:class:`iris.cube.Cube`): + A :class:`iris.cube.Cube` to be saved to a netCDF file. + + Returns: + * cf_mesh_name (string or None): + The name of the mesh variable created, or None if the cube does not + have a mesh. + + """ + cf_mesh_name = None + mesh = cube.mesh + if mesh: + if mesh in self._name_coord_map.coords: + cf_mesh_name = self._name_coord_map.name(mesh) + else: + cf_mesh_name = self._create_mesh(cube, mesh) + self._name_coord_map.append(cf_mesh_name, mesh) + + cf_mesh_var = self._dataset.variables[cf_mesh_name] + + # Get the mesh-element dim names. + mesh_dims = self._mesh_dims[mesh] + + # Add all the element coordinate variables. + for location in MESH_LOCATIONS: + coords_meshobj_attr = f"{location}_coords" + coords_file_attr = f"{location}_coordinates" + mesh_coords = getattr(mesh, coords_meshobj_attr, None) + if mesh_coords: + coord_names = [] + for coord in mesh_coords: + if coord is None: + continue # an awkward thing that mesh.coords does + coord_name = self._create_generic_cf_array_var( + cube, + [], + coord, + element_dims=(mesh_dims[location],), + ) + coord_names.append(coord_name) + # Record the coordinates (if any) on the mesh variable. + if coord_names: + coord_names = " ".join(coord_names) + _setncattr(cf_mesh_var, coords_file_attr, coord_names) + + # Add all the connectivity variables. + # pre-fetch the set + ignore "None"s -- looks like a bug ? + conns = [ + conn for conn in mesh.all_connectivities if conn is not None + ] + for conn in conns: + # Get the connectivity role, = "{loc1}_{loc2}_connectivity". + cf_conn_attr_name = conn.cf_role + loc_from, loc_to, _ = cf_conn_attr_name.split("_") + # Construct a trailing dimension name. + last_dim = f"{cf_mesh_name}_{loc_from}_N_{loc_to}s" + # Create if it does not already exist. + if last_dim not in self._dataset.dimensions: + length = conn.shape[1 - conn.src_dim] + self._dataset.createDimension(last_dim, length) + + # Create variable. + # NOTE: for connectivities *with missing points*, this will use a + # fixed standard fill-value of -1. In that case, we also add a + # mesh property '_FillValue', below. + loc_dim_name = mesh_dims[loc_from] + conn_dims = (loc_dim_name, last_dim) + if conn.src_dim == 1: + # Has the 'other' dimension order, =reversed + conn_dims = conn_dims[::-1] + cf_conn_name = self._create_generic_cf_array_var( + cube, [], conn, element_dims=conn_dims, fill_value=-1 + ) + # Add essential attributes to the Connectivity variable. + cf_conn_var = self._dataset.variables[cf_conn_name] + _setncattr(cf_conn_var, "cf_role", cf_conn_attr_name) + _setncattr(cf_conn_var, "start_index", conn.start_index) + + # Record the connectivity on the parent mesh var. + _setncattr(cf_mesh_var, cf_conn_attr_name, cf_conn_name) + # If the connectivity had the 'alternate' dimension order, add the + # relevant dimension property + if conn.src_dim == 1: + loc_dim_attr = f"{loc_from}_dimension" + # Should only get here once. + assert loc_dim_attr not in cf_mesh_var.ncattrs() + _setncattr(cf_mesh_var, loc_dim_attr, loc_dim_name) + + return cf_mesh_name + def _add_inner_related_vars( self, cube, cf_var_cube, dimension_names, coordlike_elements, - saver_create_method, role_attribute_name, ): # Common method to create a set of file variables and attach them to @@ -1373,7 +1499,9 @@ def _add_inner_related_vars( ): # Create the associated CF-netCDF variable. if element not in self._name_coord_map.coords: - cf_name = saver_create_method(cube, dimension_names, element) + cf_name = self._create_generic_cf_array_var( + cube, dimension_names, element + ) self._name_coord_map.append(cf_name, element) else: cf_name = self._name_coord_map.name(element) @@ -1404,12 +1532,15 @@ def _add_aux_coords(self, cube, cf_var_cube, dimension_names): Names associated with the dimensions of the cube. """ + # Exclude mesh coords, which are bundled in with the aux-coords. + aux_coords_no_mesh = [ + coord for coord in cube.aux_coords if not hasattr(coord, "mesh") + ] return self._add_inner_related_vars( cube, cf_var_cube, dimension_names, - cube.aux_coords, - self._create_cf_coord_variable, + aux_coords_no_mesh, "coordinates", ) @@ -1432,7 +1563,6 @@ def _add_cell_measures(self, cube, cf_var_cube, dimension_names): cf_var_cube, dimension_names, cube.cell_measures(), - self._create_cf_cell_measure_variable, "cell_measures", ) @@ -1456,7 +1586,6 @@ def _add_ancillary_variables(self, cube, cf_var_cube, dimension_names): cf_var_cube, dimension_names, cube.ancillary_variables(), - self._create_cf_ancildata_variable, "ancillary_variables", ) @@ -1476,7 +1605,7 @@ def _add_dim_coords(self, cube, dimension_names): for coord in cube.dim_coords: # Create the associated coordinate CF-netCDF variable. if coord not in self._name_coord_map.coords: - cf_name = self._create_cf_coord_variable( + cf_name = self._create_generic_cf_array_var( cube, dimension_names, coord ) self._name_coord_map.append(cf_name, coord) @@ -1554,7 +1683,7 @@ def _add_aux_factories(self, cube, cf_var_cube, dimension_names): name = self._formula_terms_cache.get(key) if name is None: # Create a new variable - name = self._create_cf_coord_variable( + name = self._create_generic_cf_array_var( cube, dimension_names, primary_coord ) cf_var = self._dataset.variables[name] @@ -1587,57 +1716,158 @@ def _get_dim_names(self, cube): A :class:`iris.cube.Cube` to be saved to a netCDF file. Returns: - List of dimension names with length equal the number of dimensions - in the cube. + cube_dimensions, mesh_dimensions + * cube_dimensions (list of string): + A lists of dimension names for each dimension of the cube + * mesh_dimensions (list of string): + A list of the mesh dimensions of the attached mesh, if any. + + ..note:: + One of the mesh dimensions will generally also appear in the cube + dimensions. """ dimension_names = [] - for dim in range(cube.ndim): - coords = cube.coords(dimensions=dim, dim_coords=True) - if coords: - coord = coords[0] - dim_name = self._get_coord_variable_name(cube, coord) - # Add only dimensions that have not already been added. - if coord not in self._dim_coords: - # Determine unique dimension name - while ( - dim_name in self._existing_dim - or dim_name in self._name_coord_map.names - ): - dim_name = self._increment_name(dim_name) + def record_dimension(dim_name, length, matching_coords=[]): + """ + Record a file dimension, its length and associated coordinates. + + If the dimension has been seen already, check that it's length + matches the earlier finding. - # Update names added, current cube dim names used and - # unique coordinates added. - self._existing_dim[dim_name] = coord.shape[0] - dimension_names.append(dim_name) - self._dim_coords.append(coord) + """ + if dim_name not in self._existing_dim: + self._existing_dim[dim_name] = length + else: + # Just make sure we never re-write one. + # TODO: possibly merits a proper Exception with message + assert self._existing_dim[dim_name] == length + + # Add new coords (mesh or dim) to the already-seen list. + for coord in matching_coords: + if coord not in self._dim_names_and_coords.coords: + self._dim_names_and_coords.append(dim_name, coord) + + # Add the latest name to the returned list + dimension_names.append(dim_name) + + # Get info on mesh, first. + dimension_names.clear() + mesh = cube.mesh + if mesh is None: + cube_mesh_dim = None + else: + # Identify all the mesh dimensions. + # NOTE: one of these will be a cube dimension, but that one does not + # get any special handling. We *do* want to list/create them in a + # definite order (face,edge,node), and before non-mesh dimensions. + mesh_location_dimnames = {} + for location in MESH_LOCATIONS: + # Find if this location exists in the mesh, and a characteristic + # coordinate to identify it with. + # To use only _required_ UGRID components, we use a location + # coord for nodes, but a connectivity for faces/edges + if location == "node": + # For nodes, identify the dim with a coordinate variable. + # Selecting the X-axis one for definiteness. + dim_coords = mesh.coords(include_nodes=True, axis="x") else: - # Return the dim_name associated with the existing - # coordinate. - dim_name = self._name_coord_map.name(coord) - dimension_names.append(dim_name) + # For face/edge, use the non-optional connectivity variable. + cf_role = f"{location}_node_connectivity" + dim_coords = mesh.connectivities(cf_role=cf_role) + if len(dim_coords) > 0: + # As the mesh contains this location, we want to include this + # dim in our returned mesh dims. + # We should have 1 identifying variable (of either type). + assert len(dim_coords) == 1 + mesh_coord = dim_coords[0] + match = mesh_coord in self._dim_names_and_coords.coords + if match: + # For mesh-identifying coords, we require the *same* + # coord, not an identical one (i.e. "is" not "==") + name = self._dim_names_and_coords.name(mesh_coord) + stored_coord = self._dim_names_and_coords.coord(name) + match = mesh_coord is stored_coord + if match: + # Use the previous name for this dim of this mesh, from + # a previously saved cube with the same mesh. + dim_name = self._dim_names_and_coords.name(mesh_coord) + else: + if location == "node": + # always 1-d + (dim_length,) = mesh_coord.shape + else: + # extract source dim, respecting dim-ordering + dim_length = mesh_coord.shape[mesh_coord.src_dim] + location_dim_attr = f"{location}_dimension" + dim_name = getattr(mesh, location_dim_attr) + if dim_name is None: + dim_name = f"{mesh.name()}_{location}" + while dim_name in self._existing_dim: + dim_name = self._increment_name(dim_name) + + record_dimension(dim_name, dim_length, dim_coords) + + # Store the mesh dims indexed by location + mesh_location_dimnames[location] = dim_name + # Identify the cube dimension which maps to the mesh. + (cube_mesh_dim,) = cube.coords(mesh_coords=True)[0].cube_dims(cube) + # Record the actual file dimension names for each mesh. + self._mesh_dims[mesh] = mesh_location_dimnames.copy() + + mesh_dimensions = dimension_names.copy() + + # Get the cube dimensions, in order. + dimension_names.clear() + for dim in range(cube.ndim): + if dim == cube_mesh_dim: + # Handle a mesh dimension: we already named this. + dim_coords = [] + dim_name = self._mesh_dims[mesh][cube.location] else: - # No CF-netCDF coordinates describe this data dimension. - dim_name = "dim%d" % dim - if dim_name in self._existing_dim: - # Increment name if conflicted with one already existing. - if self._existing_dim[dim_name] != cube.shape[dim]: + # Get a name from the dim-coord (if any). + dim_coords = cube.coords(dimensions=dim, dim_coords=True) + if dim_coords: + # Derive a dim name from a coord. + coord = dim_coords[0] # always have at least one + + # Add only dimensions that have not already been added. + if coord in self._dim_names_and_coords.coords: + # Return the dim_name associated with the existing + # coordinate. + dim_name = self._dim_names_and_coords.name(coord) + else: + # Determine a unique dimension name from the coord + dim_name = self._get_coord_variable_name(cube, coord) while ( dim_name in self._existing_dim - and self._existing_dim[dim_name] != cube.shape[dim] or dim_name in self._name_coord_map.names ): dim_name = self._increment_name(dim_name) - # Update dictionary with new entry - self._existing_dim[dim_name] = cube.shape[dim] - else: - # Update dictionary with new entry - self._existing_dim[dim_name] = cube.shape[dim] - dimension_names.append(dim_name) - return dimension_names + else: + # No CF-netCDF coordinates describe this data dimension. + # Make up a new, distinct dimension name + dim_name = "dim%d" % dim + if dim_name in self._existing_dim: + # Increment name if conflicted with one already existing. + if self._existing_dim[dim_name] != cube.shape[dim]: + while ( + dim_name in self._existing_dim + and self._existing_dim[dim_name] + != cube.shape[dim] + or dim_name in self._name_coord_map.names + ): + dim_name = self._increment_name(dim_name) + + # Record the dimension. + record_dimension(dim_name, cube.shape[dim], dim_coords) + + cube_dimensions = dimension_names.copy() + + return cube_dimensions, mesh_dimensions @staticmethod def cf_valid_var_name(var_name): @@ -1736,7 +1966,7 @@ def _create_cf_bounds(self, coord, cf_var, cf_name): None """ - if coord.has_bounds(): + if hasattr(coord, "has_bounds") and coord.has_bounds(): # Get the values in a form which is valid for the file format. bounds = self._ensure_valid_dtype( coord.bounds, "the bounds of coordinate", coord @@ -1793,15 +2023,15 @@ def _get_cube_variable_name(self, cube): def _get_coord_variable_name(self, cube, coord): """ - Returns a CF-netCDF variable name for the given coordinate. + Returns a CF-netCDF variable name for a given coordinate-like element. Args: * cube (:class:`iris.cube.Cube`): The cube that contains the given coordinate. - * coord (:class:`iris.coords.Coord`): - An instance of a coordinate for which a CF-netCDF variable - name is required. + * coord (:class:`iris.coords.DimensionalMetadata`): + An instance of a coordinate (or similar), for which a CF-netCDF + variable name is required. Returns: A CF-netCDF variable name as a string. @@ -1812,179 +2042,202 @@ def _get_coord_variable_name(self, cube, coord): else: name = coord.standard_name or coord.long_name if not name or set(name).intersection(string.whitespace): - # Auto-generate name based on associated dimensions. - name = "" - for dim in cube.coord_dims(coord): - name += "dim{}".format(dim) - # Handle scalar coordinate (dims == ()). - if not name: - name = "unknown_scalar" + # We need to invent a name, based on its associated dimensions. + if cube.coords(coord): + # It is a regular cube coordinate. + # Auto-generate a name based on the dims. + name = "" + for dim in cube.coord_dims(coord): + name += "dim{}".format(dim) + # Handle scalar coordinate (dims == ()). + if not name: + name = "unknown_scalar" + else: + # Not a cube coord, so must be a connectivity or + # element-coordinate of the mesh. + # Name it for it's first dim, i.e. mesh-dim of its location. + from iris.experimental.ugrid import Connectivity + + if isinstance(coord, Connectivity): + location = coord.cf_role.split("_")[0] + else: + # Must be a mesh-element coordinate. + location = None + for test_location in MESH_LOCATIONS: + include_key = f"include_{test_location}s" + if coord in cube.mesh.coords({include_key: True}): + location = test_location + break + assert location is not None + location_dim_attr = f"{location}_dimension" + name = getattr(cube.mesh, location_dim_attr) + # Convert to lower case and replace whitespace by underscores. cf_name = "_".join(name.lower().split()) cf_name = self.cf_valid_var_name(cf_name) return cf_name - def _inner_create_cf_cellmeasure_or_ancil_variable( - self, cube, dimension_names, dimensional_metadata - ): + def _get_mesh_variable_name(self, mesh): """ - Create the associated CF-netCDF variable in the netCDF dataset for the - given dimensional_metadata. + Returns a CF-netCDF variable name for the given coordinate. Args: - * cube (:class:`iris.cube.Cube`): - The associated cube being saved to CF-netCDF file. - * dimension_names (list): - Names for each dimension of the cube. - * dimensional_metadata (:class:`iris.coords.CellMeasure`): - A cell measure OR ancillary variable to be saved to the - CF-netCDF file. - In either case, provides data, units and standard/long/var names. + * mesh (:class:`iris.experimental.ugrid.Mesh`): + An instance of a Mesh for which a CF-netCDF variable name is + required. Returns: - The string name of the associated CF-netCDF variable saved. + A CF-netCDF variable name as a string. """ - cf_name = self._get_coord_variable_name(cube, dimensional_metadata) - while cf_name in self._dataset.variables: - cf_name = self._increment_name(cf_name) - - # Derive the data dimension names for the coordinate. - cf_dimensions = [ - dimension_names[dim] - for dim in dimensional_metadata.cube_dims(cube) - ] - - # Get the data values. - data = dimensional_metadata.data - - if isinstance(dimensional_metadata, iris.coords.CellMeasure): - # Disallow saving of *masked* cell measures. - # NOTE: currently, this is the only functional difference required - # between variable creation for an ancillary and a cell measure. - if ma.is_masked(data): - # We can't save masked points properly, as we don't maintain a - # suitable fill_value. (Load will not record one, either). - msg = "Cell measures with missing data are not supported." - raise ValueError(msg) - - # Get the values in a form which is valid for the file format. - data = self._ensure_valid_dtype( - data, "coordinate", dimensional_metadata - ) - - # Create the CF-netCDF variable. - cf_var = self._dataset.createVariable( - cf_name, data.dtype.newbyteorder("="), cf_dimensions - ) - - # Add the data to the CF-netCDF variable. - cf_var[:] = data - - if dimensional_metadata.units.is_udunits(): - _setncattr(cf_var, "units", str(dimensional_metadata.units)) - - if dimensional_metadata.standard_name is not None: - _setncattr( - cf_var, "standard_name", dimensional_metadata.standard_name - ) - - if dimensional_metadata.long_name is not None: - _setncattr(cf_var, "long_name", dimensional_metadata.long_name) - - # Add any other custom coordinate attributes. - for name in sorted(dimensional_metadata.attributes): - value = dimensional_metadata.attributes[name] - - # Don't clobber existing attributes. - if not hasattr(cf_var, name): - _setncattr(cf_var, name, value) - + cf_name = mesh.var_name + if mesh.var_name is None: + # Prefer var-name, but accept long/standard as aliases. + cf_name = mesh.var_name or mesh.long_name or mesh.standard_name + if not cf_name: + # Auto-generate a name based on mesh properties. + cf_name = f"Mesh_{mesh.topology_dimension}d" + + # Ensure valid form for var-name. + cf_name = self.cf_valid_var_name(cf_name) return cf_name - def _create_cf_cell_measure_variable( - self, cube, dimension_names, cell_measure - ): + def _create_mesh(self, cube, mesh): """ - Create the associated CF-netCDF variable in the netCDF dataset for the - given cell_measure. + Create a mesh variable in the netCDF dataset. Args: * cube (:class:`iris.cube.Cube`): The associated cube being saved to CF-netCDF file. - * dimension_names (list): - Names for each dimension of the cube. - * cell_measure (:class:`iris.coords.CellMeasure`): - The cell measure to be saved to CF-netCDF file. + * mesh (:class:`iris.experimental.ugrid.Mesh`): + The Mesh to be saved to CF-netCDF file. Returns: The string name of the associated CF-netCDF variable saved. """ - # Note: currently shares variable creation code with ancillary-variables. - return self._inner_create_cf_cellmeasure_or_ancil_variable( - cube, dimension_names, cell_measure + # First choose a var-name for the mesh variable itself. + cf_mesh_name = self._get_mesh_variable_name(mesh) + # Disambiguate any possible clashes. + while cf_mesh_name in self._dataset.variables: + cf_mesh_name = self._increment_name(cf_mesh_name) + + # Create the main variable + cf_mesh_var = self._dataset.createVariable( + cf_mesh_name, + np.dtype(np.int32), + [], ) - def _create_cf_ancildata_variable( - self, cube, dimension_names, ancillary_variable - ): - """ - Create the associated CF-netCDF variable in the netCDF dataset for the - given ancillary variable. + # Add the basic essential attributes + _setncattr(cf_mesh_var, "cf_role", "mesh_topology") + _setncattr( + cf_mesh_var, + "topology_dimension", + np.int32(mesh.topology_dimension), + ) + # Add 'standard' names + units atributes + self._set_cf_var_attributes(cf_mesh_var, mesh) - Args: + return cf_mesh_name - * cube (:class:`iris.cube.Cube`): - The associated cube being saved to CF-netCDF file. - * dimension_names (list): - Names for each dimension of the cube. - * ancillary_variable (:class:`iris.coords.AncillaryVariable`): - The ancillary variable to be saved to the CF-netCDF file. + def _set_cf_var_attributes(self, cf_var, element): + # Deal with CF-netCDF units and standard name. + if isinstance(element, iris.coords.Coord): + # Fix "degree" units if needed. + # TODO: rewrite the handler routine to a more sensible API. + _, _, units_str = self._cf_coord_identity(element) + else: + units_str = str(element.units) - Returns: - The string name of the associated CF-netCDF variable saved. + if cf_units.as_unit(units_str).is_udunits(): + _setncattr(cf_var, "units", units_str) - """ - # Note: currently shares variable creation code with cell-measures. - return self._inner_create_cf_cellmeasure_or_ancil_variable( - cube, dimension_names, ancillary_variable - ) + standard_name = element.standard_name + if standard_name is not None: + _setncattr(cf_var, "standard_name", standard_name) + + long_name = element.long_name + if long_name is not None: + _setncattr(cf_var, "long_name", long_name) + + # Add the CF-netCDF calendar attribute. + if element.units.calendar: + _setncattr(cf_var, "calendar", str(element.units.calendar)) + + # Add any other custom coordinate attributes. + for name in sorted(element.attributes): + value = element.attributes[name] + + if name == "STASH": + # Adopting provisional Metadata Conventions for representing MO + # Scientific Data encoded in NetCDF Format. + name = "um_stash_source" + value = str(value) - def _create_cf_coord_variable(self, cube, dimension_names, coord): + # Don't clobber existing attributes. + if not hasattr(cf_var, name): + _setncattr(cf_var, name, value) + + def _create_generic_cf_array_var( + self, cube, cube_dim_names, element, element_dims=None, fill_value=None + ): """ Create the associated CF-netCDF variable in the netCDF dataset for the - given coordinate. If required, also create the CF-netCDF bounds - variable and associated dimension. + given dimensional_metadata. + + ..note:: + If the metadata element is a coord, it may also contain bounds. + In which case, an additional var is created and linked to it. Args: * cube (:class:`iris.cube.Cube`): The associated cube being saved to CF-netCDF file. - * dimension_names (list): - Names for each dimension of the cube. - * coord (:class:`iris.coords.Coord`): - The coordinate to be saved to CF-netCDF file. + * cube_dim_names (list of string): + The name of each dimension of the cube. + Not used if 'element_dims' is not None. + * element (:class:`iris.coords.DimensionalMetadata`): + A cube component, represented by a file variable, e.g. a coordinate + or cell-measure. + Provides data, units and standard/long/var names. + * element_dims (list of string, or None): + If set, contains the variable dimension (names), + otherwise these are taken from `element.cube_dims[cube]`. + For Mesh components (element coordinates and connectivities), this + *must* be passed in, as "element.cube_dims" does not function. + * fill_value (number or None): + If set, fill any masked data points with this value. Returns: - The string name of the associated CF-netCDF variable saved. + var_name (string): + The name of the CF-netCDF variable created. """ - cf_name = self._get_coord_variable_name(cube, coord) + # Work out the var-name to use. + cf_name = self._get_coord_variable_name(cube, element) while cf_name in self._dataset.variables: cf_name = self._increment_name(cf_name) - # Derive the data dimension names for the coordinate. - cf_dimensions = [ - dimension_names[dim] for dim in cube.coord_dims(coord) - ] - - if np.issubdtype(coord.points.dtype, np.str_): - string_dimension_depth = coord.points.dtype.itemsize - if coord.points.dtype.kind == "U": + if element_dims is None: + # Get the list of file-dimensions (names), to create the variable. + element_dims = [ + cube_dim_names[dim] for dim in element.cube_dims(cube) + ] # NB using 'cube_dims' as this works for any type of element + + # Get the data values, in a way which works for any element type, as + # all are subclasses of _DimensionalMetadata. + # (e.g. =points if a coord, =data if an ancillary, etc) + data = element._values + + if np.issubdtype(data.dtype, np.str_): + # Deal with string-type variables. + # Typically CF label variables, but also possibly ancil-vars ? + string_dimension_depth = data.dtype.itemsize + if data.dtype.kind == "U": string_dimension_depth //= 4 string_dimension_name = "string%d" % string_dimension_depth @@ -1994,87 +2247,81 @@ def _create_cf_coord_variable(self, cube, dimension_names, coord): string_dimension_name, string_dimension_depth ) - # Add the string length dimension to dimension names. - cf_dimensions.append(string_dimension_name) + # Add the string length dimension to the variable dimensions. + element_dims.append(string_dimension_name) # Create the label coordinate variable. - cf_var = self._dataset.createVariable( - cf_name, "|S1", cf_dimensions - ) + cf_var = self._dataset.createVariable(cf_name, "|S1", element_dims) - # Add the payload to the label coordinate variable. - if len(cf_dimensions) == 1: - cf_var[:] = list( - "%- *s" % (string_dimension_depth, coord.points[0]) - ) + # Convert data from an array of strings into a character array + # with an extra string-length dimension. + if len(element_dims) == 1: + data = list("%- *s" % (string_dimension_depth, data[0])) else: - for index in np.ndindex(coord.points.shape): + orig_shape = data.shape + new_shape = orig_shape + (string_dimension_depth,) + new_data = np.zeros(new_shape, cf_var.dtype) + for index in np.ndindex(orig_shape): index_slice = tuple(list(index) + [slice(None, None)]) - cf_var[index_slice] = list( - "%- *s" % (string_dimension_depth, coord.points[index]) + new_data[index_slice] = list( + "%- *s" % (string_dimension_depth, data[index]) ) + data = new_data else: - # Identify the collection of coordinates that represent CF-netCDF - # coordinate variables. - cf_coordinates = cube.dim_coords + # A normal (numeric) variable. + # ensure a valid datatype for the file format. + element_type = type(element).__name__ + data = self._ensure_valid_dtype(data, element_type, element) + + if fill_value is not None: + if np.ma.is_masked(data): + # Use a specific fill-value in place of the netcdf default. + data = np.ma.filled(data, fill_value) + else: + # Create variable without a (non-standard) fill_value + fill_value = None + + # Check if this is a dim-coord. + is_dimcoord = element in cube.dim_coords + + if isinstance(element, iris.coords.CellMeasure): + # Disallow saving of *masked* cell measures. + # NOTE: currently, this is the only functional difference in + # variable creation between an ancillary and a cell measure. + if ma.is_masked(data): + # We can't save masked points properly, as we don't maintain + # a fill_value. (Load will not record one, either). + msg = "Cell measures with missing data are not supported." + raise ValueError(msg) - if coord in cf_coordinates: + if is_dimcoord: # By definition of a CF-netCDF coordinate variable this # coordinate must be 1-D and the name of the CF-netCDF variable # must be the same as its dimension name. - cf_name = cf_dimensions[0] - - # Get the values in a form which is valid for the file format. - points = self._ensure_valid_dtype( - coord.points, "coordinate", coord - ) + cf_name = element_dims[0] # Create the CF-netCDF variable. cf_var = self._dataset.createVariable( - cf_name, points.dtype.newbyteorder("="), cf_dimensions + cf_name, + data.dtype.newbyteorder("="), + element_dims, + fill_value=fill_value, ) # Add the axis attribute for spatio-temporal CF-netCDF coordinates. - if coord in cf_coordinates: - axis = iris.util.guess_coord_axis(coord) + if is_dimcoord: + axis = iris.util.guess_coord_axis(element) if axis is not None and axis.lower() in SPATIO_TEMPORAL_AXES: _setncattr(cf_var, "axis", axis.upper()) - # Add the data to the CF-netCDF variable. - cf_var[:] = points - - # Create the associated CF-netCDF bounds variable. - self._create_cf_bounds(coord, cf_var, cf_name) - - # Deal with CF-netCDF units and standard name. - standard_name, long_name, units = self._cf_coord_identity(coord) - - if cf_units.as_unit(units).is_udunits(): - _setncattr(cf_var, "units", units) + # Create the associated CF-netCDF bounds variable, if any. + self._create_cf_bounds(element, cf_var, cf_name) - if standard_name is not None: - _setncattr(cf_var, "standard_name", standard_name) - - if long_name is not None: - _setncattr(cf_var, "long_name", long_name) - - # Add the CF-netCDF calendar attribute. - if coord.units.calendar: - _setncattr(cf_var, "calendar", coord.units.calendar) - - # Add any other custom coordinate attributes. - for name in sorted(coord.attributes): - value = coord.attributes[name] - - if name == "STASH": - # Adopting provisional Metadata Conventions for representing MO - # Scientific Data encoded in NetCDF Format. - name = "um_stash_source" - value = str(value) + # Add the data to the CF-netCDF variable. + cf_var[:] = data - # Don't clobber existing attributes. - if not hasattr(cf_var, name): - _setncattr(cf_var, name, value) + # Add names + units + self._set_cf_var_attributes(cf_var, element) return cf_name diff --git a/lib/iris/tests/integration/experimental/test_ugrid_save.py b/lib/iris/tests/integration/experimental/test_ugrid_save.py new file mode 100644 index 0000000000..3726d16f4c --- /dev/null +++ b/lib/iris/tests/integration/experimental/test_ugrid_save.py @@ -0,0 +1,128 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +""" +Integration tests for NetCDF-UGRID file saving. + +""" +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests # isort:skip + +import glob +from pathlib import Path +import shutil +from subprocess import check_call +import tempfile + +import iris +from iris.experimental.ugrid import PARSE_UGRID_ON_LOAD +import iris.fileformats.netcdf +from iris.tests import IrisTest +from iris.tests.stock.netcdf import _add_standard_data + + +class TestBasicSave(IrisTest): + @classmethod + def setUpClass(cls): + cls.temp_dir = Path(tempfile.mkdtemp()) + cls.examples_dir = ( + Path(__file__).absolute().parent / "ugrid_conventions_examples" + ) + example_paths = glob.glob(str(cls.examples_dir / "*ex*.cdl")) + example_names = [ + str(Path(filepath).name).split("_")[1] # = "ex" + for filepath in example_paths + ] + cls.example_names_paths = { + name: path for name, path in zip(example_names, example_paths) + } + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.temp_dir) + + def test_example_result_cdls(self): + # Snapshot the result of saving the example cases. + for ex_name, filepath in self.example_names_paths.items(): + target_ncfile_path = str(self.temp_dir / f"{ex_name}.nc") + # Create a netcdf file from the test CDL. + check_call( + f"ncgen {filepath} -k4 -o {target_ncfile_path}", shell=True + ) + # Fill in blank data-variables. + _add_standard_data(target_ncfile_path) + # Load as Iris data + with PARSE_UGRID_ON_LOAD.context(): + cubes = iris.load(target_ncfile_path) + # Re-save, to check the save behaviour. + resave_ncfile_path = str(self.temp_dir / f"{ex_name}_resaved.nc") + iris.save(cubes, resave_ncfile_path) + # Check the output against a CDL snapshot. + refdir_relpath = ( + "integration/experimental/ugrid_save/TestBasicSave/" + ) + reffile_name = str(Path(filepath).name).replace(".nc", ".cdl") + reffile_path = refdir_relpath + reffile_name + self.assertCDL(resave_ncfile_path, reference_filename=reffile_path) + + def test_example_roundtrips(self): + # Check that save-and-loadback leaves Iris data unchanged, + # for data derived from each UGRID example CDL. + for ex_name, filepath in self.example_names_paths.items(): + print(f"Roundtrip checking : {ex_name}") + target_ncfile_path = str(self.temp_dir / f"{ex_name}.nc") + # Create a netcdf file from the test CDL. + check_call( + f"ncgen {filepath} -k4 -o {target_ncfile_path}", shell=True + ) + # Fill in blank data-variables. + _add_standard_data(target_ncfile_path) + # Load the original as Iris data + with PARSE_UGRID_ON_LOAD.context(): + orig_cubes = iris.load(target_ncfile_path) + + if "ex4" in ex_name: + # Discard the extra formula terms component cubes + # Saving these does not do what you expect + orig_cubes = orig_cubes.extract("datavar") + + # Save-and-load-back to compare the Iris saved result. + resave_ncfile_path = str(self.temp_dir / f"{ex_name}_resaved.nc") + iris.save(orig_cubes, resave_ncfile_path) + with PARSE_UGRID_ON_LOAD.context(): + savedloaded_cubes = iris.load(resave_ncfile_path) + + # This should match the original exactly + # ..EXCEPT for our inability to compare meshes. + for orig, reloaded in zip(orig_cubes, savedloaded_cubes): + for cube in (orig, reloaded): + # Remove conventions attributes, which may differ. + cube.attributes.pop("Conventions", None) + # Remove var-names, which may differ. + cube.var_name = None + + # Compare the mesh contents (as we can't compare actual meshes) + self.assertEqual(orig.location, reloaded.location) + orig_mesh = orig.mesh + reloaded_mesh = reloaded.mesh + self.assertEqual( + orig_mesh.all_coords, reloaded_mesh.all_coords + ) + self.assertEqual( + orig_mesh.all_connectivities, + reloaded_mesh.all_connectivities, + ) + # Index the cubes to replace meshes with meshcoord-derived aux coords. + # This needs [:0] on the mesh dim, so do that on all dims. + keys = tuple([slice(0, None)] * orig.ndim) + orig = orig[keys] + reloaded = reloaded[keys] + # Resulting cubes, with collapsed mesh, should be IDENTICAL. + self.assertEqual(orig, reloaded) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/integration/experimental/ugrid_conventions_examples/README.txt b/lib/iris/tests/integration/experimental/ugrid_conventions_examples/README.txt new file mode 100644 index 0000000000..2a9b5bde35 --- /dev/null +++ b/lib/iris/tests/integration/experimental/ugrid_conventions_examples/README.txt @@ -0,0 +1,16 @@ +Examples generated from CDL example sections in UGRID conventions v1.0 + ( see webpage: https://ugrid-conventions.github.io/ugrid-conventions/ ) + +CHANGES: + * added a data-var to all examples, for ease of iris-roundtripping + * EX4 : + - had a couple of missing ";"s at lineends + - the formula terms (depth+surface) should map to 'Mesh2_layers', and not to the mesh at all. + - use Mesh2d_layers dim, and have no 'mesh' or 'location' + * "EX4a" -- possibly (future) closer mix of hybrid-vertical and mesh dimensions + - *don't* think we can have a hybrid coord ON the mesh dimension + - mesh being a vertical location (only) seems to make no sense + - .. and implies that the mesh is 1d and ordered, which is not really unstructured at all + - *could* have hybrid-height with the _orography_ mapping to the mesh + - doesn't match the UGRID examples, but see : iris.tests.unit.fileformats.netcdf.test_Saver__ugrid.TestSaveUgrid__cube.test_nonmesh_hybrid_dim + diff --git a/lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex1_1d_mesh.cdl b/lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex1_1d_mesh.cdl new file mode 100644 index 0000000000..d022fedc61 --- /dev/null +++ b/lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex1_1d_mesh.cdl @@ -0,0 +1,55 @@ +netcdf ex1_1d_mesh { +dimensions: +nMesh1_node = 5 ; // nNodes +nMesh1_edge = 4 ; // nEdges + +Two = 2; + +variables: +// Mesh topology +integer Mesh1 ; +Mesh1:cf_role = "mesh_topology" ; +Mesh1:long_name = "Topology data of 1D network" ; +Mesh1:topology_dimension = 1 ; +Mesh1:node_coordinates = "Mesh1_node_x Mesh1_node_y" ; +Mesh1:edge_node_connectivity = "Mesh1_edge_nodes" ; +Mesh1:edge_coordinates = "Mesh1_edge_x Mesh1_edge_y" ; // optional attribute +integer Mesh1_edge_nodes(nMesh1_edge, Two) ; +Mesh1_edge_nodes:cf_role = "edge_node_connectivity" ; +Mesh1_edge_nodes:long_name = "Maps every edge/link to the two nodes that it connects." ; +Mesh1_edge_nodes:start_index = 1 ; + +// Mesh node coordinates +double Mesh1_node_x(nMesh1_node) ; +Mesh1_node_x:standard_name = "longitude" ; +Mesh1_node_x:long_name = "Longitude of 1D network nodes." ; +Mesh1_node_x:units = "degrees_east" ; +double Mesh1_node_y(nMesh1_node) ; +Mesh1_node_y:standard_name = "latitude" ; +Mesh1_node_y:long_name = "Latitude of 1D network nodes." ; +Mesh1_node_y:units = "degrees_north" ; + +// Optional mesh edge coordinate variables +double Mesh1_edge_x(nMesh1_edge) ; +Mesh1_edge_x:standard_name = "longitude" ; +Mesh1_edge_x:long_name = "Characteristic longitude of 1D network edge (e.g. midpoint of the edge)." ; +Mesh1_edge_x:units = "degrees_east" ; +Mesh1_edge_x:bounds = "Mesh1_edge_xbnds" ; +double Mesh1_edge_y(nMesh1_edge) ; +Mesh1_edge_y:standard_name = "latitude" ; +Mesh1_edge_y:long_name = "Characteristic latitude of 1D network edge (e.g. midpoint of the edge)." ; +Mesh1_edge_y:units = "degrees_north" ; +Mesh1_edge_y:bounds = "Mesh1_edge_ybnds" ; +double Mesh1_edge_xbnds(nMesh1_edge,Two) ; +Mesh1_edge_xbnds:standard_name = "longitude" ; +Mesh1_edge_xbnds:long_name = "Longitude bounds of 1D network edge (i.e. begin and end longitude)." ; +Mesh1_edge_xbnds:units = "degrees_east" ; +double Mesh1_edge_ybnds(nMesh1_edge,Two) ; +Mesh1_edge_ybnds:standard_name = "latitude" ; +Mesh1_edge_ybnds:long_name = "Latitude bounds of 1D network edge (i.e. begin and end latitude)." ; +Mesh1_edge_ybnds:units = "degrees_north" ; + +float datavar(nMesh1_edge) ; + datavar:mesh = "Mesh1" ; + datavar:location = "edge" ; +} diff --git a/lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex2_2d_triangular.cdl b/lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex2_2d_triangular.cdl new file mode 100644 index 0000000000..1e4e483826 --- /dev/null +++ b/lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex2_2d_triangular.cdl @@ -0,0 +1,84 @@ +netcdf ex2_2d_triangular { +dimensions: +nMesh2_node = 4 ; // nNodes +nMesh2_edge = 5 ; // nEdges +nMesh2_face = 2 ; // nFaces + +Two = 2 ; +Three = 3 ; + +variables: +// Mesh topology +integer Mesh2 ; +Mesh2:cf_role = "mesh_topology" ; +Mesh2:long_name = "Topology data of 2D unstructured mesh" ; +Mesh2:topology_dimension = 2 ; +Mesh2:node_coordinates = "Mesh2_node_x Mesh2_node_y" ; +Mesh2:face_node_connectivity = "Mesh2_face_nodes" ; +Mesh2:face_dimension = "nMesh2_face" ; +Mesh2:edge_node_connectivity = "Mesh2_edge_nodes" ; // attribute required if variables will be defined on edges +Mesh2:edge_dimension = "nMesh2_edge" ; +Mesh2:edge_coordinates = "Mesh2_edge_x Mesh2_edge_y" ; // optional attribute (requires edge_node_connectivity) +Mesh2:face_coordinates = "Mesh2_face_x Mesh2_face_y" ; // optional attribute +Mesh2:face_edge_connectivity = "Mesh2_face_edges" ; // optional attribute (requires edge_node_connectivity) +Mesh2:face_face_connectivity = "Mesh2_face_links" ; // optional attribute +Mesh2:edge_face_connectivity = "Mesh2_edge_face_links" ; // optional attribute (requires edge_node_connectivity) +integer Mesh2_face_nodes(nMesh2_face, Three) ; +Mesh2_face_nodes:cf_role = "face_node_connectivity" ; +Mesh2_face_nodes:long_name = "Maps every triangular face to its three corner nodes." ; +Mesh2_face_nodes:start_index = 1 ; +integer Mesh2_edge_nodes(nMesh2_edge, Two) ; +Mesh2_edge_nodes:cf_role = "edge_node_connectivity" ; +Mesh2_edge_nodes:long_name = "Maps every edge to the two nodes that it connects." ; +Mesh2_edge_nodes:start_index = 1 ; + +// Optional mesh topology variables +integer Mesh2_face_edges(nMesh2_face, Three) ; +Mesh2_face_edges:cf_role = "face_edge_connectivity" ; +Mesh2_face_edges:long_name = "Maps every triangular face to its three edges." ; +Mesh2_face_edges:start_index = 1 ; +integer Mesh2_face_links(nMesh2_face, Three) ; +Mesh2_face_links:cf_role = "face_face_connectivity" ; +Mesh2_face_links:long_name = "neighbor faces for faces" ; +Mesh2_face_links:start_index = 1 ; +Mesh2_face_links:_FillValue = -999 ; +Mesh2_face_links:comment = "missing neighbor faces are indicated using _FillValue" ; +integer Mesh2_edge_face_links(nMesh2_edge, Two) ; +Mesh2_edge_face_links:cf_role = "edge_face_connectivity" ; +Mesh2_edge_face_links:long_name = "neighbor faces for edges" ; +Mesh2_edge_face_links:start_index = 1 ; +Mesh2_edge_face_links:_FillValue = -999 ; +Mesh2_edge_face_links:comment = "missing neighbor faces are indicated using _FillValue" ; + +// Mesh node coordinates +double Mesh2_node_x(nMesh2_node) ; +Mesh2_node_x:standard_name = "longitude" ; +Mesh2_node_x:long_name = "Longitude of 2D mesh nodes." ; +Mesh2_node_x:units = "degrees_east" ; +double Mesh2_node_y(nMesh2_node) ; +Mesh2_node_y:standard_name = "latitude" ; +Mesh2_node_y:long_name = "Latitude of 2D mesh nodes." ; +Mesh2_node_y:units = "degrees_north" ; + +// Optional mesh face and edge coordinate variables +double Mesh2_face_x(nMesh2_face) ; +Mesh2_face_x:standard_name = "longitude" ; +Mesh2_face_x:long_name = "Characteristics longitude of 2D mesh triangle (e.g. circumcenter coordinate)." ; +Mesh2_face_x:units = "degrees_east" ; +double Mesh2_face_y(nMesh2_face) ; +Mesh2_face_y:standard_name = "latitude" ; +Mesh2_face_y:long_name = "Characteristics latitude of 2D mesh triangle (e.g. circumcenter coordinate)." ; +Mesh2_face_y:units = "degrees_north" ; +double Mesh2_edge_x(nMesh2_edge) ; +Mesh2_edge_x:standard_name = "longitude" ; +Mesh2_edge_x:long_name = "Characteristic longitude of 2D mesh edge (e.g. midpoint of the edge)." ; +Mesh2_edge_x:units = "degrees_east" ; +double Mesh2_edge_y(nMesh2_edge) ; +Mesh2_edge_y:standard_name = "latitude" ; +Mesh2_edge_y:long_name = "Characteristic latitude of 2D mesh edge (e.g. midpoint of the edge)." ; +Mesh2_edge_y:units = "degrees_north" ; + +float datavar(nMesh2_face) ; + datavar:mesh = "Mesh2" ; + datavar:location = "face" ; +} diff --git a/lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex3_2d_flexible.cdl b/lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex3_2d_flexible.cdl new file mode 100644 index 0000000000..2fa077d152 --- /dev/null +++ b/lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex3_2d_flexible.cdl @@ -0,0 +1,99 @@ +netcdf ex3_2d_flexible { +dimensions: +nMesh2_node = 5 ; // nNodes +nMesh2_edge = 6 ; // nEdges +nMesh2_face = 2 ; // nFaces +nMaxMesh2_face_nodes = 4 ; // MaxNumNodesPerFace + +Two = 2 ; + +variables: +// Mesh topology +integer Mesh2 ; +Mesh2:cf_role = "mesh_topology" ; +Mesh2:long_name = "Topology data of 2D unstructured mesh" ; +Mesh2:topology_dimension = 2 ; +Mesh2:node_coordinates = "Mesh2_node_x Mesh2_node_y" ; +Mesh2:face_node_connectivity = "Mesh2_face_nodes" ; +Mesh2:face_dimension = "nMesh2_face" ; +Mesh2:edge_node_connectivity = "Mesh2_edge_nodes" ; // attribute required if variables will be defined on edges +Mesh2:edge_dimension = "nMesh2_edge" ; +Mesh2:edge_coordinates = "Mesh2_edge_x Mesh2_edge_y" ; // optional attribute (requires edge_node_connectivity) +Mesh2:face_coordinates = "Mesh2_face_x Mesh2_face_y" ; // optional attribute +Mesh2:face_edge_connectivity = "Mesh2_face_edges" ; // optional attribute (requires edge_node_connectivity) +Mesh2:face_face_connectivity = "Mesh2_face_links" ; // optional attribute +Mesh2:edge_face_connectivity = "Mesh2_edge_face_links" ; // optional attribute (requires edge_node_connectivity) +integer Mesh2_face_nodes(nMesh2_face, nMaxMesh2_face_nodes) ; +Mesh2_face_nodes:cf_role = "face_node_connectivity" ; +Mesh2_face_nodes:long_name = "Maps every face to its corner nodes." ; +Mesh2_face_nodes:_FillValue = 999999 ; +Mesh2_face_nodes:start_index = 1 ; +integer Mesh2_edge_nodes(nMesh2_edge, Two) ; +Mesh2_edge_nodes:cf_role = "edge_node_connectivity" ; +Mesh2_edge_nodes:long_name = "Maps every edge to the two nodes that it connects." ; +Mesh2_edge_nodes:start_index = 1 ; + +// Optional mesh topology variables +integer Mesh2_face_edges(nMesh2_face, nMaxMesh2_face_nodes) ; +Mesh2_face_edges:cf_role = "face_edge_connectivity" ; +Mesh2_face_edges:long_name = "Maps every face to its edges." ; +Mesh2_face_edges:_FillValue = 999999 ; +Mesh2_face_edges:start_index = 1 ; +integer Mesh2_face_links(nMesh2_face, nMaxMesh2_face_nodes) ; +Mesh2_face_links:cf_role = "face_face_connectivity" ; +Mesh2_face_links:long_name = "neighbor faces for faces" ; +Mesh2_face_links:start_index = 1 ; +Mesh2_face_links:_FillValue = -999 ; +Mesh2_face_links:comment = "missing edges as well as missing neighbor faces are indicated using _FillValue" ; +integer Mesh2_edge_face_links(nMesh2_edge, Two) ; +Mesh2_edge_face_links:cf_role = "edge_face_connectivity" ; +Mesh2_edge_face_links:long_name = "neighbor faces for edges" ; +Mesh2_edge_face_links:start_index = 1 ; +Mesh2_edge_face_links:_FillValue = -999 ; +Mesh2_edge_face_links:comment = "missing neighbor faces are indicated using _FillValue" ; + +// Mesh node coordinates +double Mesh2_node_x(nMesh2_node) ; +Mesh2_node_x:standard_name = "longitude" ; +Mesh2_node_x:long_name = "Longitude of 2D mesh nodes." ; +Mesh2_node_x:units = "degrees_east" ; +double Mesh2_node_y(nMesh2_node) ; +Mesh2_node_y:standard_name = "latitude" ; +Mesh2_node_y:long_name = "Latitude of 2D mesh nodes." ; +Mesh2_node_y:units = "degrees_north" ; + +// Optional mesh face and edge coordinate variables +double Mesh2_face_x(nMesh2_face) ; +Mesh2_face_x:standard_name = "longitude" ; +Mesh2_face_x:long_name = "Characteristics longitude of 2D mesh face." ; +Mesh2_face_x:units = "degrees_east" ; +Mesh2_face_x:bounds = "Mesh2_face_xbnds" ; +double Mesh2_face_y(nMesh2_face) ; +Mesh2_face_y:standard_name = "latitude" ; +Mesh2_face_y:long_name = "Characteristics latitude of 2D mesh face." ; +Mesh2_face_y:units = "degrees_north" ; +Mesh2_face_y:bounds = "Mesh2_face_ybnds" ; +double Mesh2_face_xbnds(nMesh2_face,nMaxMesh2_face_nodes) ; +Mesh2_face_xbnds:standard_name = "longitude" ; +Mesh2_face_xbnds:long_name = "Longitude bounds of 2D mesh face (i.e. corner coordinates)." ; +Mesh2_face_xbnds:units = "degrees_east" ; +Mesh2_face_xbnds:_FillValue = 9.9692099683868690E36; +double Mesh2_face_ybnds(nMesh2_face,nMaxMesh2_face_nodes) ; +Mesh2_face_ybnds:standard_name = "latitude" ; +Mesh2_face_ybnds:long_name = "Latitude bounds of 2D mesh face (i.e. corner coordinates)." ; +Mesh2_face_ybnds:units = "degrees_north" ; +Mesh2_face_ybnds:_FillValue = 9.9692099683868690E36; +double Mesh2_edge_x(nMesh2_edge) ; +Mesh2_edge_x:standard_name = "longitude" ; +Mesh2_edge_x:long_name = "Characteristic longitude of 2D mesh edge (e.g. midpoint of the edge)." ; +Mesh2_edge_x:units = "degrees_east" ; +double Mesh2_edge_y(nMesh2_edge) ; +Mesh2_edge_y:standard_name = "latitude" ; +Mesh2_edge_y:long_name = "Characteristic latitude of 2D mesh edge (e.g. midpoint of the edge)." ; +Mesh2_edge_y:units = "degrees_north" ; +// bounds variables for edges skipped + +float datavar(nMesh2_face) ; + datavar:mesh = "Mesh2" ; + datavar:location = "face" ; +} diff --git a/lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex4_3d_layered.cdl b/lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex4_3d_layered.cdl new file mode 100644 index 0000000000..d154502018 --- /dev/null +++ b/lib/iris/tests/integration/experimental/ugrid_conventions_examples/ugrid_ex4_3d_layered.cdl @@ -0,0 +1,120 @@ +netcdf ex4_3d_layered { +dimensions: +nMesh2_node = 6 ; // nNodes +nMesh2_edge = 7 ; // nEdges +nMesh2_face = 2 ; // nFaces +nMaxMesh2_face_nodes = 4 ; // MaxNumNodesPerFace +Mesh2_layers = 10 ; + +Two = 2 ; + +variables: +// Mesh topology +integer Mesh2 ; +Mesh2:cf_role = "mesh_topology" ; +Mesh2:long_name = "Topology data of 2D unstructured mesh" ; +Mesh2:topology_dimension = 2 ; +Mesh2:node_coordinates = "Mesh2_node_x Mesh2_node_y" ; +Mesh2:face_node_connectivity = "Mesh2_face_nodes" ; +Mesh2:face_dimension = "nMesh2_face" ; +Mesh2:edge_node_connectivity = "Mesh2_edge_nodes" ; // attribute required if variables will be defined on edges +Mesh2:edge_dimension = "nMesh2_edge" ; +Mesh2:edge_coordinates = "Mesh2_edge_x Mesh2_edge_y" ; // optional attribute (requires edge_node_connectivity) +Mesh2:face_coordinates = "Mesh2_face_x Mesh2_face_y" ; // optional attribute +Mesh2:face_edge_connectivity = "Mesh2_face_edges" ; // optional attribute (requires edge_node_connectivity) +Mesh2:face_face_connectivity = "Mesh2_face_links" ; // optional attribute +Mesh2:edge_face_connectivity = "Mesh2_edge_face_links" ; // optional attribute (requires edge_node_connectivity) +integer Mesh2_face_nodes(nMesh2_face, nMaxMesh2_face_nodes) ; +Mesh2_face_nodes:cf_role = "face_node_connectivity" ; +Mesh2_face_nodes:long_name = "Maps every face to its corner nodes." ; +Mesh2_face_nodes:_FillValue = 999999 ; +Mesh2_face_nodes:start_index = 1 ; +integer Mesh2_edge_nodes(nMesh2_edge, Two) ; +Mesh2_edge_nodes:cf_role = "edge_node_connectivity" ; +Mesh2_edge_nodes:long_name = "Maps every edge to the two nodes that it connects." ; +Mesh2_edge_nodes:start_index = 1 ; + +// Optional mesh topology variables +integer Mesh2_face_edges(nMesh2_face, nMaxMesh2_face_nodes) ; +Mesh2_face_edges:cf_role = "face_edge_connectivity" ; +Mesh2_face_edges:long_name = "Maps every face to its edges." ; +Mesh2_face_edges:_FillValue = 999999 ; +Mesh2_face_edges:start_index = 1 ; +integer Mesh2_face_links(nMesh2_face, nMaxMesh2_face_nodes) ; +Mesh2_face_links:cf_role = "face_face_connectivity" ; +Mesh2_face_links:long_name = "neighbor faces for faces" ; +Mesh2_face_links:start_index = 1 ; +Mesh2_face_links:_FillValue = -999 ; +Mesh2_face_links:comment = "missing edges as well as missing neighbor faces are indicated using _FillValue" ; +integer Mesh2_edge_face_links(nMesh2_edge, Two) ; +Mesh2_edge_face_links:cf_role = "edge_face_connectivity" ; +Mesh2_edge_face_links:long_name = "neighbor faces for edges" ; +Mesh2_edge_face_links:start_index = 1 ; +Mesh2_edge_face_links:_FillValue = -999 ; +Mesh2_edge_face_links:comment = "missing neighbor faces are indicated using _FillValue" ; + +// Mesh node coordinates +double Mesh2_node_x(nMesh2_node) ; +Mesh2_node_x:standard_name = "longitude" ; +Mesh2_node_x:long_name = "Longitude of 2D mesh nodes." ; +Mesh2_node_x:units = "degrees_east" ; +double Mesh2_node_y(nMesh2_node) ; +Mesh2_node_y:standard_name = "latitude" ; +Mesh2_node_y:long_name = "Latitude of 2D mesh nodes." ; +Mesh2_node_y:units = "degrees_north" ; + +// Optional mesh face and edge coordinate variables +double Mesh2_face_x(nMesh2_face) ; +Mesh2_face_x:standard_name = "longitude" ; +Mesh2_face_x:long_name = "Characteristics longitude of 2D mesh face." ; +Mesh2_face_x:units = "degrees_east" ; +Mesh2_face_x:bounds = "Mesh2_face_xbnds" ; +double Mesh2_face_y(nMesh2_face) ; +Mesh2_face_y:standard_name = "latitude" ; +Mesh2_face_y:long_name = "Characteristics latitude of 2D mesh face." ; +Mesh2_face_y:units = "degrees_north" ; +Mesh2_face_y:bounds = "Mesh2_face_ybnds" ; +double Mesh2_face_xbnds(nMesh2_face,nMaxMesh2_face_nodes) ; +Mesh2_face_xbnds:standard_name = "longitude" ; +Mesh2_face_xbnds:long_name = "Longitude bounds of 2D mesh face (i.e. corner coordinates)." ; +Mesh2_face_xbnds:units = "degrees_east" ; +Mesh2_face_xbnds:_FillValue = 9.9692099683868690E36; +double Mesh2_face_ybnds(nMesh2_face,nMaxMesh2_face_nodes) ; +Mesh2_face_ybnds:standard_name = "latitude" ; +Mesh2_face_ybnds:long_name = "Latitude bounds of 2D mesh face (i.e. corner coordinates)." ; +Mesh2_face_ybnds:units = "degrees_north" ; +Mesh2_face_ybnds:_FillValue = 9.9692099683868690E36; +double Mesh2_edge_x(nMesh2_edge) ; +Mesh2_edge_x:standard_name = "longitude" ; +Mesh2_edge_x:long_name = "Characteristic longitude of 2D mesh edge (e.g. midpoint of the edge)." ; +Mesh2_edge_x:units = "degrees_east" ; +double Mesh2_edge_y(nMesh2_edge) ; +Mesh2_edge_y:standard_name = "latitude" ; +Mesh2_edge_y:long_name = "Characteristic latitude of 2D mesh edge (e.g. midpoint of the edge)." ; +Mesh2_edge_y:units = "degrees_north" ; +// bounds variables for edges skipped + +// Vertical coordinate +double Mesh2_layers(Mesh2_layers) ; +Mesh2_layers:standard_name = "ocean_sigma_coordinate" ; +Mesh2_layers:long_name = "sigma at layer midpoints" ; +Mesh2_layers:positive = "up" ; +Mesh2_layers:formula_terms = "sigma: Mesh2_layers eta: Mesh2_surface depth: Mesh2_depth" ; +double Mesh2_depth(Mesh2_layers) ; +Mesh2_depth:standard_name = "sea_floor_depth_below_geoid" ; +Mesh2_depth:units = "m" ; +Mesh2_depth:positive = "down" ; +Mesh2_depth:coordinates = "Mesh2_node_x Mesh2_node_y" ; +double Mesh2_surface(Mesh2_layers) ; +Mesh2_surface:standard_name = "sea_surface_height_above_geoid" ; +Mesh2_surface:units = "m" ; +Mesh2_surface:coordinates = "Mesh2_face_x Mesh2_face_y" ; + +float datavar(Mesh2_layers, nMesh2_face) ; + datavar:mesh = "Mesh2" ; + datavar:location = "face" ; + +data: +Mesh2_layers = 0., 1., 2., 3., 4., 5., 6., 7., 8., 9. ; + +} diff --git a/lib/iris/tests/results/integration/experimental/ugrid_save/TestBasicSave/ugrid_ex1_1d_mesh.cdl b/lib/iris/tests/results/integration/experimental/ugrid_save/TestBasicSave/ugrid_ex1_1d_mesh.cdl new file mode 100644 index 0000000000..517991a17a --- /dev/null +++ b/lib/iris/tests/results/integration/experimental/ugrid_save/TestBasicSave/ugrid_ex1_1d_mesh.cdl @@ -0,0 +1,39 @@ +dimensions: + Mesh1_edge_N_nodes = 2 ; + nMesh1_edge = 4 ; + nMesh1_node = 5 ; +variables: + int Mesh1 ; + Mesh1:cf_role = "mesh_topology" ; + Mesh1:topology_dimension = 1 ; + Mesh1:long_name = "Topology data of 1D network" ; + Mesh1:node_coordinates = "Mesh1_node_x Mesh1_node_y" ; + Mesh1:edge_coordinates = "Mesh1_edge_x Mesh1_edge_y" ; + Mesh1:edge_node_connectivity = "Mesh1_edge_nodes" ; + double Mesh1_node_x(nMesh1_node) ; + Mesh1_node_x:units = "degrees_east" ; + Mesh1_node_x:standard_name = "longitude" ; + Mesh1_node_x:long_name = "Longitude of 1D network nodes." ; + double Mesh1_node_y(nMesh1_node) ; + Mesh1_node_y:units = "degrees_north" ; + Mesh1_node_y:standard_name = "latitude" ; + Mesh1_node_y:long_name = "Latitude of 1D network nodes." ; + double Mesh1_edge_x(nMesh1_edge) ; + Mesh1_edge_x:units = "degrees_east" ; + Mesh1_edge_x:standard_name = "longitude" ; + Mesh1_edge_x:long_name = "Characteristic longitude of 1D network edge (e.g. midpoint of the edge)." ; + double Mesh1_edge_y(nMesh1_edge) ; + Mesh1_edge_y:units = "degrees_north" ; + Mesh1_edge_y:standard_name = "latitude" ; + Mesh1_edge_y:long_name = "Characteristic latitude of 1D network edge (e.g. midpoint of the edge)." ; + int Mesh1_edge_nodes(nMesh1_edge, Mesh1_edge_N_nodes) ; + Mesh1_edge_nodes:long_name = "Maps every edge/link to the two nodes that it connects." ; + Mesh1_edge_nodes:cf_role = "edge_node_connectivity" ; + Mesh1_edge_nodes:start_index = 1 ; + float datavar(nMesh1_edge) ; + datavar:mesh = "Mesh1" ; + datavar:location = "edge" ; + +// global attributes: + :Conventions = "CF-1.7" ; +} diff --git a/lib/iris/tests/results/integration/experimental/ugrid_save/TestBasicSave/ugrid_ex2_2d_triangular.cdl b/lib/iris/tests/results/integration/experimental/ugrid_save/TestBasicSave/ugrid_ex2_2d_triangular.cdl new file mode 100644 index 0000000000..5d01a263d6 --- /dev/null +++ b/lib/iris/tests/results/integration/experimental/ugrid_save/TestBasicSave/ugrid_ex2_2d_triangular.cdl @@ -0,0 +1,75 @@ +dimensions: + Mesh2_edge_N_faces = 2 ; + Mesh2_edge_N_nodes = 2 ; + Mesh2_face_N_edges = 3 ; + Mesh2_face_N_faces = 3 ; + Mesh2_face_N_nodes = 3 ; + nMesh2_edge = 5 ; + nMesh2_face = 2 ; + nMesh2_node = 4 ; +variables: + int Mesh2 ; + Mesh2:cf_role = "mesh_topology" ; + Mesh2:topology_dimension = 2 ; + Mesh2:long_name = "Topology data of 2D unstructured mesh" ; + Mesh2:node_coordinates = "Mesh2_node_x Mesh2_node_y" ; + Mesh2:edge_coordinates = "Mesh2_edge_x Mesh2_edge_y" ; + Mesh2:face_coordinates = "Mesh2_face_x Mesh2_face_y" ; + Mesh2:face_node_connectivity = "Mesh2_face_nodes" ; + Mesh2:edge_node_connectivity = "Mesh2_edge_nodes" ; + Mesh2:face_edge_connectivity = "Mesh2_face_edges" ; + Mesh2:face_face_connectivity = "Mesh2_face_links" ; + Mesh2:edge_face_connectivity = "Mesh2_edge_face_links" ; + double Mesh2_node_x(nMesh2_node) ; + Mesh2_node_x:units = "degrees_east" ; + Mesh2_node_x:standard_name = "longitude" ; + Mesh2_node_x:long_name = "Longitude of 2D mesh nodes." ; + double Mesh2_node_y(nMesh2_node) ; + Mesh2_node_y:units = "degrees_north" ; + Mesh2_node_y:standard_name = "latitude" ; + Mesh2_node_y:long_name = "Latitude of 2D mesh nodes." ; + double Mesh2_edge_x(nMesh2_edge) ; + Mesh2_edge_x:units = "degrees_east" ; + Mesh2_edge_x:standard_name = "longitude" ; + Mesh2_edge_x:long_name = "Characteristic longitude of 2D mesh edge (e.g. midpoint of the edge)." ; + double Mesh2_edge_y(nMesh2_edge) ; + Mesh2_edge_y:units = "degrees_north" ; + Mesh2_edge_y:standard_name = "latitude" ; + Mesh2_edge_y:long_name = "Characteristic latitude of 2D mesh edge (e.g. midpoint of the edge)." ; + double Mesh2_face_x(nMesh2_face) ; + Mesh2_face_x:units = "degrees_east" ; + Mesh2_face_x:standard_name = "longitude" ; + Mesh2_face_x:long_name = "Characteristics longitude of 2D mesh triangle (e.g. circumcenter coordinate)." ; + double Mesh2_face_y(nMesh2_face) ; + Mesh2_face_y:units = "degrees_north" ; + Mesh2_face_y:standard_name = "latitude" ; + Mesh2_face_y:long_name = "Characteristics latitude of 2D mesh triangle (e.g. circumcenter coordinate)." ; + int Mesh2_face_nodes(nMesh2_face, Mesh2_face_N_nodes) ; + Mesh2_face_nodes:long_name = "Maps every triangular face to its three corner nodes." ; + Mesh2_face_nodes:cf_role = "face_node_connectivity" ; + Mesh2_face_nodes:start_index = 1 ; + int Mesh2_edge_nodes(nMesh2_edge, Mesh2_edge_N_nodes) ; + Mesh2_edge_nodes:long_name = "Maps every edge to the two nodes that it connects." ; + Mesh2_edge_nodes:cf_role = "edge_node_connectivity" ; + Mesh2_edge_nodes:start_index = 1 ; + int Mesh2_face_edges(nMesh2_face, Mesh2_face_N_edges) ; + Mesh2_face_edges:long_name = "Maps every triangular face to its three edges." ; + Mesh2_face_edges:cf_role = "face_edge_connectivity" ; + Mesh2_face_edges:start_index = 1 ; + int Mesh2_face_links(nMesh2_face, Mesh2_face_N_faces) ; + Mesh2_face_links:long_name = "neighbor faces for faces" ; + Mesh2_face_links:comment = "missing neighbor faces are indicated using _FillValue" ; + Mesh2_face_links:cf_role = "face_face_connectivity" ; + Mesh2_face_links:start_index = 1 ; + int Mesh2_edge_face_links(nMesh2_edge, Mesh2_edge_N_faces) ; + Mesh2_edge_face_links:long_name = "neighbor faces for edges" ; + Mesh2_edge_face_links:comment = "missing neighbor faces are indicated using _FillValue" ; + Mesh2_edge_face_links:cf_role = "edge_face_connectivity" ; + Mesh2_edge_face_links:start_index = 1 ; + float datavar(nMesh2_face) ; + datavar:mesh = "Mesh2" ; + datavar:location = "face" ; + +// global attributes: + :Conventions = "CF-1.7" ; +} diff --git a/lib/iris/tests/results/integration/experimental/ugrid_save/TestBasicSave/ugrid_ex3_2d_flexible.cdl b/lib/iris/tests/results/integration/experimental/ugrid_save/TestBasicSave/ugrid_ex3_2d_flexible.cdl new file mode 100644 index 0000000000..355799e0d8 --- /dev/null +++ b/lib/iris/tests/results/integration/experimental/ugrid_save/TestBasicSave/ugrid_ex3_2d_flexible.cdl @@ -0,0 +1,75 @@ +dimensions: + Mesh2_edge_N_faces = 2 ; + Mesh2_edge_N_nodes = 2 ; + Mesh2_face_N_edges = 4 ; + Mesh2_face_N_faces = 4 ; + Mesh2_face_N_nodes = 4 ; + nMesh2_edge = 6 ; + nMesh2_face = 2 ; + nMesh2_node = 5 ; +variables: + int Mesh2 ; + Mesh2:cf_role = "mesh_topology" ; + Mesh2:topology_dimension = 2 ; + Mesh2:long_name = "Topology data of 2D unstructured mesh" ; + Mesh2:node_coordinates = "Mesh2_node_x Mesh2_node_y" ; + Mesh2:edge_coordinates = "Mesh2_edge_x Mesh2_edge_y" ; + Mesh2:face_coordinates = "Mesh2_face_x Mesh2_face_y" ; + Mesh2:face_node_connectivity = "Mesh2_face_nodes" ; + Mesh2:edge_node_connectivity = "Mesh2_edge_nodes" ; + Mesh2:face_edge_connectivity = "Mesh2_face_edges" ; + Mesh2:face_face_connectivity = "Mesh2_face_links" ; + Mesh2:edge_face_connectivity = "Mesh2_edge_face_links" ; + double Mesh2_node_x(nMesh2_node) ; + Mesh2_node_x:units = "degrees_east" ; + Mesh2_node_x:standard_name = "longitude" ; + Mesh2_node_x:long_name = "Longitude of 2D mesh nodes." ; + double Mesh2_node_y(nMesh2_node) ; + Mesh2_node_y:units = "degrees_north" ; + Mesh2_node_y:standard_name = "latitude" ; + Mesh2_node_y:long_name = "Latitude of 2D mesh nodes." ; + double Mesh2_edge_x(nMesh2_edge) ; + Mesh2_edge_x:units = "degrees_east" ; + Mesh2_edge_x:standard_name = "longitude" ; + Mesh2_edge_x:long_name = "Characteristic longitude of 2D mesh edge (e.g. midpoint of the edge)." ; + double Mesh2_edge_y(nMesh2_edge) ; + Mesh2_edge_y:units = "degrees_north" ; + Mesh2_edge_y:standard_name = "latitude" ; + Mesh2_edge_y:long_name = "Characteristic latitude of 2D mesh edge (e.g. midpoint of the edge)." ; + double Mesh2_face_x(nMesh2_face) ; + Mesh2_face_x:units = "degrees_east" ; + Mesh2_face_x:standard_name = "longitude" ; + Mesh2_face_x:long_name = "Characteristics longitude of 2D mesh face." ; + double Mesh2_face_y(nMesh2_face) ; + Mesh2_face_y:units = "degrees_north" ; + Mesh2_face_y:standard_name = "latitude" ; + Mesh2_face_y:long_name = "Characteristics latitude of 2D mesh face." ; + int Mesh2_face_nodes(nMesh2_face, Mesh2_face_N_nodes) ; + Mesh2_face_nodes:long_name = "Maps every face to its corner nodes." ; + Mesh2_face_nodes:cf_role = "face_node_connectivity" ; + Mesh2_face_nodes:start_index = 1 ; + int Mesh2_edge_nodes(nMesh2_edge, Mesh2_edge_N_nodes) ; + Mesh2_edge_nodes:long_name = "Maps every edge to the two nodes that it connects." ; + Mesh2_edge_nodes:cf_role = "edge_node_connectivity" ; + Mesh2_edge_nodes:start_index = 1 ; + int Mesh2_face_edges(nMesh2_face, Mesh2_face_N_edges) ; + Mesh2_face_edges:long_name = "Maps every face to its edges." ; + Mesh2_face_edges:cf_role = "face_edge_connectivity" ; + Mesh2_face_edges:start_index = 1 ; + int Mesh2_face_links(nMesh2_face, Mesh2_face_N_faces) ; + Mesh2_face_links:long_name = "neighbor faces for faces" ; + Mesh2_face_links:comment = "missing edges as well as missing neighbor faces are indicated using _FillValue" ; + Mesh2_face_links:cf_role = "face_face_connectivity" ; + Mesh2_face_links:start_index = 1 ; + int Mesh2_edge_face_links(nMesh2_edge, Mesh2_edge_N_faces) ; + Mesh2_edge_face_links:long_name = "neighbor faces for edges" ; + Mesh2_edge_face_links:comment = "missing neighbor faces are indicated using _FillValue" ; + Mesh2_edge_face_links:cf_role = "edge_face_connectivity" ; + Mesh2_edge_face_links:start_index = 1 ; + float datavar(nMesh2_face) ; + datavar:mesh = "Mesh2" ; + datavar:location = "face" ; + +// global attributes: + :Conventions = "CF-1.7" ; +} diff --git a/lib/iris/tests/results/integration/experimental/ugrid_save/TestBasicSave/ugrid_ex4_3d_layered.cdl b/lib/iris/tests/results/integration/experimental/ugrid_save/TestBasicSave/ugrid_ex4_3d_layered.cdl new file mode 100644 index 0000000000..64962e79aa --- /dev/null +++ b/lib/iris/tests/results/integration/experimental/ugrid_save/TestBasicSave/ugrid_ex4_3d_layered.cdl @@ -0,0 +1,100 @@ +dimensions: + Mesh2_edge_N_faces = 2 ; + Mesh2_edge_N_nodes = 2 ; + Mesh2_face_N_edges = 4 ; + Mesh2_face_N_faces = 4 ; + Mesh2_face_N_nodes = 4 ; + Mesh2_layers = 10 ; + nMesh2_edge = 7 ; + nMesh2_face = 2 ; + nMesh2_node = 6 ; +variables: + int Mesh2 ; + Mesh2:cf_role = "mesh_topology" ; + Mesh2:topology_dimension = 2 ; + Mesh2:long_name = "Topology data of 2D unstructured mesh" ; + Mesh2:node_coordinates = "Mesh2_node_x Mesh2_node_y" ; + Mesh2:edge_coordinates = "Mesh2_edge_x Mesh2_edge_y" ; + Mesh2:face_coordinates = "Mesh2_face_x Mesh2_face_y" ; + Mesh2:face_node_connectivity = "Mesh2_face_nodes" ; + Mesh2:edge_node_connectivity = "Mesh2_edge_nodes" ; + Mesh2:face_edge_connectivity = "Mesh2_face_edges" ; + Mesh2:face_face_connectivity = "Mesh2_face_links" ; + Mesh2:edge_face_connectivity = "Mesh2_edge_face_links" ; + double Mesh2_node_x(nMesh2_node) ; + Mesh2_node_x:units = "degrees_east" ; + Mesh2_node_x:standard_name = "longitude" ; + Mesh2_node_x:long_name = "Longitude of 2D mesh nodes." ; + double Mesh2_node_y(nMesh2_node) ; + Mesh2_node_y:units = "degrees_north" ; + Mesh2_node_y:standard_name = "latitude" ; + Mesh2_node_y:long_name = "Latitude of 2D mesh nodes." ; + double Mesh2_edge_x(nMesh2_edge) ; + Mesh2_edge_x:units = "degrees_east" ; + Mesh2_edge_x:standard_name = "longitude" ; + Mesh2_edge_x:long_name = "Characteristic longitude of 2D mesh edge (e.g. midpoint of the edge)." ; + double Mesh2_edge_y(nMesh2_edge) ; + Mesh2_edge_y:units = "degrees_north" ; + Mesh2_edge_y:standard_name = "latitude" ; + Mesh2_edge_y:long_name = "Characteristic latitude of 2D mesh edge (e.g. midpoint of the edge)." ; + double Mesh2_face_x(nMesh2_face) ; + Mesh2_face_x:units = "degrees_east" ; + Mesh2_face_x:standard_name = "longitude" ; + Mesh2_face_x:long_name = "Characteristics longitude of 2D mesh face." ; + double Mesh2_face_y(nMesh2_face) ; + Mesh2_face_y:units = "degrees_north" ; + Mesh2_face_y:standard_name = "latitude" ; + Mesh2_face_y:long_name = "Characteristics latitude of 2D mesh face." ; + int Mesh2_face_nodes(nMesh2_face, Mesh2_face_N_nodes) ; + Mesh2_face_nodes:long_name = "Maps every face to its corner nodes." ; + Mesh2_face_nodes:cf_role = "face_node_connectivity" ; + Mesh2_face_nodes:start_index = 1 ; + int Mesh2_edge_nodes(nMesh2_edge, Mesh2_edge_N_nodes) ; + Mesh2_edge_nodes:long_name = "Maps every edge to the two nodes that it connects." ; + Mesh2_edge_nodes:cf_role = "edge_node_connectivity" ; + Mesh2_edge_nodes:start_index = 1 ; + int Mesh2_face_edges(nMesh2_face, Mesh2_face_N_edges) ; + Mesh2_face_edges:long_name = "Maps every face to its edges." ; + Mesh2_face_edges:cf_role = "face_edge_connectivity" ; + Mesh2_face_edges:start_index = 1 ; + int Mesh2_face_links(nMesh2_face, Mesh2_face_N_faces) ; + Mesh2_face_links:long_name = "neighbor faces for faces" ; + Mesh2_face_links:comment = "missing edges as well as missing neighbor faces are indicated using _FillValue" ; + Mesh2_face_links:cf_role = "face_face_connectivity" ; + Mesh2_face_links:start_index = 1 ; + int Mesh2_edge_face_links(nMesh2_edge, Mesh2_edge_N_faces) ; + Mesh2_edge_face_links:long_name = "neighbor faces for edges" ; + Mesh2_edge_face_links:comment = "missing neighbor faces are indicated using _FillValue" ; + Mesh2_edge_face_links:cf_role = "edge_face_connectivity" ; + Mesh2_edge_face_links:start_index = 1 ; + float datavar(Mesh2_layers, nMesh2_face) ; + datavar:mesh = "Mesh2" ; + datavar:location = "face" ; + datavar:coordinates = "Mesh2_depth Mesh2_surface" ; + double Mesh2_layers(Mesh2_layers) ; + Mesh2_layers:axis = "Z" ; + Mesh2_layers:units = "1" ; + Mesh2_layers:standard_name = "ocean_sigma_coordinate" ; + Mesh2_layers:long_name = "sigma at layer midpoints" ; + Mesh2_layers:positive = "up" ; + Mesh2_layers:formula_terms = "sigma: Mesh2_layers eta: Mesh2_surface depth: Mesh2_depth" ; + double Mesh2_depth(Mesh2_layers) ; + Mesh2_depth:units = "m" ; + Mesh2_depth:standard_name = "sea_floor_depth_below_geoid" ; + Mesh2_depth:positive = "down" ; + double Mesh2_surface(Mesh2_layers) ; + Mesh2_surface:units = "m" ; + Mesh2_surface:standard_name = "sea_surface_height_above_geoid" ; + double Mesh2_depth_0(Mesh2_layers) ; + Mesh2_depth_0:standard_name = "sea_floor_depth_below_geoid" ; + Mesh2_depth_0:units = "m" ; + Mesh2_depth_0:positive = "down" ; + Mesh2_depth_0:coordinates = "Mesh2_depth Mesh2_surface" ; + double Mesh2_surface_0(Mesh2_layers) ; + Mesh2_surface_0:standard_name = "sea_surface_height_above_geoid" ; + Mesh2_surface_0:units = "m" ; + Mesh2_surface_0:coordinates = "Mesh2_depth Mesh2_surface" ; + +// global attributes: + :Conventions = "CF-1.7" ; +} diff --git a/lib/iris/tests/results/unit/fileformats/netcdf/Saver__ugrid/TestSaveUgrid__cube/basic_mesh.cdl b/lib/iris/tests/results/unit/fileformats/netcdf/Saver__ugrid/TestSaveUgrid__cube/basic_mesh.cdl new file mode 100644 index 0000000000..91516ddae3 --- /dev/null +++ b/lib/iris/tests/results/unit/fileformats/netcdf/Saver__ugrid/TestSaveUgrid__cube/basic_mesh.cdl @@ -0,0 +1,29 @@ +dimensions: + Mesh2d_face_N_nodes = 4 ; + Mesh2d_faces = 2 ; + Mesh2d_nodes = 5 ; +variables: + int Mesh2d ; + Mesh2d:cf_role = "mesh_topology" ; + Mesh2d:topology_dimension = 2 ; + Mesh2d:node_coordinates = "node_x node_y" ; + Mesh2d:face_coordinates = "face_x face_y" ; + Mesh2d:face_node_connectivity = "mesh2d_faces" ; + int64 node_x(Mesh2d_nodes) ; + node_x:standard_name = "longitude" ; + int64 node_y(Mesh2d_nodes) ; + node_y:standard_name = "latitude" ; + int64 face_x(Mesh2d_faces) ; + face_x:standard_name = "longitude" ; + int64 face_y(Mesh2d_faces) ; + face_y:standard_name = "latitude" ; + int mesh2d_faces(Mesh2d_faces, Mesh2d_face_N_nodes) ; + mesh2d_faces:cf_role = "face_node_connectivity" ; + mesh2d_faces:start_index = 0LL ; + float unknown(Mesh2d_faces) ; + unknown:mesh = "Mesh2d" ; + unknown:location = "face" ; + +// global attributes: + :Conventions = "CF-1.7" ; +} diff --git a/lib/iris/tests/stock/netcdf.py b/lib/iris/tests/stock/netcdf.py index 78dc08eafd..30fd0facce 100644 --- a/lib/iris/tests/stock/netcdf.py +++ b/lib/iris/tests/stock/netcdf.py @@ -62,6 +62,8 @@ def _add_standard_data(nc_path, unlimited_dim_size=0): ] # Data addition dependent on this assumption: assert len(unlimited_dim_names) < 2 + if len(unlimited_dim_names) == 0: + unlimited_dim_names = ["*unused*"] # Fill variables data with placeholder numbers. for var in ds.variables.values(): @@ -72,11 +74,13 @@ def _add_standard_data(nc_path, unlimited_dim_size=0): unlimited_dim_size if dim == unlimited_dim_names[0] else size for dim, size in zip(dims, shape) ] - data = np.zeros(shape, dtype=var.dtype) + data = np.ones(shape, dtype=var.dtype) # Do not use zero if len(var.dimensions) == 1 and var.dimensions[0] == var.name: # Fill the var with ascending values (not all zeroes), # so it can be a dim-coord. - data = np.arange(data.size, dtype=data.dtype).reshape(data.shape) + data = np.arange(1, data.size + 1, dtype=data.dtype).reshape( + data.shape + ) var[:] = data ds.close() diff --git a/lib/iris/tests/unit/fileformats/netcdf/test_Saver.py b/lib/iris/tests/unit/fileformats/netcdf/test_Saver.py index 2b0372dfa9..447cdd83eb 100644 --- a/lib/iris/tests/unit/fileformats/netcdf/test_Saver.py +++ b/lib/iris/tests/unit/fileformats/netcdf/test_Saver.py @@ -1031,7 +1031,7 @@ def test_masked_data__insitu(self): with self.temp_filename(".nc") as nc_path: saver = Saver(nc_path, "NETCDF4") with self.assertRaisesRegex(ValueError, self.exp_emsg): - saver._create_cf_cell_measure_variable( + saver._create_generic_cf_array_var( self.cube, self.names_map, self.cm ) diff --git a/lib/iris/tests/unit/fileformats/netcdf/test_Saver__ugrid.py b/lib/iris/tests/unit/fileformats/netcdf/test_Saver__ugrid.py new file mode 100644 index 0000000000..9cd00a2676 --- /dev/null +++ b/lib/iris/tests/unit/fileformats/netcdf/test_Saver__ugrid.py @@ -0,0 +1,698 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +"""Unit tests for the `iris.fileformats.netcdf.Saver` class.""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests # isort:skip + +from pathlib import Path +import shutil +from subprocess import check_output +import tempfile + +import netCDF4 as nc +import numpy as np + +from iris import save +from iris.coords import AuxCoord +from iris.cube import Cube, CubeList +from iris.experimental.ugrid import Connectivity, Mesh +from iris.tests.stock import realistic_4d + +XY_LOCS = ("x", "y") +XY_NAMES = ("longitude", "latitude") + + +def build_mesh( + n_nodes=2, + n_faces=0, + n_edges=0, + nodecoord_xyargs=None, + edgecoord_xyargs=None, + facecoord_xyargs=None, + conn_role_kwargs=None, # mapping {connectivity-role: connectivity-kwargs} + mesh_kwargs=None, +): + """ + Make a test mesh. + + Mesh has faces edges, face-coords and edge-coords, numbers of which can be controlled. + + """ + + def applyargs(coord, kwargs): + if kwargs: + for key, val in kwargs.items(): + # kwargs is a dict + setattr(coord, key, val) + + def apply_xyargs(coords, xyargs): + if xyargs: + for coord, kwargs in zip(coords, xyargs): + # coords and xyargs both iterables : implicitly=(x,y) + applyargs(coord, kwargs) + + # NB when creating coords, supply axis to make Mesh.to_AuxCoords work + node_coords = [ + AuxCoord(np.arange(n_nodes), standard_name=name) + for loc, name in zip(XY_LOCS, XY_NAMES) + ] + apply_xyargs(node_coords, nodecoord_xyargs) + + connectivities = {} + edge_coords = [] + face_coords = [] + topology_dimension = 0 + if n_edges: + topology_dimension = 1 + connectivities["edge_node_connectivity"] = Connectivity( + np.zeros((n_edges, 2), np.int32), cf_role="edge_node_connectivity" + ) + edge_coords = [ + AuxCoord(np.arange(n_edges), standard_name=name) + for loc, name in zip(XY_LOCS, XY_NAMES) + ] + apply_xyargs(edge_coords, edgecoord_xyargs) + + if n_faces: + topology_dimension = 2 + connectivities["face_node_connectivity"] = Connectivity( + np.zeros((n_faces, 4), np.int32), cf_role="face_node_connectivity" + ) + face_coords = [ + AuxCoord(np.arange(n_faces), standard_name=name) + for loc, name in zip(XY_LOCS, XY_NAMES) + ] + apply_xyargs(face_coords, facecoord_xyargs) + + mesh_dims = {"node": n_nodes, "edge": n_edges, "face": n_faces} + + if conn_role_kwargs: + for role, kwargs in conn_role_kwargs.items(): + if role in connectivities: + conn = connectivities[role] + else: + loc_from, loc_to, _ = role.split("_") + dims = [mesh_dims[loc] for loc in (loc_from, loc_to)] + conn = Connectivity( + np.zeros(dims, dtype=np.int32), cf_role=role + ) + connectivities[role] = conn + applyargs(conn, kwargs) + + mesh = Mesh( + topology_dimension=topology_dimension, + node_coords_and_axes=zip(node_coords, XY_LOCS), + edge_coords_and_axes=zip(edge_coords, XY_LOCS), + face_coords_and_axes=zip(face_coords, XY_LOCS), + connectivities=connectivities.values(), + ) + applyargs(mesh, mesh_kwargs) + + return mesh + + +def make_mesh(basic=True, **kwargs): + if basic: + # Use some helpful non-minimal settings as our 'basic' mesh. + use_kwargs = dict( + n_nodes=5, + n_faces=2, + nodecoord_xyargs=tuple( + dict(var_name=f"node_{loc}") for loc in XY_LOCS + ), + facecoord_xyargs=tuple( + dict(var_name=f"face_{loc}") for loc in XY_LOCS + ), + mesh_kwargs=dict( + var_name="Mesh2d", + node_dimension="Mesh2d_nodes", + face_dimension="Mesh2d_faces", + ), + ) + use_kwargs.update(kwargs) + else: + use_kwargs = kwargs + + mesh = build_mesh(**use_kwargs) + return mesh + + +def mesh_location_size(mesh, location): + """Get the size of a location-dimension from a mesh.""" + if location == "node": + # Use a node coordinate (which always exists). + node_coord = mesh.node_coords[0] + result = node_coord.shape[0] + else: + # Use a _node_connectivity, if any. + conn_name = f"{location}_node_connectivity" + conn = getattr(mesh, conn_name, None) + if conn is None: + result = 0 + else: + result = conn.shape[conn.src_dim] + return result + + +# Pre-create a simple "standard" test mesh for multiple uses +_DEFAULT_MESH = make_mesh() + + +def make_cube(mesh=_DEFAULT_MESH, location="face", **kwargs): + dim = mesh_location_size(mesh, location) + cube = Cube(np.zeros(dim, np.float32)) + for meshco in mesh.to_MeshCoords(location): + cube.add_aux_coord(meshco, (0,)) + for key, val in kwargs.items(): + setattr(cube, key, val) + return cube + + +def add_height_dim(cube): + # Add an extra inital 'height' dimension onto a cube. + cube = cube.copy() # Avoid trashing the input cube. + cube.add_aux_coord(AuxCoord([0.0], standard_name="height", units="m")) + # Make three copies with different heights + cubes = [cube.copy() for _ in range(3)] + for i_cube, cube in enumerate(cubes): + cube.coord("height").points = [i_cube] + # Merge to create an additional 'height' dimension. + cube = CubeList(cubes).merge_cube() + return cube + + +def scan_dataset(filepath): + """ + Snapshot a netcdf dataset (the key metadata). + + Returns: + dimsdict, varsdict + * dimsdict (dict): + A map of dimension-name: length. + * varsdict (dict): + A map of each variable's properties, {var_name: propsdict} + Each propsdict is {attribute-name: value} over the var's ncattrs(). + Each propsdict ALSO contains a ['_DIMS'] entry listing the + variable's dims. + + """ + ds = nc.Dataset(filepath) + # dims dict is {name: len} + dimsdict = {name: dim.size for name, dim in ds.dimensions.items()} + # vars dict is {name: {attr:val}} + varsdict = {} + for name, var in ds.variables.items(): + varsdict[name] = {prop: getattr(var, prop) for prop in var.ncattrs()} + varsdict[name]["_DIMS"] = list(var.dimensions) + ds.close() + return dimsdict, varsdict + + +def vars_w_props(varsdict, **kwargs): + """ + Subset a vars dict, {name:props}, returning only those where each + =, defined by the given keywords. + Except that '="*"' means that '' merely _exists_, with any value. + + """ + + def check_attrs_match(attrs): + result = True + for key, val in kwargs.items(): + result = key in attrs + if result: + # val='*'' for a simple existence check + result = (val == "*") or attrs[key] == val + if not result: + break + return result + + varsdict = { + name: attrs + for name, attrs in varsdict.items() + if check_attrs_match(attrs) + } + return varsdict + + +def vars_w_dims(varsdict, dim_names): + """Subset a vars dict, returning all those which map all the specified dims.""" + varsdict = { + name: propsdict + for name, propsdict in varsdict.items() + if all(dim in propsdict["_DIMS"] for dim in dim_names) + } + return varsdict + + +def vars_meshnames(vars): + """Return the names of all the mesh variables (found by cf_role).""" + return list(vars_w_props(vars, cf_role="mesh_topology").keys()) + + +def vars_meshdim(vars, location, mesh_name=None): + """ " + Extract a dim-name for a given element location. + + Args: + * vars (varsdict): + file varsdict, as returned from 'snapshot_dataset'. + * location (string): + a mesh location : 'node' / 'edge' / 'face' + * mesh_name (string or None): + If given, identifies the mesh var. + Otherwise, find a unique mesh var (i.e. there must be exactly 1). + + Returns: + dim_name (string) + The dim-name of the mesh dim for the given location. + + TODO: relies on the element having coordinates, which in future will not + always be the case. This can be fixed + + """ + if mesh_name is None: + # Find "the" meshvar -- assuming there is just one. + (mesh_name,) = vars_meshnames(vars) + mesh_props = vars[mesh_name] + loc_coords = mesh_props[f"{location}_coordinates"].split(" ") + (single_location_dim,) = vars[loc_coords[0]]["_DIMS"] + return single_location_dim + + +class TestSaveUgrid__cube(tests.IrisTest): + @classmethod + def setUpClass(cls): + cls.temp_dir = Path(tempfile.mkdtemp()) + cls.tempfile_path = str(cls.temp_dir / "tmp.nc") + + @classmethod + def tearDownClass(cls): + shutil.rmtree(cls.temp_dir) + + def check_save(self, data): + save(data, self.tempfile_path) + text = check_output(f"ncdump -h {self.tempfile_path}", shell=True) + text = text.decode() + print(text) + return scan_dataset(self.tempfile_path) + + def test_basic_mesh(self): + # Save a small mesh example and check most aspects of the resultin file. + data = make_cube() # A simple face-mapped data example. + + # Save and snapshot the result + dims, vars = self.check_save(data) + + # There is exactly 1 mesh var. + (mesh_name,) = vars_meshnames(vars) + + # There is exactly 1 mesh-linked (data)var + data_vars = vars_w_props(vars, mesh="*") + ((a_name, a_props),) = data_vars.items() + mesh_props = vars[mesh_name] + + # The mesh var links to the mesh, with location 'faces' + self.assertEqual(a_name, "unknown") + self.assertEqual(a_props["mesh"], mesh_name) + self.assertEqual(a_props["location"], "face") + + # There are 2 face coords == those listed in the mesh + face_coords = mesh_props["face_coordinates"].split(" ") + self.assertEqual(len(face_coords), 2) + + # The face coords should both map that single dim. + face_dim = vars_meshdim(vars, "face") + self.assertTrue( + all(vars[co]["_DIMS"] == [face_dim] for co in face_coords) + ) + + # The dims of the datavar also == [] + self.assertEqual(a_props["_DIMS"], [face_dim]) + + # There are 2 node coordinates == those listed in the mesh. + node_coords = mesh_props["node_coordinates"].split(" ") + self.assertEqual(len(node_coords), 2) + # These are the *only* ones using the 'nodes' dimension. + node_dim = vars_meshdim(vars, "node") + self.assertEqual( + sorted(node_coords), sorted(vars_w_dims(vars, [node_dim]).keys()) + ) + + # There are no edges. + self.assertNotIn("edge_node_connectivity", mesh_props) + self.assertEqual( + len(vars_w_props(vars, cf_role="edge_node_connectivity")), 0 + ) + + # The dims are precisely (nodes, faces, nodes-per-face), in that order. + self.assertEqual( + list(dims.keys()), + ["Mesh2d_nodes", "Mesh2d_faces", "Mesh2d_face_N_nodes"], + ) + + # The variables are (mesh, 2*node-coords, 2*face-coords, face-nodes, data), + # in that order + self.assertEqual( + list(vars.keys()), + [ + "Mesh2d", + "node_x", + "node_y", + "face_x", + "face_y", + "mesh2d_faces", + "unknown", + ], + ) + + # For definiteness, also check against a full CDL snapshot + self.assertCDL(self.tempfile_path) + + def test_multi_cubes_common_mesh(self): + cube1 = make_cube(var_name="a") + cube2 = make_cube(var_name="b") + + # Save and snapshot the result + dims, vars = self.check_save([cube1, cube2]) + + # there is exactly 1 mesh in the file + (mesh_name,) = vars_meshnames(vars) + + # both the main variables reference the same mesh, and 'face' location + v_a, v_b = vars["a"], vars["b"] + self.assertEqual(v_a["mesh"], mesh_name) + self.assertEqual(v_a["location"], "face") + self.assertEqual(v_b["mesh"], mesh_name) + self.assertEqual(v_b["location"], "face") + + def test_multi_cubes_different_locations(self): + cube1 = make_cube(var_name="a", location="face") + cube2 = make_cube(var_name="b", location="node") + + # Save and snapshot the result + dims, vars = self.check_save([cube1, cube2]) + + # there is exactly 1 mesh in the file + (mesh_name,) = vars_meshnames(vars) + + # the main variables reference the same mesh at different locations + v_a, v_b = vars["a"], vars["b"] + self.assertEqual(v_a["mesh"], mesh_name) + self.assertEqual(v_a["location"], "face") + self.assertEqual(v_b["mesh"], mesh_name) + self.assertEqual(v_b["location"], "node") + + # the main variables map the face and node dimensions + face_dim = vars_meshdim(vars, "face") + node_dim = vars_meshdim(vars, "node") + self.assertEqual(v_a["_DIMS"], [face_dim]) + self.assertEqual(v_b["_DIMS"], [node_dim]) + + def test_multi_cubes_identical_meshes(self): + # Make 2 identical meshes + # NOTE: *can't* name these explicitly, as it stops them being identical. + mesh1 = make_mesh() + mesh2 = make_mesh() + cube1 = make_cube(var_name="a", mesh=mesh1) + cube2 = make_cube(var_name="b", mesh=mesh2) + + # Save and snapshot the result + dims, vars = self.check_save([cube1, cube2]) + + # there are exactly 2 meshes in the file + mesh_names = vars_meshnames(vars) + self.assertEqual(sorted(mesh_names), ["Mesh2d", "Mesh2d_0"]) + + # they use different dimensions + self.assertEqual( + vars_meshdim(vars, "node", mesh_name="Mesh2d"), "Mesh2d_nodes" + ) + self.assertEqual( + vars_meshdim(vars, "face", mesh_name="Mesh2d"), "Mesh2d_faces" + ) + self.assertEqual( + vars_meshdim(vars, "node", mesh_name="Mesh2d_0"), "Mesh2d_nodes_0" + ) + self.assertEqual( + vars_meshdim(vars, "face", mesh_name="Mesh2d_0"), "Mesh2d_faces_0" + ) + + # there are exactly two data-variables with a 'mesh' property + mesh_datavars = vars_w_props(vars, mesh="*") + self.assertEqual(["a", "b"], list(mesh_datavars)) + + # the data variables reference the two separate meshes + a_props, b_props = vars["a"], vars["b"] + self.assertEqual(a_props["mesh"], "Mesh2d") + self.assertEqual(a_props["location"], "face") + self.assertEqual(b_props["mesh"], "Mesh2d_0") + self.assertEqual(b_props["location"], "face") + + # the data variables map the appropriate node dimensions + self.assertEqual(a_props["_DIMS"], ["Mesh2d_faces"]) + self.assertEqual(b_props["_DIMS"], ["Mesh2d_faces_0"]) + + def test_multi_cubes_different_mesh(self): + # Check that we can correctly distinguish 2 different meshes. + cube1 = make_cube(var_name="a") + cube2 = make_cube(var_name="b", mesh=make_mesh(n_faces=4)) + + # Save and snapshot the result + dims, vars = self.check_save([cube1, cube2]) + + # there are 2 meshes in the file + mesh_names = vars_meshnames(vars) + self.assertEqual(len(mesh_names), 2) + + # there are two (data)variables with a 'mesh' property + mesh_datavars = vars_w_props(vars, mesh="*") + self.assertEqual(2, len(mesh_datavars)) + self.assertEqual(["a", "b"], sorted(mesh_datavars.keys())) + + # the main variables reference the respective meshes, and 'face' location + a_props, b_props = vars["a"], vars["b"] + mesh_a, loc_a = a_props["mesh"], a_props["location"] + mesh_b, loc_b = b_props["mesh"], b_props["location"] + self.assertNotEqual(mesh_a, mesh_b) + self.assertEqual(loc_a, "face") + self.assertEqual(loc_b, "face") + + def test_nonmesh_dim(self): + # Check where the data variable has a 'normal' dim and a mesh dim. + cube = make_cube() + cube = add_height_dim(cube) + + # Save and snapshot the result + dims, vars = self.check_save(cube) + + # have just 1 mesh, including a face and node coordinates. + (mesh_name,) = vars_meshnames(vars) + face_dim = vars_meshdim(vars, "face", mesh_name) + _ = vars_meshdim(vars, "node", mesh_name) + + # have just 1 data-variable + ((data_name, data_props),) = vars_w_props(vars, mesh="*").items() + + # data maps to the height + mesh dims + self.assertEqual(data_props["_DIMS"], ["height", face_dim]) + self.assertEqual(data_props["mesh"], mesh_name) + self.assertEqual(data_props["location"], "face") + + def test_nonmesh_hybrid_dim(self): + # Check a case with a hybrid non-mesh dimension + cube = realistic_4d() + # Strip off the time and longtude dims, to make it simpler. + cube = cube[0, ..., 0] + # Remove all the unwanted coords (also loses the coord-system) + lose_coords = ( + "time", + "forecast_period", + "grid_longitude", + "grid_latitude", + ) + for coord in lose_coords: + cube.remove_coord(coord) + + # Add a mesh on the remaining (now anonymous) horizontal dimension. + i_horizontal_dim = len(cube.shape) - 1 + n_places = cube.shape[i_horizontal_dim] + mesh = make_mesh( + n_faces=n_places, + n_nodes=30, # arbitrary + unrealistic, but doesn't actually matter + ) + # Attach the mesh by adding MeshCoords + for coord in mesh.to_MeshCoords("face"): + cube.add_aux_coord(coord, (i_horizontal_dim,)) + + # Save and snapshot the result + dims, vars = self.check_save(cube) + + # have just 1 mesh, including face and node coordinates. + (mesh_name,) = vars_meshnames(vars) + face_dim = vars_meshdim(vars, "face", mesh_name) + _ = vars_meshdim(vars, "node", mesh_name) + + # have hybrid vertical dimension, with all the usual term variables. + self.assertIn("model_level_number", dims) + vert_vars = list(vars_w_dims(vars, ["model_level_number"]).keys()) + # The list of file variables mapping the vertical dimensio: + # = the data-var, plus all the height terms + self.assertEqual( + vert_vars, + [ + "air_potential_temperature", + "model_level_number", + "level_height", + "level_height_bnds", + "sigma", + "sigma_bnds", + ], + ) + + # have just 1 data-variable, which maps to hybrid-height and mesh dims + ((data_name, data_props),) = vars_w_props(vars, mesh="*").items() + self.assertEqual(data_props["_DIMS"], ["model_level_number", face_dim]) + self.assertEqual(data_props["mesh"], mesh_name) + self.assertEqual(data_props["location"], "face") + + def test_alternate_cube_dim_order(self): + # A cube transposed from the 'usual' order + # Should work much the same as the "basic" case. + cube_1 = make_cube(var_name="a") + cube_1 = add_height_dim(cube_1) + + cube_2 = cube_1.copy() + cube_2.var_name = "b" + cube_2.transpose() + + # Save and snapshot the result + dims, vars = self.check_save([cube_1, cube_2]) + + # There is only 1 mesh + (mesh_name,) = vars_meshnames(vars) + + # both variables reference the same mesh + v_a, v_b = vars["a"], vars["b"] + self.assertEqual(v_a["mesh"], mesh_name) + self.assertEqual(v_a["location"], "face") + self.assertEqual(v_b["mesh"], mesh_name) + self.assertEqual(v_b["location"], "face") + + # Check the var dimensions + self.assertEqual(v_a["_DIMS"], ["height", "Mesh2d_faces"]) + self.assertEqual(v_b["_DIMS"], ["Mesh2d_faces", "height"]) + + def test_alternate_connectivity_dim_order(self): + # A mesh with some connectivities in the 'other' order. + # This should also create a property with the dimension name + mesh = make_mesh(n_edges=7) + # Get the face-node and edge-node connectivities + face_nodes_conn = mesh.face_node_connectivity + edge_nodes_conn = mesh.edge_node_connectivity + # Transpose them : N.B. this sets src_dim=1, as it should be. + nodesfirst_faces_conn = face_nodes_conn.transpose() + nodesfirst_edges_conn = edge_nodes_conn.transpose() + # Make a new mesh with both face and edge connectivities 'transposed'. + mesh2 = Mesh( + topology_dimension=mesh.topology_dimension, + node_coords_and_axes=zip(mesh.node_coords, XY_LOCS), + face_coords_and_axes=zip(mesh.face_coords, XY_LOCS), + connectivities=[nodesfirst_faces_conn, nodesfirst_edges_conn], + ) + + # Build a cube on the modified mesh + cube = make_cube(mesh=mesh2) + + # Save and snapshot the result + dims, vars = self.check_save(cube) + + # Check shape and dimensions of the associated connectivity variables. + (mesh_name,) = vars_meshnames(vars) + mesh_props = vars[mesh_name] + faceconn_name = mesh_props["face_node_connectivity"] + edgeconn_name = mesh_props["edge_node_connectivity"] + faceconn_props = vars[faceconn_name] + edgeconn_props = vars[edgeconn_name] + self.assertEqual( + faceconn_props["_DIMS"], ["Mesh_2d_face_N_nodes", "Mesh2d_face"] + ) + self.assertEqual( + edgeconn_props["_DIMS"], ["Mesh_2d_edge_N_nodes", "Mesh2d_edge"] + ) + + # Check the dimension lengths are also as expected + self.assertEqual(dims["Mesh2d_face"], 2) + self.assertEqual(dims["Mesh_2d_face_N_nodes"], 4) + self.assertEqual(dims["Mesh2d_edge"], 7) + self.assertEqual(dims["Mesh_2d_edge_N_nodes"], 2) + + # the mesh has extra location-dimension properties + self.assertEqual(mesh_props["face_dimension"], "Mesh2d_face") + self.assertEqual(mesh_props["edge_dimension"], "Mesh2d_edge") + + def test_nonuniform_connectivity(self): + # Check handling of connecitivities with missing points. + n_faces = 7 + mesh = make_mesh(n_faces=n_faces) + + # In this case, add on a partial face-face connectivity. + # construct a vaguely plausible face-face index array + indices = np.ma.arange(n_faces * 4).reshape((7, 4)) + indices = indices % 7 + # include some missing points -- i.e. not all faces have 4 neighbours + indices[(2, (2, 3))] = np.ma.masked + indices[(3, (0, 2))] = np.ma.masked + indices[6, :] = np.ma.masked + + conn = Connectivity( + indices, + cf_role="face_face_connectivity", + ) + mesh.add_connectivities(conn) + cube = make_cube(mesh=mesh) + + # Save and snapshot the result + dims, vars = self.check_save(cube) + + # Check that the mesh saved with the additional connectivity + (mesh_name,) = vars_meshnames(vars) + mesh_props = vars[mesh_name] + self.assertIn("face_face_connectivity", mesh_props) + ff_conn_name = mesh_props["face_face_connectivity"] + + # check that the connectivity has the corrects dims and fill-property + ff_props = vars[ff_conn_name] + self.assertEqual( + ff_props["_DIMS"], ["Mesh2d_faces", "Mesh2d_face_N_faces"] + ) + self.assertIn("_FillValue", ff_props) + self.assertEqual(ff_props["_FillValue"], -1) + + # Check that a 'normal' connectivity does *not* have a _FillValue + fn_conn_name = mesh_props["face_node_connectivity"] + fn_props = vars[fn_conn_name] + self.assertNotIn("_FillValue", fn_props) + + # For what it's worth, *also* check the actual data array in the file + ds = nc.Dataset(self.tempfile_path) + try: + conn_var = ds.variables[ff_conn_name] + data = conn_var[:] + finally: + ds.close() + self.assertIsInstance(data, np.ma.MaskedArray) + self.assertEqual(data.fill_value, -1) + # Compare raw values stored, to indices with -1 at missing points + raw_data = data.data + filled_indices = indices.filled(-1) + self.assertArrayEqual(raw_data, filled_indices) + + +if __name__ == "__main__": + tests.main() diff --git a/lib/iris/tests/unit/representation/cube_printout/test_CubePrintout.py b/lib/iris/tests/unit/representation/cube_printout/test_CubePrintout.py index f49c9f9c0c..8370c719f0 100644 --- a/lib/iris/tests/unit/representation/cube_printout/test_CubePrintout.py +++ b/lib/iris/tests/unit/representation/cube_printout/test_CubePrintout.py @@ -18,6 +18,7 @@ DimCoord, ) from iris.cube import Cube +from iris.tests.stock.mesh import sample_mesh_cube class TestCubePrintout___str__(tests.IrisTest): @@ -513,6 +514,23 @@ def test_section_cell_methods(self): ] self.assertEqual(rep, expected) + def test_unstructured_cube(self): + # Check a sample mesh-cube against the expected result. + cube = sample_mesh_cube() + rep = cube_replines(cube) + expected = [ + "mesh_phenom / (unknown) (level: 2; i_mesh_face: 3)", + " Dimension coordinates:", + " level x -", + " i_mesh_face - x", + " Mesh coordinates:", + " latitude - x", + " longitude - x", + " Auxiliary coordinates:", + " mesh_face_aux - x", + ] + self.assertEqual(rep, expected) + if __name__ == "__main__": tests.main() diff --git a/lib/iris/tests/unit/representation/cube_summary/test_CubeSummary.py b/lib/iris/tests/unit/representation/cube_summary/test_CubeSummary.py index 79baf65c8b..c8af3437e6 100644 --- a/lib/iris/tests/unit/representation/cube_summary/test_CubeSummary.py +++ b/lib/iris/tests/unit/representation/cube_summary/test_CubeSummary.py @@ -20,6 +20,7 @@ DimCoord, ) from iris.cube import Cube +from iris.tests.stock.mesh import sample_mesh_cube def example_cube(): @@ -56,6 +57,7 @@ def test_blank_cube(self): expected_vector_sections = [ "Dimension coordinates:", + "Mesh coordinates:", "Auxiliary coordinates:", "Derived coordinates:", "Cell measures:", @@ -211,7 +213,7 @@ def test_scalar_cube(self): self.assertEqual(rep.header.dimension_header.dim_names, []) self.assertEqual(rep.header.dimension_header.shape, []) self.assertEqual(rep.header.dimension_header.contents, ["scalar cube"]) - self.assertEqual(len(rep.vector_sections), 5) + self.assertEqual(len(rep.vector_sections), 6) self.assertTrue( all(sect.is_empty() for sect in rep.vector_sections.values()) ) @@ -303,6 +305,17 @@ def test_attributes_subtle_differences(self): self.assertEqual(co3a_summ.extra, "arr1=array([5, 6])") self.assertEqual(co3b_summ.extra, "arr1=array([[5], [6]])") + def test_unstructured_cube(self): + cube = sample_mesh_cube() + rep = CubeSummary(cube) + # Just check that coordinates appear in the expected sections + dim_section = rep.vector_sections["Dimension coordinates:"] + mesh_section = rep.vector_sections["Mesh coordinates:"] + aux_section = rep.vector_sections["Auxiliary coordinates:"] + self.assertEqual(len(dim_section.contents), 2) + self.assertEqual(len(mesh_section.contents), 2) + self.assertEqual(len(aux_section.contents), 1) + if __name__ == "__main__": tests.main() diff --git a/requirements/ci/nox.lock/py37-linux-64.lock b/requirements/ci/nox.lock/py37-linux-64.lock index 7eb23a9ed9..53ed75a0a2 100644 --- a/requirements/ci/nox.lock/py37-linux-64.lock +++ b/requirements/ci/nox.lock/py37-linux-64.lock @@ -1,6 +1,6 @@ # Generated by conda-lock. # platform: linux-64 -# input_hash: 6dc0000cc1dab34473c629d4c739a5828bcc51d769c258d205d0bbe3817e25ae +# input_hash: 1984641fa445d61b084bb71648b01b32c2709a1807689360923f1b71f9724328 @EXPLICIT https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2021.5.30-ha878542_0.tar.bz2#6a777890e94194dc94a29a76d2a7e721 @@ -9,16 +9,16 @@ https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed3 https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-hab24e00_0.tar.bz2#19410c3df09dfb12d1206132a1d357c5 https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.36.1-hea4e1c9_2.tar.bz2#bd4f2e711b39af170e7ff15163fe87ee -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-11.1.0-h6c583b3_8.tar.bz2#478b6358c5d08b7e133a5da71c5c81bd -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-11.1.0-h56837e0_8.tar.bz2#930957b6bff66cfd539ada080c5ca3e8 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-11.2.0-h5c6108e_8.tar.bz2#1672a7e59c23aac19cb01260e873a4b0 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-11.2.0-he4da1e4_8.tar.bz2#cee237cb6cd08c65c2f74ada9524b768 https://conda.anaconda.org/conda-forge/linux-64/mpi-1.0-mpich.tar.bz2#c1fcff3417b5a22bbc4cf6e8c23648cf https://conda.anaconda.org/conda-forge/linux-64/mysql-common-8.0.25-ha770c72_2.tar.bz2#b1ba065c6d2b9468035472a9d63e5b08 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-11.1.0-h69a702a_8.tar.bz2#7bacab270c077a054525e8afe29feaa9 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-11.1.0-hc902ee8_8.tar.bz2#f2dd961d1ae80d9d81b3d5068807f11b +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-11.2.0-h69a702a_8.tar.bz2#d246e04ac94318cfe8ad8879a41e908e +https://conda.anaconda.org/conda-forge/linux-64/libgomp-11.2.0-h1d223b6_8.tar.bz2#9c5ec5d954d2009c6b267ed25346ef87 https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-1_gnu.tar.bz2#561e277319a41d4f24f5c05a9ef63c04 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-11.1.0-hc902ee8_8.tar.bz2#da6221956ce8582d8e71acc16dfe4c3e +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-11.2.0-h1d223b6_8.tar.bz2#2de68a054a079032d30dcd54ebf2ecb9 https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.3-h516909a_0.tar.bz2#1378b88874f42ac31b2f8e4f6975cb7b https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h7f98852_4.tar.bz2#a1fd65c7ccbf10880423d82bca54eb54 https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.17.2-h7f98852_0.tar.bz2#a25871010e5104556045aa01850fbddf @@ -33,7 +33,7 @@ https://conda.anaconda.org/conda-forge/linux-64/jpeg-9d-h36c2ea0_0.tar.bz2#ea02c https://conda.anaconda.org/conda-forge/linux-64/lerc-2.2.1-h9c3ff4c_0.tar.bz2#ea833dcaeb9e7ac4fac521f1a7abec82 https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.7-h7f98852_5.tar.bz2#10e242842cd30c59c12d79371dc0f583 https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-h516909a_1.tar.bz2#6f8720dff19e17ce5d48cfe7f3d2f0a3 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.3-h58526e2_2.tar.bz2#665369991d8dd290ac5ee92fce3e6bf5 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h9c3ff4c_1.tar.bz2#81c88f272dda26a0ab7db544f1dfbb1e https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.16-h516909a_0.tar.bz2#5c0f338a513a2943c659ae619fca9211 https://conda.anaconda.org/conda-forge/linux-64/libmo_unpack-3.1.2-hf484d3e_1001.tar.bz2#95f32a6a5a666d33886ca5627239f03d https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.4-h7f98852_1.tar.bz2#6e8cc2173440d77708196c5b93771680 @@ -61,7 +61,7 @@ https://conda.anaconda.org/conda-forge/linux-64/xxhash-0.8.0-h7f98852_3.tar.bz2# https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.5-h516909a_1.tar.bz2#33f601066901f3e1a85af3522a8113f9 https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h516909a_0.tar.bz2#03a530e925414902547cf48da7756db8 https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.11-h516909a_1010.tar.bz2#339cc5584e6d26bc73a875ba900028c3 -https://conda.anaconda.org/conda-forge/linux-64/gettext-0.19.8.1-h0b5b191_1005.tar.bz2#ff6f69b593a9e74c0e6b61908ac513fa +https://conda.anaconda.org/conda-forge/linux-64/gettext-0.19.8.1-h73d1719_1006.tar.bz2#58e06d8c5cd5267bba8160dfbaaf9dfe https://conda.anaconda.org/conda-forge/linux-64/hdf4-4.2.15-h10796ff_3.tar.bz2#21a8d66dc17f065023b33145c42652fe https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-11_linux64_openblas.tar.bz2#b8a498e2cac5746b808d5961cb584a13 https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2#4d331e44109e3f0e19b4cb8f9b82f3e1 @@ -83,7 +83,7 @@ https://conda.anaconda.org/conda-forge/linux-64/freetype-2.10.4-h0708190_1.tar.b https://conda.anaconda.org/conda-forge/linux-64/krb5-1.19.2-hcc1bbae_0.tar.bz2#81256fa86f9b65cf8ca726eeb3a7f283 https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-11_linux64_openblas.tar.bz2#59bf439337c9ec59297f701e4ee97e09 https://conda.anaconda.org/conda-forge/linux-64/libclang-11.1.0-default_ha53f305_1.tar.bz2#b9b71585ca4fcb5d442c5a9df5dd7e98 -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.68.4-h3e27bee_0.tar.bz2#23767bef4fd0fb2bda64405df72c9454 +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.68.4-h174f98d_1.tar.bz2#bbdd1d97559e052c98edd08a408218b4 https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-11_linux64_openblas.tar.bz2#00d3680586af1f0689398b080e273cbb https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.3.0-hf544144_1.tar.bz2#a65a4158716bd7d95bfa69bcfd83081c https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.0.3-he3ba5ed_0.tar.bz2#f9dbabc7e01c459ed7a1d1d64b206e9b @@ -93,7 +93,7 @@ https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.7.2-h7f98852_0.tar https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.36.0-h3371d22_4.tar.bz2#661e1ed5d92552785d9f8c781ce68685 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.13.1-hba837de_1005.tar.bz2#fd3611672eb91bc9d24fd6fb970037eb https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.42.6-h04a7f16_0.tar.bz2#b24a1e18325a6e8f8b6b4a2ec5860ce2 -https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.68.4-h9c3ff4c_0.tar.bz2#1e3e305bf1db79b99c3255b557751519 +https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.68.4-h9c3ff4c_1.tar.bz2#023f5d95c2ab4dd78ff5fb62fa421266 https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.18.5-h76c114f_0.tar.bz2#2372f6206f9861a1af8e3b086a053b2b https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h64030ff_2.tar.bz2#112eb9b5b93f0c02e59aea4fd1967363 https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.12-hddcbb42_0.tar.bz2#797117394a4aa588de6d741b06fad80f @@ -101,7 +101,7 @@ https://conda.anaconda.org/conda-forge/linux-64/libcurl-7.78.0-h2574ce0_0.tar.bz https://conda.anaconda.org/conda-forge/linux-64/libpq-13.3-hd57d9b9_0.tar.bz2#66ef2cacc483205b7d303f7b02601c3b https://conda.anaconda.org/conda-forge/linux-64/libwebp-1.2.1-h3452ae3_0.tar.bz2#6d4bf6265d998b6c975c26a6a24062a2 https://conda.anaconda.org/conda-forge/linux-64/nss-3.69-hb5efdd6_0.tar.bz2#da3ad4247c36e2248cfc2aadb1e423b0 -https://conda.anaconda.org/conda-forge/linux-64/python-3.7.10-hffdb5ce_100_cpython.tar.bz2#7425fffa658971915f595e9110163c3c +https://conda.anaconda.org/conda-forge/linux-64/python-3.7.10-hb7a2778_101_cpython.tar.bz2#f5b99583f45e5ab8bcc5ccdd1202f905 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.4-h7f98852_1.tar.bz2#536cc5db4d0a3ba0630541aec064b5e4 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.10-h7f98852_1003.tar.bz2#f59c1242cc1dd93e72c2ee2b360979eb https://conda.anaconda.org/conda-forge/noarch/alabaster-0.7.12-py_0.tar.bz2#2489a97287f90176ecdc3ca982b4b0a0 @@ -115,7 +115,7 @@ https://conda.anaconda.org/conda-forge/linux-64/curl-7.78.0-hea6ffbf_0.tar.bz2#8 https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.2-pyhd8ed1ab_0.tar.bz2#ae8b866c376568b0342ae2c9b68f1e65 https://conda.anaconda.org/conda-forge/noarch/filelock-3.0.12-pyh9f0ad1d_0.tar.bz2#7544ed05bbbe9bb687bc9bcbe4d6cb46 https://conda.anaconda.org/conda-forge/noarch/fsspec-2021.8.1-pyhd8ed1ab_0.tar.bz2#c91815f86a9c2ed7525e9dc78fb189e4 -https://conda.anaconda.org/conda-forge/linux-64/glib-2.68.4-h9c3ff4c_0.tar.bz2#8b8a2eca3496b279cd4f45888490437f +https://conda.anaconda.org/conda-forge/linux-64/glib-2.68.4-h9c3ff4c_1.tar.bz2#32b3393dcde206d3e448c004af06a93b https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.18.5-hf529b03_0.tar.bz2#b1f3d74a3b66432adddfc118ff02238a https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.12.1-mpi_mpich_h9c45103_0.tar.bz2#9a62ae44bbd9d891824816c452491f49 https://conda.anaconda.org/conda-forge/noarch/heapdict-1.0.1-py_0.tar.bz2#77242bfb1e74a627fb06319b5a2d3b95 @@ -153,7 +153,7 @@ https://conda.anaconda.org/conda-forge/noarch/zipp-3.5.0-pyhd8ed1ab_0.tar.bz2#f9 https://conda.anaconda.org/conda-forge/linux-64/antlr-python-runtime-4.7.2-py37h89c1867_1002.tar.bz2#cf3aeeb80dbd517761019a8edcd5b108 https://conda.anaconda.org/conda-forge/noarch/babel-2.9.1-pyh44b312d_0.tar.bz2#74136ed39bfea0832d338df1e58d013e https://conda.anaconda.org/conda-forge/linux-64/certifi-2021.5.30-py37h89c1867_0.tar.bz2#105f18ae8597a5f4d4e3188bcb06c796 -https://conda.anaconda.org/conda-forge/linux-64/cffi-1.14.6-py37hc58025e_0.tar.bz2#22353c1d4290972e3ed35e50af0b9c70 +https://conda.anaconda.org/conda-forge/linux-64/cffi-1.14.6-py37h036bc23_1.tar.bz2#43cbbebef925c942310d814502323c63 https://conda.anaconda.org/conda-forge/linux-64/chardet-4.0.0-py37h89c1867_1.tar.bz2#f4fbd4721b80f0d6b53b3a3374914068 https://conda.anaconda.org/conda-forge/noarch/cycler-0.10.0-py_2.tar.bz2#f6d7c7e6d8f42cbbec7e07a8d879f91c https://conda.anaconda.org/conda-forge/linux-64/cython-0.29.24-py37hcd2ae1e_0.tar.bz2#254a23562f74201cd69ff2df5368c960 @@ -195,8 +195,8 @@ https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.4.3-py37h1058f https://conda.anaconda.org/conda-forge/linux-64/mo_pack-0.2.0-py37h6f94858_1005.tar.bz2#6b203f153bcc537f836ac50c4d9a2e16 https://conda.anaconda.org/conda-forge/linux-64/netcdf-fortran-4.5.3-mpi_mpich_h1364a43_6.tar.bz2#9caa0cf923af3d037897c6d7f8ea57c0 https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.6.0-pyhd8ed1ab_0.tar.bz2#0941325bf48969e2b3b19d0951740950 -https://conda.anaconda.org/conda-forge/linux-64/pandas-1.3.2-py37he8f5f7f_0.tar.bz2#481a4474af8c235b7ad2cd03aa09b2a1 -https://conda.anaconda.org/conda-forge/linux-64/pango-1.48.9-hb8ff022_0.tar.bz2#d76e859d2f213a006191b3c0e9682aa1 +https://conda.anaconda.org/conda-forge/linux-64/pandas-1.3.3-py37he8f5f7f_0.tar.bz2#cc77ff6e46e50194896e8efc65aff01a +https://conda.anaconda.org/conda-forge/linux-64/pango-1.48.10-hb8ff022_0.tar.bz2#5681e615ad3ffde245320b9942b2deab https://conda.anaconda.org/conda-forge/noarch/pip-21.2.4-pyhd8ed1ab_0.tar.bz2#4104ada314dd5639ea36cc12bce0a2cd https://conda.anaconda.org/conda-forge/noarch/pygments-2.10.0-pyhd8ed1ab_0.tar.bz2#32bcce837f1316f1c3208118b6c5e5fc https://conda.anaconda.org/conda-forge/linux-64/python-stratify-0.1.1-py37h6f94858_1004.tar.bz2#42b37830a63405589fef3d13db505e7d @@ -225,12 +225,11 @@ https://conda.anaconda.org/conda-forge/linux-64/graphviz-2.49.0-h85b4f2f_0.tar.b https://conda.anaconda.org/conda-forge/linux-64/pre-commit-2.15.0-py37h89c1867_0.tar.bz2#0ce932f881d9de271f5c962f38840f2f https://conda.anaconda.org/conda-forge/linux-64/pyqtchart-5.12-py37he336c9b_7.tar.bz2#2b1959f3a87b5ad66690340ef921323c https://conda.anaconda.org/conda-forge/linux-64/pyqtwebengine-5.12.1-py37he336c9b_7.tar.bz2#15f5cbcafb4889bb41da2a0a0e338f2a -https://conda.anaconda.org/conda-forge/noarch/pyugrid-0.3.1-py_2.tar.bz2#7d7361886fbcf2be663fd185bf6d244d https://conda.anaconda.org/conda-forge/noarch/urllib3-1.26.6-pyhd8ed1ab_0.tar.bz2#dea5b6d93cfbfbc2a253168ad05b3f89 https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.12.3-py37h89c1867_7.tar.bz2#1754ec587a9ac26e9507fea7eb6bebc2 https://conda.anaconda.org/conda-forge/noarch/requests-2.26.0-pyhd8ed1ab_0.tar.bz2#0ed2ccbde6db9dd5789068eb7194463f https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.4.3-py37h89c1867_0.tar.bz2#c187b5e294bc645d400637518a7aa3ff -https://conda.anaconda.org/conda-forge/noarch/sphinx-4.1.2-pyh6c4a22f_1.tar.bz2#b80d092c478bfc93589f9e338e78b8d2 +https://conda.anaconda.org/conda-forge/noarch/sphinx-4.2.0-pyh6c4a22f_0.tar.bz2#ea5c6881a11a0d9c4d04f1ebae87bb34 https://conda.anaconda.org/conda-forge/noarch/sphinx-copybutton-0.4.0-pyhd8ed1ab_0.tar.bz2#80fd2cc25ad45911b4e42d5b91593e2f https://conda.anaconda.org/conda-forge/noarch/sphinx-gallery-0.9.0-pyhd8ed1ab_0.tar.bz2#5ef222a3e1b5904742e376e05046692b https://conda.anaconda.org/conda-forge/noarch/sphinx-panels-0.6.0-pyhd8ed1ab_0.tar.bz2#6eec6480601f5d15babf9c3b3987f34a diff --git a/requirements/ci/nox.lock/py38-linux-64.lock b/requirements/ci/nox.lock/py38-linux-64.lock index eda0a6f52f..01486c6640 100644 --- a/requirements/ci/nox.lock/py38-linux-64.lock +++ b/requirements/ci/nox.lock/py38-linux-64.lock @@ -1,6 +1,6 @@ # Generated by conda-lock. # platform: linux-64 -# input_hash: b18cda575ddf26d4adbc2e8fa98625982e3a50c991b0082207b9a5bd493af9b1 +# input_hash: 27d0190648a30af19dc196b4f68747f2565c7ba0c8ae5da75814ce68f045cde2 @EXPLICIT https://conda.anaconda.org/conda-forge/linux-64/_libgcc_mutex-0.1-conda_forge.tar.bz2#d7c89558ba9fa0495403155b64376d81 https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2021.5.30-ha878542_0.tar.bz2#6a777890e94194dc94a29a76d2a7e721 @@ -9,16 +9,16 @@ https://conda.anaconda.org/conda-forge/noarch/font-ttf-inconsolata-3.000-h77eed3 https://conda.anaconda.org/conda-forge/noarch/font-ttf-source-code-pro-2.038-h77eed37_0.tar.bz2#4d59c254e01d9cde7957100457e2d5fb https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-hab24e00_0.tar.bz2#19410c3df09dfb12d1206132a1d357c5 https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.36.1-hea4e1c9_2.tar.bz2#bd4f2e711b39af170e7ff15163fe87ee -https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-11.1.0-h6c583b3_8.tar.bz2#478b6358c5d08b7e133a5da71c5c81bd -https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-11.1.0-h56837e0_8.tar.bz2#930957b6bff66cfd539ada080c5ca3e8 +https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-11.2.0-h5c6108e_8.tar.bz2#1672a7e59c23aac19cb01260e873a4b0 +https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-11.2.0-he4da1e4_8.tar.bz2#cee237cb6cd08c65c2f74ada9524b768 https://conda.anaconda.org/conda-forge/linux-64/mpi-1.0-mpich.tar.bz2#c1fcff3417b5a22bbc4cf6e8c23648cf https://conda.anaconda.org/conda-forge/linux-64/mysql-common-8.0.25-ha770c72_2.tar.bz2#b1ba065c6d2b9468035472a9d63e5b08 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-forge-1-0.tar.bz2#f766549260d6815b0c52253f1fb1bb29 -https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-11.1.0-h69a702a_8.tar.bz2#7bacab270c077a054525e8afe29feaa9 -https://conda.anaconda.org/conda-forge/linux-64/libgomp-11.1.0-hc902ee8_8.tar.bz2#f2dd961d1ae80d9d81b3d5068807f11b +https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-11.2.0-h69a702a_8.tar.bz2#d246e04ac94318cfe8ad8879a41e908e +https://conda.anaconda.org/conda-forge/linux-64/libgomp-11.2.0-h1d223b6_8.tar.bz2#9c5ec5d954d2009c6b267ed25346ef87 https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-1_gnu.tar.bz2#561e277319a41d4f24f5c05a9ef63c04 https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2#fee5683a3f04bd15cbd8318b096a27ab -https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-11.1.0-hc902ee8_8.tar.bz2#da6221956ce8582d8e71acc16dfe4c3e +https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-11.2.0-h1d223b6_8.tar.bz2#2de68a054a079032d30dcd54ebf2ecb9 https://conda.anaconda.org/conda-forge/linux-64/alsa-lib-1.2.3-h516909a_0.tar.bz2#1378b88874f42ac31b2f8e4f6975cb7b https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h7f98852_4.tar.bz2#a1fd65c7ccbf10880423d82bca54eb54 https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.17.2-h7f98852_0.tar.bz2#a25871010e5104556045aa01850fbddf @@ -33,7 +33,7 @@ https://conda.anaconda.org/conda-forge/linux-64/jpeg-9d-h36c2ea0_0.tar.bz2#ea02c https://conda.anaconda.org/conda-forge/linux-64/lerc-2.2.1-h9c3ff4c_0.tar.bz2#ea833dcaeb9e7ac4fac521f1a7abec82 https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.7-h7f98852_5.tar.bz2#10e242842cd30c59c12d79371dc0f583 https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-h516909a_1.tar.bz2#6f8720dff19e17ce5d48cfe7f3d2f0a3 -https://conda.anaconda.org/conda-forge/linux-64/libffi-3.3-h58526e2_2.tar.bz2#665369991d8dd290ac5ee92fce3e6bf5 +https://conda.anaconda.org/conda-forge/linux-64/libffi-3.4.2-h9c3ff4c_1.tar.bz2#81c88f272dda26a0ab7db544f1dfbb1e https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.16-h516909a_0.tar.bz2#5c0f338a513a2943c659ae619fca9211 https://conda.anaconda.org/conda-forge/linux-64/libmo_unpack-3.1.2-hf484d3e_1001.tar.bz2#95f32a6a5a666d33886ca5627239f03d https://conda.anaconda.org/conda-forge/linux-64/libogg-1.3.4-h7f98852_1.tar.bz2#6e8cc2173440d77708196c5b93771680 @@ -61,7 +61,7 @@ https://conda.anaconda.org/conda-forge/linux-64/xxhash-0.8.0-h7f98852_3.tar.bz2# https://conda.anaconda.org/conda-forge/linux-64/xz-5.2.5-h516909a_1.tar.bz2#33f601066901f3e1a85af3522a8113f9 https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h516909a_0.tar.bz2#03a530e925414902547cf48da7756db8 https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.11-h516909a_1010.tar.bz2#339cc5584e6d26bc73a875ba900028c3 -https://conda.anaconda.org/conda-forge/linux-64/gettext-0.19.8.1-h0b5b191_1005.tar.bz2#ff6f69b593a9e74c0e6b61908ac513fa +https://conda.anaconda.org/conda-forge/linux-64/gettext-0.19.8.1-h73d1719_1006.tar.bz2#58e06d8c5cd5267bba8160dfbaaf9dfe https://conda.anaconda.org/conda-forge/linux-64/hdf4-4.2.15-h10796ff_3.tar.bz2#21a8d66dc17f065023b33145c42652fe https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-11_linux64_openblas.tar.bz2#b8a498e2cac5746b808d5961cb584a13 https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2#4d331e44109e3f0e19b4cb8f9b82f3e1 @@ -83,7 +83,7 @@ https://conda.anaconda.org/conda-forge/linux-64/freetype-2.10.4-h0708190_1.tar.b https://conda.anaconda.org/conda-forge/linux-64/krb5-1.19.2-hcc1bbae_0.tar.bz2#81256fa86f9b65cf8ca726eeb3a7f283 https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-11_linux64_openblas.tar.bz2#59bf439337c9ec59297f701e4ee97e09 https://conda.anaconda.org/conda-forge/linux-64/libclang-11.1.0-default_ha53f305_1.tar.bz2#b9b71585ca4fcb5d442c5a9df5dd7e98 -https://conda.anaconda.org/conda-forge/linux-64/libglib-2.68.4-h3e27bee_0.tar.bz2#23767bef4fd0fb2bda64405df72c9454 +https://conda.anaconda.org/conda-forge/linux-64/libglib-2.68.4-h174f98d_1.tar.bz2#bbdd1d97559e052c98edd08a408218b4 https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-11_linux64_openblas.tar.bz2#00d3680586af1f0689398b080e273cbb https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.3.0-hf544144_1.tar.bz2#a65a4158716bd7d95bfa69bcfd83081c https://conda.anaconda.org/conda-forge/linux-64/libxkbcommon-1.0.3-he3ba5ed_0.tar.bz2#f9dbabc7e01c459ed7a1d1d64b206e9b @@ -93,14 +93,14 @@ https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.7.2-h7f98852_0.tar https://conda.anaconda.org/conda-forge/linux-64/atk-1.0-2.36.0-h3371d22_4.tar.bz2#661e1ed5d92552785d9f8c781ce68685 https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.13.1-hba837de_1005.tar.bz2#fd3611672eb91bc9d24fd6fb970037eb https://conda.anaconda.org/conda-forge/linux-64/gdk-pixbuf-2.42.6-h04a7f16_0.tar.bz2#b24a1e18325a6e8f8b6b4a2ec5860ce2 -https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.68.4-h9c3ff4c_0.tar.bz2#1e3e305bf1db79b99c3255b557751519 +https://conda.anaconda.org/conda-forge/linux-64/glib-tools-2.68.4-h9c3ff4c_1.tar.bz2#023f5d95c2ab4dd78ff5fb62fa421266 https://conda.anaconda.org/conda-forge/linux-64/gstreamer-1.18.5-h76c114f_0.tar.bz2#2372f6206f9861a1af8e3b086a053b2b https://conda.anaconda.org/conda-forge/linux-64/gts-0.7.6-h64030ff_2.tar.bz2#112eb9b5b93f0c02e59aea4fd1967363 https://conda.anaconda.org/conda-forge/linux-64/libcurl-7.78.0-h2574ce0_0.tar.bz2#9c06cc5692dcd0b91699413fcc18405b https://conda.anaconda.org/conda-forge/linux-64/libpq-13.3-hd57d9b9_0.tar.bz2#66ef2cacc483205b7d303f7b02601c3b https://conda.anaconda.org/conda-forge/linux-64/libwebp-1.2.1-h3452ae3_0.tar.bz2#6d4bf6265d998b6c975c26a6a24062a2 https://conda.anaconda.org/conda-forge/linux-64/nss-3.69-hb5efdd6_0.tar.bz2#da3ad4247c36e2248cfc2aadb1e423b0 -https://conda.anaconda.org/conda-forge/linux-64/python-3.8.10-h49503c6_1_cpython.tar.bz2#69f7d6ef1f00c3a109b1b06279e6d6a9 +https://conda.anaconda.org/conda-forge/linux-64/python-3.8.10-hb7a2778_2_cpython.tar.bz2#3d03b31e20d36494a1ef48f19c51c60c https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.4-h7f98852_1.tar.bz2#536cc5db4d0a3ba0630541aec064b5e4 https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.10-h7f98852_1003.tar.bz2#f59c1242cc1dd93e72c2ee2b360979eb https://conda.anaconda.org/conda-forge/noarch/alabaster-0.7.12-py_0.tar.bz2#2489a97287f90176ecdc3ca982b4b0a0 @@ -114,7 +114,7 @@ https://conda.anaconda.org/conda-forge/linux-64/curl-7.78.0-hea6ffbf_0.tar.bz2#8 https://conda.anaconda.org/conda-forge/noarch/distlib-0.3.2-pyhd8ed1ab_0.tar.bz2#ae8b866c376568b0342ae2c9b68f1e65 https://conda.anaconda.org/conda-forge/noarch/filelock-3.0.12-pyh9f0ad1d_0.tar.bz2#7544ed05bbbe9bb687bc9bcbe4d6cb46 https://conda.anaconda.org/conda-forge/noarch/fsspec-2021.8.1-pyhd8ed1ab_0.tar.bz2#c91815f86a9c2ed7525e9dc78fb189e4 -https://conda.anaconda.org/conda-forge/linux-64/glib-2.68.4-h9c3ff4c_0.tar.bz2#8b8a2eca3496b279cd4f45888490437f +https://conda.anaconda.org/conda-forge/linux-64/glib-2.68.4-h9c3ff4c_1.tar.bz2#32b3393dcde206d3e448c004af06a93b https://conda.anaconda.org/conda-forge/linux-64/gst-plugins-base-1.18.5-hf529b03_0.tar.bz2#b1f3d74a3b66432adddfc118ff02238a https://conda.anaconda.org/conda-forge/linux-64/hdf5-1.12.1-mpi_mpich_h9c45103_0.tar.bz2#9a62ae44bbd9d891824816c452491f49 https://conda.anaconda.org/conda-forge/noarch/heapdict-1.0.1-py_0.tar.bz2#77242bfb1e74a627fb06319b5a2d3b95 @@ -151,7 +151,7 @@ https://conda.anaconda.org/conda-forge/noarch/zipp-3.5.0-pyhd8ed1ab_0.tar.bz2#f9 https://conda.anaconda.org/conda-forge/linux-64/antlr-python-runtime-4.7.2-py38h578d9bd_1002.tar.bz2#2b2207e2c8a05fc0bc5b62fc32c355e6 https://conda.anaconda.org/conda-forge/noarch/babel-2.9.1-pyh44b312d_0.tar.bz2#74136ed39bfea0832d338df1e58d013e https://conda.anaconda.org/conda-forge/linux-64/certifi-2021.5.30-py38h578d9bd_0.tar.bz2#a2e14464711f8e76010cd7e0c49bc4ae -https://conda.anaconda.org/conda-forge/linux-64/cffi-1.14.6-py38ha65f79e_0.tar.bz2#6b52289a4d2cfb2107fb54efd7dd1aa1 +https://conda.anaconda.org/conda-forge/linux-64/cffi-1.14.6-py38h3931269_1.tar.bz2#01a5442b5cc3c7e901823fd70fb1aeb5 https://conda.anaconda.org/conda-forge/linux-64/chardet-4.0.0-py38h578d9bd_1.tar.bz2#9294a5e2c7545a2f67ac348aadd53344 https://conda.anaconda.org/conda-forge/linux-64/click-8.0.1-py38h578d9bd_0.tar.bz2#45426acde32f0ddd94dcee3478fd13e3 https://conda.anaconda.org/conda-forge/noarch/cycler-0.10.0-py_2.tar.bz2#f6d7c7e6d8f42cbbec7e07a8d879f91c @@ -192,8 +192,8 @@ https://conda.anaconda.org/conda-forge/linux-64/matplotlib-base-3.4.3-py38hf4fb8 https://conda.anaconda.org/conda-forge/linux-64/mo_pack-0.2.0-py38hb5d20a5_1005.tar.bz2#d51285f235753f5620474c60da0e5d68 https://conda.anaconda.org/conda-forge/linux-64/netcdf-fortran-4.5.3-mpi_mpich_h1364a43_6.tar.bz2#9caa0cf923af3d037897c6d7f8ea57c0 https://conda.anaconda.org/conda-forge/noarch/nodeenv-1.6.0-pyhd8ed1ab_0.tar.bz2#0941325bf48969e2b3b19d0951740950 -https://conda.anaconda.org/conda-forge/linux-64/pandas-1.3.2-py38h43a58ef_0.tar.bz2#88f62890e4594df2e7da213dde8d823c -https://conda.anaconda.org/conda-forge/linux-64/pango-1.48.9-hb8ff022_0.tar.bz2#d76e859d2f213a006191b3c0e9682aa1 +https://conda.anaconda.org/conda-forge/linux-64/pandas-1.3.3-py38h43a58ef_0.tar.bz2#0b22b7a9e57e4ce2184fafce3d20a15f +https://conda.anaconda.org/conda-forge/linux-64/pango-1.48.10-hb8ff022_0.tar.bz2#5681e615ad3ffde245320b9942b2deab https://conda.anaconda.org/conda-forge/noarch/pip-21.2.4-pyhd8ed1ab_0.tar.bz2#4104ada314dd5639ea36cc12bce0a2cd https://conda.anaconda.org/conda-forge/noarch/pygments-2.10.0-pyhd8ed1ab_0.tar.bz2#32bcce837f1316f1c3208118b6c5e5fc https://conda.anaconda.org/conda-forge/linux-64/python-stratify-0.2.post0-py38hb5d20a5_0.tar.bz2#cc6852249c01884469560082943b689f @@ -221,12 +221,11 @@ https://conda.anaconda.org/conda-forge/linux-64/graphviz-2.49.0-h85b4f2f_0.tar.b https://conda.anaconda.org/conda-forge/linux-64/pre-commit-2.15.0-py38h578d9bd_0.tar.bz2#100bf2896c8e61ce39e323ba8f6360fd https://conda.anaconda.org/conda-forge/linux-64/pyqtchart-5.12-py38h7400c14_7.tar.bz2#3003444b4f41742a33b7afdeb3260cbc https://conda.anaconda.org/conda-forge/linux-64/pyqtwebengine-5.12.1-py38h7400c14_7.tar.bz2#1c17944e118b314ff4d0bfc05f03a5e1 -https://conda.anaconda.org/conda-forge/noarch/pyugrid-0.3.1-py_2.tar.bz2#7d7361886fbcf2be663fd185bf6d244d https://conda.anaconda.org/conda-forge/noarch/urllib3-1.26.6-pyhd8ed1ab_0.tar.bz2#dea5b6d93cfbfbc2a253168ad05b3f89 https://conda.anaconda.org/conda-forge/linux-64/pyqt-5.12.3-py38h578d9bd_7.tar.bz2#7166890c160d0441f59973a40b74f6e5 https://conda.anaconda.org/conda-forge/noarch/requests-2.26.0-pyhd8ed1ab_0.tar.bz2#0ed2ccbde6db9dd5789068eb7194463f https://conda.anaconda.org/conda-forge/linux-64/matplotlib-3.4.3-py38h578d9bd_0.tar.bz2#a88165a7cbabf536cddd46db3b62e38c -https://conda.anaconda.org/conda-forge/noarch/sphinx-4.1.2-pyh6c4a22f_1.tar.bz2#b80d092c478bfc93589f9e338e78b8d2 +https://conda.anaconda.org/conda-forge/noarch/sphinx-4.2.0-pyh6c4a22f_0.tar.bz2#ea5c6881a11a0d9c4d04f1ebae87bb34 https://conda.anaconda.org/conda-forge/noarch/sphinx-copybutton-0.4.0-pyhd8ed1ab_0.tar.bz2#80fd2cc25ad45911b4e42d5b91593e2f https://conda.anaconda.org/conda-forge/noarch/sphinx-gallery-0.9.0-pyhd8ed1ab_0.tar.bz2#5ef222a3e1b5904742e376e05046692b https://conda.anaconda.org/conda-forge/noarch/sphinx-panels-0.6.0-pyhd8ed1ab_0.tar.bz2#6eec6480601f5d15babf9c3b3987f34a