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 53 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 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
33c6b6e
Merge branch 'SciTools:main' into bugfix-shape-masking
hsteptoe Jul 1, 2025
b528b02
Complete draft units tests for transform_geometry
hsteptoe Jul 2, 2025
e803a3d
Complete draft units tests for create_shapefile_mask
hsteptoe Jul 2, 2025
c02bebc
Complete draft units tests for _make_raster_cube_transform
hsteptoe Jul 2, 2025
7b452cd
Complete draft units tests for is_geometry_valid
hsteptoe Jul 2, 2025
0ebb9af
Auto formatting fixes
hsteptoe Jul 2, 2025
417be29
Complete draft units tests for _get_weighted_mask
hsteptoe Jul 3, 2025
f91f26a
Merge branch 'bugfix-shape-masking' of github.com:hsteptoe/iris into …
hsteptoe Jul 3, 2025
dcb85f6
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 3, 2025
df3e457
Merge branch 'main' into bugfix-shape-masking
hsteptoe Jul 3, 2025
df7c1a5
Remove def _get_mod_rebased_coord_bounds
hsteptoe Jul 3, 2025
42cca35
Update reqs to include affine package
hsteptoe Jul 3, 2025
24180a3
Linting edits
hsteptoe Jul 3, 2025
e322de4
Linting edits
hsteptoe Jul 3, 2025
e60f3ab
Add cube crs error checking
hsteptoe Jul 3, 2025
ab02993
Working integration tests for mask_cube_from_shapefie
hsteptoe Jul 4, 2025
f1c6c37
Working unit tests for mask_cube_from_shapefie
hsteptoe Jul 4, 2025
d2aa50a
Merge branch 'SciTools:main' into bugfix-shape-masking
hsteptoe Jul 4, 2025
42eed14
Merge branch 'bugfix-shape-masking' of github.com:hsteptoe/iris into …
hsteptoe Jul 4, 2025
f28f7b8
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 4, 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
545 changes: 378 additions & 167 deletions lib/iris/_shapefiles.py

Large diffs are not rendered by default.

277 changes: 184 additions & 93 deletions lib/iris/tests/integration/test_mask_cube_from_shapefile.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,104 +6,195 @@

import math

import cartopy.crs as ccrs
import cartopy.io.shapereader as shpreader
import numpy as np
from pyproj import CRS
import pytest
from pytest import approx
from shapely.geometry import LineString, MultiLineString, MultiPoint, Point

import iris
from iris.coord_systems import GeogCS
import iris.tests as tests
from iris.util import mask_cube_from_shapefile

wgs84 = CRS.from_epsg(4326)

@tests.skip_data
class TestCubeMasking(tests.IrisTest):
"""integration tests of mask_cube_from_shapefile
using different projections in iris_test_data -
values are the KGO calculated using ASCEND.
"""

def setUp(self):
ne_countries = shpreader.natural_earth(
resolution="10m", category="cultural", name="admin_0_countries"
)
self.reader = shpreader.Reader(ne_countries)

def test_global_proj_russia(self):
path = tests.get_data_path(
["NetCDF", "global", "xyt", "SMALL_hires_wind_u_for_ipcc4.nc"]
)
test_global = iris.load_cube(path)
ne_russia = [
country.geometry
for country in self.reader.records()
if "Russia" in country.attributes["NAME_LONG"]
][0]
masked_test = mask_cube_from_shapefile(test_global, ne_russia)
print(np.sum(masked_test.data))
assert math.isclose(np.sum(masked_test.data), 76845.37, rel_tol=0.001), (
"Global data with Russia mask failed test"
)

def test_rotated_pole_proj_germany(self):
path = tests.get_data_path(
["NetCDF", "rotated", "xy", "rotPole_landAreaFraction.nc"]
)
test_rotated = iris.load_cube(path)
ne_germany = [
country.geometry
for country in self.reader.records()
if "Germany" in country.attributes["NAME_LONG"]
][0]
masked_test = mask_cube_from_shapefile(test_rotated, ne_germany)
assert math.isclose(np.sum(masked_test.data), 179.46872, rel_tol=0.001), (
"rotated europe data with German mask failed test"
)

