From 8540adff5e7a27f609cc4dd1f60a42fd2bd38c4e Mon Sep 17 00:00:00 2001 From: Max White Date: Tue, 2 Jun 2026 15:59:33 +0100 Subject: [PATCH 1/7] Fix function --- .../generate_miscellaneous_ancillaries.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py b/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py index 8861088703..1e6fd9d750 100644 --- a/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py +++ b/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py @@ -68,8 +68,7 @@ def generate_land_area_fraction_at_sites( Args: land_cover_cube: A cube containing the Corine Land cover data. The data values should be - integers representing - different land cover types. + integers representing different land cover types. neighbour_cube: A cube containing information about the spot data sites. We use this rather than a site list as it contains a completed set of altitudes which have @@ -151,4 +150,7 @@ def generate_land_area_fraction_at_sites( site_frac.coord(crd).points = site.coord(crd).points.astype(crd_type) land_fraction.append(site_frac) - return land_fraction.concatenate_cube() + land_fraction = land_fraction.concatenate_cube() + land_fraction.rename("land_area_fraction") + + return land_fraction From 950d8e5042698736f3cdcca0d9704595f98fd240 Mon Sep 17 00:00:00 2001 From: Max White Date: Thu, 4 Jun 2026 20:53:43 +0100 Subject: [PATCH 2/7] Refactor for ease of use --- .../generate_miscellaneous_ancillaries.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py b/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py index 1e6fd9d750..5468210d89 100644 --- a/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py +++ b/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py @@ -19,7 +19,7 @@ def generate_roughness_length_at_sites( - roughness_length: Cube, neighbour_cube: Cube + roughness_length_cube: Cube, neighbour_cube: Cube, ignore_grid_match: bool = False ) -> Cube: """Generate a roughness length ancillary cube at the site locations. This performs a spot extraction of the roughness length data at the site locations and removes time @@ -31,12 +31,16 @@ def generate_roughness_length_at_sites( neighbour_cube: A cube containing information about the spot data sites and their grid point neighbours. + ignore_grid_match: + If True, the coordinate hash comparison between the diagnostic cube and + the neighbour cube will be ignored. This allows the version of Iris and/or + Numpy to be different from those that generated the neighbour cube. Returns: A cube containing the roughness length at the site locations. """ - roughness_length_spot = SpotExtraction(neighbour_selection_method="nearest")( - neighbour_cube, roughness_length - ) + roughness_length_spot = SpotExtraction( + neighbour_selection_method="nearest", ignore_grid_match=ignore_grid_match + )(neighbour_cube, roughness_length_cube) # Update metadata to remove any time coordinates cube_coord = [coord.name() for coord in roughness_length_spot.coords()] From 7543a51a6dfcdde3755423680f22ce3d01b97a7a Mon Sep 17 00:00:00 2001 From: Max White Date: Tue, 30 Jun 2026 09:35:10 +0100 Subject: [PATCH 3/7] Review fixes --- .../generate_miscellaneous_ancillaries.py | 12 ++++++------ .../test_miscellaneous_ancillaries.py | 12 +++++++++--- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py b/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py index 5468210d89..39d0fd0f88 100644 --- a/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py +++ b/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py @@ -26,7 +26,7 @@ def generate_roughness_length_at_sites( related coordinates. Args: - roughness_length: + roughness_length_cube: A cube containing the roughness length data. neighbour_cube: A cube containing information about the spot data sites and @@ -137,7 +137,7 @@ def generate_land_area_fraction_at_sites( x_index_con = Constraint(grid_attributes_key="x_index") y_index_con = Constraint(grid_attributes_key="y_index") - land_fraction = CubeList() + land_area_fraction = CubeList() for site in neighbours.slices_over("spot_index"): ix, iy = ( site.extract(x_index_con).data.astype(int).item(), @@ -152,9 +152,9 @@ def generate_land_area_fraction_at_sites( site_frac = template.copy(data=np.array([fraction], dtype=np.float32)) for crd, crd_type in crd_types.items(): site_frac.coord(crd).points = site.coord(crd).points.astype(crd_type) - land_fraction.append(site_frac) + land_area_fraction.append(site_frac) - land_fraction = land_fraction.concatenate_cube() - land_fraction.rename("land_area_fraction") + land_area_fraction = land_area_fraction.concatenate_cube() + land_area_fraction.rename("land_area_fraction") - return land_fraction + return land_area_fraction diff --git a/improver_tests/generate_ancillaries/test_miscellaneous_ancillaries.py b/improver_tests/generate_ancillaries/test_miscellaneous_ancillaries.py index c446e2a9d7..e80ba07eb3 100644 --- a/improver_tests/generate_ancillaries/test_miscellaneous_ancillaries.py +++ b/improver_tests/generate_ancillaries/test_miscellaneous_ancillaries.py @@ -195,8 +195,12 @@ def test_land_area_fraction_ancillary(corine_land_cover, radius, expected): assert_array_almost_equal(land_area_fraction.data, expected) -def test_roughness_length_ancillary(default_neighbour_cube, gridded_template_cube): - """Test that the roughness length ancillary is generated correctly.""" +@pytest.mark.parametrize("choice", (True, False)) +def test_roughness_length_ancillary( + default_neighbour_cube, gridded_template_cube, choice +): + """Test that the roughness length ancillary is generated correctly, with and without + ignoring the grid match between the diagnostic cube and the spot neighbour cube.""" # Create a gridded roughness length cube roughness_cube = gridded_template_cube.copy() @@ -205,7 +209,9 @@ def test_roughness_length_ancillary(default_neighbour_cube, gridded_template_cub roughness_cube.data = np.array([[0.1, 0.2, 0.3], [0.4, 0.5, 0.6], [0.7, 0.8, 0.9]]) # Generate the roughness length ancillary - result = generate_roughness_length_at_sites(roughness_cube, default_neighbour_cube) + result = generate_roughness_length_at_sites( + roughness_cube, default_neighbour_cube, ignore_grid_match=choice + ) # Ensure the cube has the correct metadata assert result.name() == "roughness_length" From cf51b806b67f9c52d2cf374445cfbc163fa0bcb0 Mon Sep 17 00:00:00 2001 From: Max White Date: Wed, 1 Jul 2026 09:31:05 +0100 Subject: [PATCH 4/7] Make pytest parameterize variable more explicit --- .../generate_ancillaries/test_miscellaneous_ancillaries.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/improver_tests/generate_ancillaries/test_miscellaneous_ancillaries.py b/improver_tests/generate_ancillaries/test_miscellaneous_ancillaries.py index e80ba07eb3..a441a56513 100644 --- a/improver_tests/generate_ancillaries/test_miscellaneous_ancillaries.py +++ b/improver_tests/generate_ancillaries/test_miscellaneous_ancillaries.py @@ -195,9 +195,9 @@ def test_land_area_fraction_ancillary(corine_land_cover, radius, expected): assert_array_almost_equal(land_area_fraction.data, expected) -@pytest.mark.parametrize("choice", (True, False)) +@pytest.mark.parametrize("ignore_grid_match", (True, False)) def test_roughness_length_ancillary( - default_neighbour_cube, gridded_template_cube, choice + default_neighbour_cube, gridded_template_cube, ignore_grid_match ): """Test that the roughness length ancillary is generated correctly, with and without ignoring the grid match between the diagnostic cube and the spot neighbour cube.""" @@ -210,7 +210,7 @@ def test_roughness_length_ancillary( # Generate the roughness length ancillary result = generate_roughness_length_at_sites( - roughness_cube, default_neighbour_cube, ignore_grid_match=choice + roughness_cube, default_neighbour_cube, ignore_grid_match=ignore_grid_match ) # Ensure the cube has the correct metadata From 2152abda55908630bcb42f3e0698e2bb2e1f0f66 Mon Sep 17 00:00:00 2001 From: Max White Date: Wed, 1 Jul 2026 10:21:59 +0100 Subject: [PATCH 5/7] Fix CoPilot suggestion --- .../generate_miscellaneous_ancillaries.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py b/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py index 39d0fd0f88..ca032883dc 100644 --- a/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py +++ b/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py @@ -137,7 +137,7 @@ def generate_land_area_fraction_at_sites( x_index_con = Constraint(grid_attributes_key="x_index") y_index_con = Constraint(grid_attributes_key="y_index") - land_area_fraction = CubeList() + land_area_fraction_cubelist = CubeList() for site in neighbours.slices_over("spot_index"): ix, iy = ( site.extract(x_index_con).data.astype(int).item(), @@ -152,9 +152,9 @@ def generate_land_area_fraction_at_sites( site_frac = template.copy(data=np.array([fraction], dtype=np.float32)) for crd, crd_type in crd_types.items(): site_frac.coord(crd).points = site.coord(crd).points.astype(crd_type) - land_area_fraction.append(site_frac) + land_area_fraction_cubelist.append(site_frac) - land_area_fraction = land_area_fraction.concatenate_cube() + land_area_fraction = land_area_fraction_cubelist.concatenate_cube() land_area_fraction.rename("land_area_fraction") return land_area_fraction From 3360cd683f78e50dcbb4d3cb6ec2e0e5bb801cc8 Mon Sep 17 00:00:00 2001 From: Max White Date: Thu, 9 Jul 2026 14:49:11 +0100 Subject: [PATCH 6/7] Fix error in Corine classification --- .../generate_miscellaneous_ancillaries.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py b/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py index ca032883dc..50aeecf059 100644 --- a/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py +++ b/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py @@ -94,8 +94,8 @@ def generate_land_area_fraction_at_sites( xaxis, yaxis = land_cover_cube.coord(axis="x"), land_cover_cube.coord(axis="y") land_cover_cube = next(land_cover_cube.slices([xaxis, yaxis])) - # 41-44 is the key for water in the Corine Land cover dataset. - land_mask = land_cover_cube.copy(data=np.where(land_cover_cube.data > 40, 0, 1)) + # 40-44 are the keys for water bodies in the Corine Land cover dataset. + land_mask = land_cover_cube.copy(data=np.where(land_cover_cube.data > 39, 0, 1)) # Oceans far from the coast have data value -128 to represent no data land_mask.data = np.where(land_cover_cube.data < 0, 0, land_mask.data) # 48 is the key for complex land surfaces in Corine From b20f0b41a5bc4efc1f19507caec7d489bd92c8ad Mon Sep 17 00:00:00 2001 From: Max White Date: Thu, 9 Jul 2026 15:18:34 +0100 Subject: [PATCH 7/7] Tidy classification to reduce unnecessary re-masking --- .../generate_miscellaneous_ancillaries.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py b/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py index 50aeecf059..d1cf537024 100644 --- a/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py +++ b/improver/generate_ancillaries/generate_miscellaneous_ancillaries.py @@ -94,12 +94,18 @@ def generate_land_area_fraction_at_sites( xaxis, yaxis = land_cover_cube.coord(axis="x"), land_cover_cube.coord(axis="y") land_cover_cube = next(land_cover_cube.slices([xaxis, yaxis])) - # 40-44 are the keys for water bodies in the Corine Land cover dataset. - land_mask = land_cover_cube.copy(data=np.where(land_cover_cube.data > 39, 0, 1)) - # Oceans far from the coast have data value -128 to represent no data - land_mask.data = np.where(land_cover_cube.data < 0, 0, land_mask.data) - # 48 is the key for complex land surfaces in Corine - land_mask.data = np.where(land_cover_cube.data == 48, 1, land_mask.data) + # In the Corine Land cover dataset, keys 40-44 are for water bodies, -128 is for + # oceans far from the coast, and 48 is for complex land surfaces + # Thus, keys between 40 and 44 and below zero should be classified as non-land and + # all other keys as land. + land_mask = land_cover_cube.copy( + data=np.where( + ((land_cover_cube.data >= 40) & (land_cover_cube.data <= 44)) + | (land_cover_cube.data < 0), + 0, + 1, + ) + ) # Extract the site definitions from the provided neighbour cube. site_definitions = extract_site_json(neighbour_cube)