Skip to content

Commit

Permalink
Merge branch 'main' into ugrid_load_default
Browse files Browse the repository at this point in the history
  • Loading branch information
pp-mo authored Jul 19, 2024
2 parents 554eba1 + c06bf55 commit a5bd1af
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 42 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci-manifest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,4 @@ concurrency:
jobs:
manifest:
name: "check-manifest"
uses: scitools/workflows/.github/workflows/ci-manifest.yml@2024.07.1
uses: scitools/workflows/.github/workflows/ci-manifest.yml@2024.07.3
2 changes: 1 addition & 1 deletion .github/workflows/refresh-lockfiles.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ on:

jobs:
refresh_lockfiles:
uses: scitools/workflows/.github/workflows/refresh-lockfiles.yml@2024.07.1
uses: scitools/workflows/.github/workflows/refresh-lockfiles.yml@2024.07.3
secrets: inherit
10 changes: 10 additions & 0 deletions docs/src/whatsnew/latest.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ This document explains the changes made to Iris for this release
the :class:`~iris.cube.Cube` :attr:`~iris.cube.Cube.mesh_dim` (see
:ref:`cube-statistics-collapsing`). (:issue:`5377`, :pull:`6003`)

#. `@pp-mo`_ made a MeshCoord inherit a coordinate system from its location coord,
as it does its metadata. N.B. mesh location coords can not however load a
coordinate system from netcdf at present, as this needs the 'extended'
grid-mappping syntax -- see : :issue:`3388`.
(:issue:`5562`, :pull:`6016`)


🐛 Bugs Fixed
=============
Expand All @@ -53,6 +59,10 @@ This document explains the changes made to Iris for this release
#. `@pp-mo`_ corrected the use of mesh dimensions when saving with multiple
meshes. (:issue:`5908`, :pull:`6004`)

#. `@trexfeathers`_ fixed the datum :class:`python:FutureWarning` to only be raised if
the ``datum_support`` :class:`~iris.Future` flag is disabled AND a datum is
present on the loaded NetCDF grid mapping. (:issue:`5749`, :pull:`6050`)


💣 Incompatible Changes
=======================
Expand Down
29 changes: 27 additions & 2 deletions lib/iris/experimental/ugrid/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -2782,6 +2782,11 @@ def fix_repr(val):
)
raise ValueError(msg)

# Don't use 'coord_system' as a constructor arg, since for
# MeshCoords it is deduced from the mesh.
# (Otherwise a non-None coord_system breaks the 'copy' operation)
use_metadict.pop("coord_system")

# Call parent constructor to handle the common constructor args.
super().__init__(points, bounds=bounds, **use_metadict)

Expand All @@ -2807,8 +2812,28 @@ def axis(self):

@property
def coord_system(self):
"""The coordinate-system of a MeshCoord is always 'None'."""
return None
"""The coordinate-system of a MeshCoord.
It comes from the `related` location coordinate in the mesh.
"""
# This matches where the coord metadata is drawn from.
# See : https://github.com/SciTools/iris/issues/4860
select_kwargs = {
f"include_{self.location}s": True,
"axis": self.axis,
}
try:
# NOTE: at present, a MeshCoord *always* references the relevant location
# coordinate in the mesh, from which its points are taken.
# However this might change in future ..
# see : https://github.com/SciTools/iris/discussions/4438#bounds-no-points
location_coord = self.mesh.coord(**select_kwargs)
coord_system = location_coord.coord_system
except CoordinateNotFoundError:
# No such coord : possible in UGRID, but probably not Iris (at present).
coord_system = None

return coord_system

@coord_system.setter
def coord_system(self, value):
Expand Down
2 changes: 1 addition & 1 deletion lib/iris/fileformats/_nc_load_rules/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ def _get_ellipsoid(cf_grid_var):
if datum == "unknown":
datum = None