def test_transverse_mercator_proj_uk(self):
path = tests.get_data_path(
["NetCDF", "transverse_mercator", "tmean_1910_1910.nc"]
)
test_transverse = iris.load_cube(path)
ne_uk = [
country.geometry
for country in self.reader.records()
if "United Kingdom" in country.attributes["NAME_LONG"]
][0]
masked_test = mask_cube_from_shapefile(test_transverse, ne_uk)
assert math.isclose(np.sum(masked_test.data), 90740.25, rel_tol=0.001), (
"transverse mercator UK data with UK mask failed test"
)

def test_rotated_pole_proj_germany_weighted_area(self):
path = tests.get_data_path(
["NetCDF", "rotated", "xy", "rotPole_landAreaFraction.nc"]
)
test_rotated = iris.load_cube(path)
ne_germany = [
country.geometry
for country in self.reader.records()
if "Germany" in country.attributes["NAME_LONG"]
][0]
masked_test = mask_cube_from_shapefile(
test_rotated, ne_germany, minimum_weight=0.9
)
assert math.isclose(np.sum(masked_test.data), 125.60199, rel_tol=0.001), (
"rotated europe data with 0.9 weight germany mask failed test"
)

def test_4d_global_proj_brazil(self):
path = tests.get_data_path(["NetCDF", "global", "xyz_t", "GEMS_CO2_Apr2006.nc"])
test_4d_brazil = iris.load_cube(path, "Carbon Dioxide")
ne_brazil = [
country.geometry
for country in self.reader.records()
if "Brazil" in country.attributes["NAME_LONG"]
][0]
masked_test = mask_cube_from_shapefile(
test_4d_brazil,
ne_brazil,
)
print(np.sum(masked_test.data))
# breakpoint()
assert math.isclose(np.sum(masked_test.data), 18616921.2, rel_tol=0.001), (
"4d data with brazil mask failed test"
)
ne_countries = shpreader.natural_earth(
resolution="10m", category="cultural", name="admin_0_countries"
)
reader = shpreader.Reader(ne_countries)


@pytest.mark.parametrize(
("minimum_weight", "all_touched", "invert", "expected_sum"),
[
(0.0, None, None, 10522684.77), # Minimum weight == 0
(0.0, None, False, 10522684.77), # Minimum weight == 0
(0.0, True, False, 10522684.77), # All touched == True
(0.5, None, False, 8965584.47), # Minimum weight == 0.5
(1.0, None, False, 7504361.29), # Minimum weight == 1
(0.0, False, False, 8953582.05), # All touched == False
(0.0, True, True, 605637718.12), # All touched == True, Invert == True
],
)
def test_global_proj_china(minimum_weight, all_touched, invert, expected_sum):
"""Test masking with a shape for China with various parameter combinations."""
path = tests.get_data_path(["NetCDF", "global", "xyt", "SMALL_total_column_co2.nc"])
test_global = iris.load_cube(path) # Crop to avoid edge effects
test_global.coord("latitude").coord_system = GeogCS(6371229)
test_global.coord("longitude").coord_system = GeogCS(6371229)
ne_china = [
country.geometry
for country in reader.records()
if "China" in country.attributes["NAME_LONG"]
][0]
masked_test = mask_cube_from_shapefile(
test_global,
ne_china,
shape_crs=wgs84,
minimum_weight=minimum_weight,
all_touched=all_touched,
invert=invert,
)
assert masked_test.ndim == 3
assert approx(np.sum(masked_test.data), rel=0.001) == expected_sum


def test_global_proj_russia():
"""Test masking with a shape that crosses the antimeridian."""
path = tests.get_data_path(["NetCDF", "global", "xyt", "SMALL_total_column_co2.nc"])
test_global = iris.load_cube(path)
test_global.coord("latitude").coord_system = GeogCS(6371229)
test_global.coord("longitude").coord_system = GeogCS(6371229)
ne_russia = [
country.geometry
for country in reader.records()
if "Russia" in country.attributes["NAME_LONG"]
][0]

with pytest.raises(
ValueError, match="Geometry crossing the antimeridian is not supported."
):
mask_cube_from_shapefile(test_global, ne_russia, shape_crs=wgs84)


