Skip to content

Bugfix shape masking #6129

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 33 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ebee977
Updates to allow varying shape crs
hsteptoe Aug 22, 2024
e32f911
Ruff fixes
hsteptoe Aug 22, 2024
76eae38
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 22, 2024
bcdca30
Merge branch 'SciTools:main' into bugfix-shape-masking
hsteptoe Nov 22, 2024
9dab7c6
Merge branch 'SciTools:main' into bugfix-shape-masking
hsteptoe Dec 6, 2024
ebaa1ee
Adding rasterio as optional dependency
hsteptoe Dec 6, 2024
103e3d7
Adding rasterio functionality
hsteptoe Dec 19, 2024
3c65dab
First working version with rasterio
hsteptoe Dec 20, 2024
25de18d
Add further shape geometry checks
hsteptoe Jan 3, 2025
10e4413
Update shapefile tests
hsteptoe Jan 3, 2025
cd5cf99
Reorganise test fixtures
hsteptoe Jan 3, 2025
edc25cc
Reorganise test fixtures
hsteptoe Jan 3, 2025
789c98d
Fix fixture fetching
hsteptoe Jan 3, 2025
27a9025
Merge branch 'SciTools:main' into bugfix-shape-masking
hsteptoe Jan 3, 2025
dc090a5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jan 3, 2025
5b1054b
Merge branch 'SciTools:main' into bugfix-shape-masking
hsteptoe Feb 7, 2025
8948ce6
Recording Affine transform trials
hsteptoe Feb 13, 2025
a77c628
Working masking
hsteptoe Feb 14, 2025
1793e1f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 14, 2025
32ebef8
Merge branch 'main' into bugfix-shape-masking
hsteptoe Apr 4, 2025
b972c35
Linting and doc edits
hsteptoe Apr 4, 2025
9675c0f
Merge remote-tracking branch 'refs/remotes/origin/bugfix-shape-maskin…
hsteptoe Apr 4, 2025
458d9b7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 4, 2025
e28bea3
Merge branch 'main' into bugfix-shape-masking
hsteptoe May 16, 2025
4fcac0e
Merge branch 'SciTools:main' into bugfix-shape-masking
hsteptoe May 22, 2025
768af68
First-cut rewrite of masking by weight
hsteptoe May 23, 2025
b062e8e
Tidy function calls
hsteptoe May 23, 2025
b70fd1d
Working mask by weight
hsteptoe May 27, 2025
45f5324
Refactor create_shapefile_mask
hsteptoe May 28, 2025
3ded4fd
Passing tests for test_is_geometry_valid.py
hsteptoe May 30, 2025
89aad15
Minor geometry additions to test_is_geometry_valid.py
hsteptoe May 30, 2025
19365be
Working tests for transform_geometry
hsteptoe Jun 3, 2025
6251093
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 6, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
539 changes: 377 additions & 162 deletions lib/iris/_shapefiles.py

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions lib/iris/tests/unit/_shapefiles/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 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.
"""Unit tests for the :mod:`iris._shapefiles` module."""
189 changes: 189 additions & 0 deletions lib/iris/tests/unit/_shapefiles/test_is_geometry_valid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
# 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.
"""Unit tests for :func:`iris._shapefiles.is_geometry_valid`."""

# import iris tests first so that some things can be initialised before
# importing anything else
import iris.tests as tests # isort:skip

from pyproj import CRS
import pytest
from shapely.geometry import (
LineString,
MultiLineString,
MultiPoint,
MultiPolygon,
Point,
box,
)

from iris._shapefiles import is_geometry_valid


# Shareable shape fixtures used in:
# - util/test_mask_cube_from_shapefile.py
# - _shapefiles/test_is_geometry_valid.py
@pytest.fixture(scope="session")
def wgs84_crs():
return CRS.from_epsg(4326)


@pytest.fixture(scope="session")
def osgb_crs():
return CRS.from_epsg(27700)