if not iris.FUTURE.datum_support:
if datum is not None and not iris.FUTURE.datum_support:
wmsg = (
"Ignoring a datum in netCDF load for consistency with existing "
"behaviour. In a future version of Iris, this datum will be "
Expand Down
129 changes: 129 additions & 0 deletions lib/iris/tests/integration/experimental/test_meshcoord_coordsys.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Copyright Iris contributors
#
# This file is part of Iris and is released under the BSD license.
# See LICENSE in the root of the repository for full licensing details.
"""Integration tests for MeshCoord.coord_system() behaviour."""

import pytest

import iris
from iris.coord_systems import GeogCS
from iris.experimental.ugrid.load import PARSE_UGRID_ON_LOAD
from iris.tests.stock.netcdf import ncgen_from_cdl

TEST_CDL = """
netcdf mesh_test {
dimensions:
node = 3 ;
face = 1 ;
vertex = 3 ;
variables:
int mesh ;
mesh:cf_role = "mesh_topology" ;
mesh:topology_dimension = 2 ;
mesh:node_coordinates = "node_x node_y" ;
mesh:face_node_connectivity = "face_nodes" ;
float node_x(node) ;
node_x:standard_name = "longitude" ;
float node_y(node) ;
node_y:standard_name = "latitude" ;
int face_nodes(face, vertex) ;
face_nodes:cf_role = "face_node_connectivity" ;
face_nodes:start_index = 0 ;
float node_data(node) ;
node_data:coordinates = "node_x node_y" ;
node_data:location = "node" ;
node_data:mesh = "mesh" ;
}
"""

BASIC_CRS_STR = """
int crs ;
crs:grid_mapping_name = "latitude_longitude" ;
crs:semi_major_axis = 6371000.0 ;
"""


def find_i_line(lines, match):
(i_line,) = [i for i, line in enumerate(lines) if match in line]
return i_line


def make_file(nc_path, node_x_crs=False, node_y_crs=False):
lines = TEST_CDL.split("\n")

includes = ["node_x"] if node_x_crs else []
includes += ["node_y"] if node_y_crs else []
includes = " ".join(includes)
if includes:
# insert a grid-mapping
i_line = find_i_line(lines, "variables:")
lines[i_line + 1 : i_line + 1] = BASIC_CRS_STR.split("\n")

i_line = find_i_line(lines, "float node_data(")
ref_lines = [
# NOTE: space to match the surrounding indent
f' node_data:grid_mapping = "crs: {includes}" ;',
# NOTE: this is already present
# f'node_data:coordinates = "{includes}" ;'
]
lines[i_line + 1 : i_line + 1] = ref_lines

cdl_str = "\n".join(lines)
ncgen_from_cdl(cdl_str=cdl_str, cdl_path=None, nc_path=nc_path)


@pytest.mark.parametrize("cs_axes", ["--", "x-", "-y", "xy"])
def test_default_mesh_cs(tmp_path, cs_axes):
"""Test coord-systems of mesh cube and coords, if location coords have a crs."""
nc_path = tmp_path / "test_temp.nc"
do_x = "x" in cs_axes
do_y = "y" in cs_axes
make_file(nc_path, node_x_crs=do_x, node_y_crs=do_y)
with PARSE_UGRID_ON_LOAD.context():
cube = iris.load_cube(nc_path, "node_data")
meshco_x, meshco_y = [cube.coord(mesh_coords=True, axis=ax) for ax in ("x", "y")]
# NOTE: at present, none of these load with a coordinate system,
# because we don't support the extended grid-mapping syntax.
# see: https://github.com/SciTools/iris/issues/3388
assert meshco_x.coord_system is None
assert meshco_y.coord_system is None


def test_assigned_mesh_cs(tmp_path):
# Check that when a coord system is manually assigned to a location coord,
# the corresponding meshcoord reports the same cs.
nc_path = tmp_path / "test_temp.nc"
make_file(nc_path)
with PARSE_UGRID_ON_LOAD.context():
cube = iris.load_cube(nc_path, "node_data")
nodeco_x = cube.mesh.coord(include_nodes=True, axis="x")
meshco_x, meshco_y = [cube.coord(axis=ax) for ax in ("x", "y")]
assert nodeco_x.coord_system is None
assert meshco_x.coord_system is None
assert meshco_y.coord_system is None
assigned_cs = GeogCS(1.0)
nodeco_x.coord_system = assigned_cs
assert meshco_x.coord_system is assigned_cs
assert meshco_y.coord_system is None
# This also affects cube.coord_system(), even though it is an auxcoord,
# since there are no dim-coords, or any other coord with a c-s.
# TODO: this may be a mistake -- see https://github.com/SciTools/iris/issues/6051
assert cube.coord_system() is assigned_cs


def test_meshcoord_coordsys_copy(tmp_path):
# Check that copying a meshcoord with a coord system works properly.
nc_path = tmp_path / "test_temp.nc"
make_file(nc_path)
with PARSE_UGRID_ON_LOAD.context():
cube = iris.load_cube(nc_path, "node_data")
node_coord = cube.mesh.coord(include_nodes=True, axis="x")
assigned_cs = GeogCS(1.0)
node_coord.coord_system = assigned_cs
mesh_coord = cube.coord(axis="x")
assert mesh_coord.coord_system is assigned_cs
meshco_copy = mesh_coord.copy()
assert meshco_copy == mesh_coord
# Note: still the same object, because it is derived from the same node_coord
assert meshco_copy.coord_system is assigned_cs
33 changes: 23 additions & 10 deletions lib/iris/tests/integration/netcdf/test_coord_systems.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
from os.path import join as path_join
import shutil
import tempfile
import warnings

import pytest

import iris
from iris.coords import DimCoord
Expand Down Expand Up @@ -135,15 +138,25 @@ def test_load_datum_wkt(self):
cube = iris.load_cube(nc_path)
test_crs = cube.coord("projection_y_coordinate").coord_system
actual = str(test_crs.as_cartopy_crs().datum)
self.assertMultiLineEqual(expected, actual)
assert actual == expected

def test_no_load_datum_wkt(self):
nc_path = tlc.cdl_to_nc(self.datum_wkt_cdl)
with self.assertWarnsRegex(FutureWarning, "iris.FUTURE.datum_support"):
with pytest.warns(FutureWarning, match="iris.FUTURE.datum_support"):
cube = iris.load_cube(nc_path)
test_crs = cube.coord("projection_y_coordinate").coord_system
actual = str(test_crs.as_cartopy_crs().datum)
self.assertMultiLineEqual(actual, "unknown")
assert actual == "unknown"

def test_no_datum_no_warn(self):
new_cdl = self.datum_wkt_cdl.splitlines()
new_cdl = [line for line in new_cdl if "DATUM" not in line]
new_cdl = "\n".join(new_cdl)
nc_path = tlc.cdl_to_nc(new_cdl)
with warnings.catch_warnings():
# pytest's recommended way to assert for no warnings.
warnings.simplefilter("error", FutureWarning)
_ = iris.load_cube(nc_path)

def test_load_datum_cf_var(self):
expected = "OSGB 1936"
Expand All @@ -152,15 +165,15 @@ def test_load_datum_cf_var(self):
cube = iris.load_cube(nc_path)
test_crs = cube.coord("projection_y_coordinate").coord_system
actual = str(test_crs.as_cartopy_crs().datum)
self.assertMultiLineEqual(expected, actual)
assert actual == expected

def test_no_load_datum_cf_var(self):
nc_path = tlc.cdl_to_nc(self.datum_cf_var_cdl)
with self.assertWarnsRegex(FutureWarning, "iris.FUTURE.datum_support"):
with pytest.warns(FutureWarning, match="iris.FUTURE.datum_support"):
cube = iris.load_cube(nc_path)
test_crs = cube.coord("projection_y_coordinate").coord_system
actual = str(test_crs.as_cartopy_crs().datum)
self.assertMultiLineEqual(actual, "unknown")
assert actual == "unknown"

def test_save_datum(self):
expected = "OSGB 1936"
Expand Down Expand Up @@ -199,7 +212,7 @@ def test_save_datum(self):

test_crs = cube.coord("projection_y_coordinate").coord_system
actual = str(test_crs.as_cartopy_crs().datum)
self.assertMultiLineEqual(expected, actual)
assert actual == expected


class TestLoadMinimalGeostationary(tests.IrisTest):
Expand Down Expand Up @@ -270,9 +283,9 @@ def test_geostationary_no_false_offsets(self):
cube = iris.load_cube(self.path_test_nc)
# Check the coordinate system properties has the correct default properties.
cs = cube.coord_system()
self.assertIsInstance(cs, iris.coord_systems.Geostationary)
self.assertEqual(cs.false_easting, 0.0)
self.assertEqual(cs.false_northing, 0.0)
assert isinstance(cs, iris.coord_systems.Geostationary)
assert cs.false_easting == 0.0
assert cs.false_northing == 0.0


if __name__ == "__main__":
Expand Down
55 changes: 28 additions & 27 deletions lib/iris/tests/integration/netcdf/test_general.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,37 +488,38 @@ def test_path_string_save_same(self):

@tests.skip_data
class TestWarningRepeats(tests.IrisTest):
def test_datum_once(self):
"""Tests for warnings being duplicated.
def test_warning_repeats(self):
"""Confirm Iris load does not break Python duplicate warning handling."""
# units.nc is designed for testing Iris' 'ignoring invalid units'
# warning; it contains two variables with invalid units, producing two
# unique warnings (due to two different messages).
file_path = tests.get_data_path(("NetCDF", "testing", "units.nc"))

Notes
-----
This test relies on `iris.load` throwing a warning. This warning might
be removed in the future, in which case `assert len(record) == 2 should`
be change to `assert len(record) == 1`.
toa_brightness_temperature.nc has an AuxCoord with lazy data, and triggers a
specific part of dask which contains a `catch_warnings()` call which
causes warnings to be repeated, and so has been removed from the
`fnames` list until a solution is found for such a file.
"""
#
fnames = [
"false_east_north_merc.nc",
"non_unit_scale_factor_merc.nc",
# toa_brightness_temperature.nc,
]
fpaths = [
tests.get_data_path(("NetCDF", "mercator", fname)) for fname in fnames
]
def _raise_warning() -> None:
# Contain in function so warning always has identical line number.
warnings.warn("Dummy warning", category=iris.warnings.IrisUserWarning)

with warnings.catch_warnings(record=True) as record:
warnings.simplefilter("default")
for fpath in fpaths:
iris.load(fpath)
warnings.warn("Dummy warning", category=iris.warnings.IrisUserWarning)
assert len(record) == 2

# Warn before Iris has been invoked.
_raise_warning()
assert len(record) == 1

# This Iris call should raise 2 warnings and should NOT affect
# Python's duplicate warning handling.
_ = iris.load(file_path)
assert len(record) == 3
# Raise a duplicate warning.
_raise_warning()
assert len(record) == 3

# Repeated identical calls should only raise duplicate warnings
# and therefore not affect the record.
for i in range(2):
_ = iris.load(file_path)
_raise_warning()
assert len(record) == 3


if __name__ == "__main__":
Expand Down

0 comments on commit a5bd1af

Please sign in to comment.