def test_rotated_pole_proj_uk():
"""Test masking a rotated pole projection cube for the UK with lat/lon shape."""
path = tests.get_data_path(
["NetCDF", "rotated", "xy", "rotPole_landAreaFraction.nc"]
)
test_rotated = iris.load_cube(path)
ne_uk = [
country.geometry
for country in reader.records()
if "United Kingdom" in country.attributes["NAME_LONG"]
][0]
masked_test = mask_cube_from_shapefile(test_rotated, ne_uk, shape_crs=wgs84)
assert masked_test.ndim == 2
assert approx(np.sum(masked_test.data), rel=0.001) == 102.77


def test_transverse_mercator_proj_uk():
"""Test masking a transverse mercator projection cube for the UK with lat/lon shape."""
path = tests.get_data_path(["NetCDF", "transverse_mercator", "tmean_1910_1910.nc"])
test_transverse = iris.load_cube(path)
ne_uk = [
country.geometry
for country in reader.records()
if "United Kingdom" in country.attributes["NAME_LONG"]
][0]
masked_test = mask_cube_from_shapefile(test_transverse, ne_uk, shape_crs=wgs84)
assert masked_test.ndim == 3
assert approx(np.sum(masked_test.data), rel=0.001) == 90740.25


def test_rotated_pole_proj_germany_weighted_area():
"""Test masking a rotated pole projection cube for Germany with weighted area."""
path = tests.get_data_path(
["NetCDF", "rotated", "xy", "rotPole_landAreaFraction.nc"]
)
test_rotated = iris.load_cube(path)
ne_germany = [
country.geometry
for country in reader.records()
if "Germany" in country.attributes["NAME_LONG"]
][0]
masked_test = mask_cube_from_shapefile(
test_rotated, ne_germany, shape_crs=wgs84, minimum_weight=0.9
)
assert masked_test.ndim == 2
assert approx(np.sum(masked_test.data), rel=0.001) == 125.60199


def test_4d_global_proj_brazil():
"""Test masking a 4D global projection cube for Brazil with lat/lon shape."""
path = tests.get_data_path(["NetCDF", "global", "xyz_t", "GEMS_CO2_Apr2006.nc"])
test_4d_brazil = iris.load_cube(path, "Carbon Dioxide")
test_4d_brazil.coord("latitude").coord_system = GeogCS(6371229)
test_4d_brazil.coord("longitude").coord_system = GeogCS(6371229)
ne_brazil = [
country.geometry
for country in reader.records()
if "Brazil" in country.attributes["NAME_LONG"]
][0]
masked_test = mask_cube_from_shapefile(
test_4d_brazil, ne_brazil, shape_crs=wgs84, all_touched=True
)
assert masked_test.ndim == 4
assert approx(np.sum(masked_test.data), rel=0.001) == 18616921.2


@pytest.mark.parametrize(
("shape", "expected_value"),
[
(Point(-3.475446894622651, 50.72770791320487), 12061.74), # (x,y)
(
LineString(
[
(-5.712431305030631, 50.06590599588483),
(-3.0704940433528947, 58.644091639685456),
]
),
120530.41,
), # (x,y) to (x,y)
(
MultiPoint(
[
(-5.712431305030631, 50.06590599588483),
(-3.0704940433528947, 58.644091639685456),
]
),
24097.47,
),
(
MultiLineString(
[
[
(-5.206405826948041, 49.95891620303525),
(-3.376975634580173, 58.67197421392852),
],
[
(-6.2276386132877475, 56.71561805509071),
(1.7626540441873777, 52.48118683241357),
],
]
),
253248.44,
),
],
)
def test_global_proj_uk_shapes(shape, expected_value):
"""Test masking with a variety of shape types."""
path = tests.get_data_path(["NetCDF", "global", "xyt", "SMALL_total_column_co2.nc"])
test_global = iris.load_cube(path) # Crop to avoid edge effects
test_global.coord("latitude").coord_system = GeogCS(6371229)
test_global.coord("longitude").coord_system = GeogCS(6371229)
masked_test = mask_cube_from_shapefile(
test_global,
shape,
shape_crs=wgs84,
)
assert masked_test.ndim == 3
assert approx(np.sum(masked_test.data), rel=0.001) == expected_value
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."""
Loading
Loading