@pytest.fixture(scope="session")
def basic_polygon_geometry():
# Define the coordinates of a basic rectangle
min_lon = -90
min_lat = -45
max_lon = 90
max_lat = 45

# Create the rectangular geometry
return box(min_lon, min_lat, max_lon, max_lat)


@pytest.fixture(scope="session")
def basic_multipolygon_geometry():
# Define the coordinates of a basic rectangle
min_lon = 0
min_lat = 0
max_lon = 8
max_lat = 8

# Create the rectangular geometry
return MultiPolygon(
[
box(min_lon, min_lat, max_lon, max_lat),
box(min_lon + 10, min_lat + 10, max_lon + 10, max_lat + 10),
]
)


@pytest.fixture(scope="session")
def basic_point_geometry():
# Define the coordinates of a basic point (lon, lat)
return Point((-3.476204, 50.727059))


@pytest.fixture(scope="session")
def basic_line_geometry():
# Define the coordinates of a basic line
return LineString([(0, 0), (10, 10)])


@pytest.fixture(scope="session")
def basic_multiline_geometry():
# Define the coordinates of a basic line
return MultiLineString([[(0, 0), (10, 10)], [(20, 20), (30, 30)]])


@pytest.fixture(scope="session")
def basic_point_collection():
# Define the coordinates of a basic collection of points
# as (lon, lat) tuples, assuming a WGS84 projection.
points = MultiPoint(
[
(0, 0),
(10, 10),
(-10, -10),
(-3.476204, 50.727059),
(174.761067, -36.846211),
(-77.032801, 38.892717),
]
)

return points


@pytest.fixture(scope="session")
def canada_geometry():
# Define the coordinates of a rectangle that covers Canada
return box(-143.5, 42.6, -37.8, 84.0)


@pytest.fixture(scope="session")
def bering_sea_geometry():
# Define the coordinates of a rectangle that covers the Bering Sea
return box(148.42, 49.1, -138.74, 73.12)


@pytest.fixture(scope="session")
def uk_geometry():
# Define the coordinates of a rectangle that covers the UK
return box(-10, 49, 2, 61)


@pytest.fixture(scope="session")
def invalid_geometry_poles():
# Define the coordinates of a rectangle that crosses the poles
return box(-10, -90, 10, 90)


@pytest.fixture(scope="session")
def invalid_geometry_bounds():
# Define the coordinates of a rectangle that is outside the bounds of the coordinate system
return box(-200, -100, 200, 100)


@pytest.fixture(scope="session")
def not_a_valid_geometry():
# Return an invalid geometry type
# This is not a valid geometry, e.g., a string
return "This is not a valid geometry"


# Test validity of different geometries
@pytest.mark.parametrize(
"test_input",
[
"basic_polygon_geometry",
"basic_multipolygon_geometry",
"basic_point_geometry",
"basic_point_collection",
"basic_line_geometry",
"basic_multiline_geometry",
"canada_geometry",
],
)
def test_valid_geometry(test_input, request, wgs84_crs):
# Assert that all valid geometries are return None
assert is_geometry_valid(request.getfixturevalue(test_input), wgs84_crs) is None


# Fixtures retrieved from conftest.py
# N.B. error message comparison is done with regex so
# any parentheses in the error message must be escaped (\)
@pytest.mark.parametrize(
"test_input, errortype, errormessage",
[
(
"bering_sea_geometry",
ValueError,
"Geometry crossing the antimeridian is not supported.",
),
(
"invalid_geometry_poles",
ValueError,
"Geometry crossing the poles is not supported.",
),
(
"invalid_geometry_bounds",
ValueError,
r"Geometry \[<POLYGON \(\(200 -100, 200 100, -200 100, -200 -100, 200 -100\)\)>\] is not valid for the given coordinate system EPSG:4326. \nCheck that your coordinates are correctly specified.",
),
(
"not_a_valid_geometry",
TypeError,
r"Shape geometry is not a valid shape \(not well formed\).",
),
],
)
def test_invalid_geometry(test_input, errortype, errormessage, request, wgs84_crs):
# Assert that all invalid geometries raise the expected error
with pytest.raises(errortype, match=errormessage):
is_geometry_valid(request.getfixturevalue(test_input), wgs84_crs)
100 changes: 100 additions & 0 deletions lib/iris/tests/unit/_shapefiles/test_transform_geometry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from pyproj import CRS
from pyproj import exceptions as pyproj_exceptions
import pytest
import shapely
from shapely.geometry import LineString, MultiLineString, MultiPoint, Point, Polygon

import iris
from iris._shapefiles import _transform_geometry
from iris.tests import _shared_utils, stock

wgs84 = CRS.from_epsg(4326) # WGS84 coordinate system
osgb = CRS.from_epsg(27700) # OSGB coordinate system


@pytest.mark.parametrize(
"input_geometry, input_geometry_crs, input_cube_crs, output_expected_geometry",
[
( # Basic geometry in WGS84, no transformation needed
shapely.geometry.box(-10, 50, 2, 60),
wgs84,
stock.simple_pp().coord_system()._crs,
shapely.geometry.box(-10, 50, 2, 60),
),
( # Basic geometry in WGS84, transformed to OSGB
shapely.geometry.box(-10, 50, 2, 60),
wgs84,
iris.load_cube(
_shared_utils.get_data_path(
("NetCDF", "transverse_mercator", "tmean_1910_1910.nc")
)
)
.coord_system()
.as_cartopy_projection(),
Polygon(
[
(686600.5247600826, 18834.835866007765),
(622998.2965261643, 1130592.5248690124),
(-45450.063023168186, 1150844.9676151862),
(-172954.59474739246, 41898.60193228102),
(686600.5247600826, 18834.835866007765),
]
),
),
( # Basic geometry in WGS84, no transformation needed
LineString([(-10, 50), (2, 60)]),
wgs84,
stock.simple_pp().coord_system()._crs,
LineString([(-10, 50), (2, 60)]),
),
( # Basic geometry in WGS84, no transformation needed
Point((-10, 50)),
wgs84,
stock.simple_pp().coord_system()._crs,
Point((-10, 50)),
),
],
)
def test_transform_geometry(
input_geometry,
input_geometry_crs,
input_cube_crs,
output_expected_geometry,
):
# Assert that all invalid geometries raise the expected error
out_geometry = _transform_geometry(
input_geometry, input_geometry_crs, input_cube_crs
)
assert isinstance(out_geometry, shapely.geometry.base.BaseGeometry)
assert output_expected_geometry == out_geometry


# Assert that an invalid inputs raise the expected errors
@pytest.mark.parametrize(
"input_geometry, input_geometry_crs, input_cube_crs, expected_error",
[
( # Basic geometry in WGS84, no transformation needed
"bad_input_geometry",
wgs84,
stock.simple_pp().coord_system()._crs,
AttributeError,
),
( # Basic geometry in WGS84, no transformation needed
shapely.geometry.box(-10, 50, 2, 60),
"bad_input_crs",
stock.simple_pp().coord_system()._crs,
pyproj_exceptions.CRSError,
),
( # Basic geometry in WGS84, no transformation needed
shapely.geometry.box(-10, 50, 2, 60),
wgs84,
"bad_input_cube_crs",
pyproj_exceptions.CRSError,
),
],
)
def test_transform_geometry_invalid_input(
input_geometry, input_geometry_crs, input_cube_crs, expected_error
):
with pytest.raises(expected_error):
_transform_geometry(input_geometry, input_geometry_crs, input_cube_crs)
1 change: 1 addition & 0 deletions lib/iris/tests/unit/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# See LICENSE in the root of the repository for full licensing details.
"""Unit tests fixture infra-structure."""

from pyproj import CRS
import pytest

import iris
Expand Down
Loading
Loading