-
Notifications
You must be signed in to change notification settings - Fork 315
Equalise cubes #6257
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
Merged
Merged
Equalise cubes #6257
Changes from 7 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
fdc78be
Initial equalise_cubes util.
pp-mo 0b71f59
Initial something working.
pp-mo f483d4b
Tweaks, improvements, notes.
pp-mo cbf7514
Initial partial testing
pp-mo 94a70a2
Small tweaks.
pp-mo dcf40d5
Tidy a bit. Test 'unify_time_units'. NB time-units are on coords not…
pp-mo 1265c26
Fix grouping efficiency.
pp-mo f6150de
Review changes: rename vars.
pp-mo 844b18d
Review changes: rename 'unify_names'.
pp-mo 8a00808
Review changes: Add warning on null operation.
pp-mo 8b56344
Review changes: Remove mistaken docstring reference to re-ordering.
pp-mo ae992ff
Review changes: Simplify in-place replacement of list/array content.
pp-mo cc5ffc9
Merge branch 'main' into equalise_cubes
pp-mo 62285b5
Added whatsnew.
pp-mo e841ec2
Review changes: explain scrambling; in-place scramble doesn't return …
pp-mo e30e5d8
Added specific test for array attribute handling.
pp-mo a19b409
Simplify 'scramble' operation.
pp-mo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,266 @@ | ||
| # 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 :func:`iris.util.equalise_cubes` function.""" | ||
|
|
||
| from cf_units import Unit | ||
| import numpy as np | ||
| from numpy.random import Generator | ||
| import pytest | ||
|
|
||
| from iris.coords import DimCoord | ||
| from iris.cube import Cube | ||
| from iris.util import equalise_cubes | ||
|
|
||
|
|
||
| def _scramble(inputs, rng=95297): | ||
| # Reorder items to check that order does not affect the operation | ||
| if not isinstance(rng, Generator): | ||
| rng = np.random.default_rng(rng) | ||
| n_inputs = len(inputs) | ||
| # NOTE: make object array of explicit shape + fill it, | ||
| # since np.array(inputs) *fails* specifically with a list of metadata objects | ||
| inputs_array = np.empty((n_inputs,), dtype=object) | ||
| inputs_array[:] = inputs | ||
| n_inputs = inputs_array.shape[0] | ||
| scramble_inds = rng.permutation(n_inputs) | ||
| inputs_array = inputs_array[scramble_inds] | ||
| # Modify input list **BUT N.B. IN PLACE** | ||
| inputs[0:] = inputs_array | ||
|
stephenworsley marked this conversation as resolved.
Outdated
|
||
| return inputs | ||
|
|
||
|
|
||
| @pytest.fixture(params=["off", "on", "applyall", "scrambled"]) | ||
| def usage(request): | ||
| # Fixture to check different usage modes for a given operation control keyword | ||
| return request.param | ||
|
|
||
|
|
||
| def _usage_common(usage, op_keyword_name, test_cubes): | ||
| kwargs = {} | ||
| if usage == "off": | ||
| pass | ||
| elif usage in ("on", "scrambled"): | ||
| kwargs[op_keyword_name] = True | ||
| if usage == "scrambled": | ||
| # reorder the input cubes, but in-place | ||
| _scramble(test_cubes) | ||
| elif usage == "applyall": | ||
| kwargs["apply_all"] = True | ||
| else: | ||
| raise ValueError(f"Unrecognised 'usage' option {usage!r}") | ||
| default_expected_metadatas = [cube.metadata for cube in test_cubes] | ||
| return kwargs, default_expected_metadatas | ||
|
|
||
|
|
||
| def _cube( | ||
| stdname=None, | ||
| longname=None, | ||
| varname=None, | ||
| units="unknown", | ||
| cell_methods=(), | ||
| **attributes, | ||
| ): | ||
| # Construct a simple test-cube with given metadata properties. | ||
| cube = Cube( | ||
| [1], | ||
| standard_name=stdname, | ||
| long_name=longname, | ||
| var_name=varname, | ||
| cell_methods=cell_methods, | ||
| units=units, | ||
| attributes=attributes, | ||
| ) | ||
| return cube | ||
|
|
||
|
|
||
| class TestUnifyNames: | ||
| # Test the 'unify_names' operation. | ||
| def test_simple(self, usage): | ||
| sn = "air_temperature" | ||
| stdnames = [sn, sn, sn] | ||
| longnames = [None, "long1", "long2"] | ||
| varnames = ["var1", None, "var2"] | ||
| test_cubes = [ | ||
| _cube(stdname=stdname, longname=longname, varname=varname) | ||
| for stdname, longname, varname in zip(stdnames, longnames, varnames) | ||
| ] | ||
| kwargs, expected_metadatas = _usage_common(usage, "unify_names", test_cubes) | ||
|
|
||
| # Calculate expected results | ||
| if usage != "off": | ||
| # result cube metadata should all be the same, with no varname | ||
| meta = _cube(stdname=sn).metadata | ||
| expected_metadatas = [meta, meta, meta] | ||
|
|
||
| # Apply operation | ||
| results = equalise_cubes(test_cubes, **kwargs) | ||
|
|
||
| # Assert result | ||
| assert [cube.metadata for cube in results] == expected_metadatas | ||
|
|
||
| def test_multi(self, usage): | ||
| # Show that different cases are resolved independently | ||
| sn1, sn2 = "air_temperature", "air_pressure" | ||
| stdnames = [sn1, None, None, None, sn2, None] | ||
| longnames = ["long1", "long2", None, None, "long3", None] | ||
| varnames = ["var1", None, "var3", "var4", None, None] | ||
| test_cubes = [ | ||
| _cube(stdname=stdname, longname=longname, varname=varname) | ||
| for stdname, longname, varname in zip(stdnames, longnames, varnames) | ||
| ] | ||
| kwargs, expected_metadatas = _usage_common(usage, "unify_names", test_cubes) | ||
|
|
||
| # Calculate expected results | ||
| if usage != "off": | ||
| stdnames = [sn1, None, None, None, sn2, None] | ||
| longnames = [None, "long2", None, None, None, None] | ||
| varnames = [None, None, "var3", "var4", None, None] | ||
| expected_metadatas = [ | ||
| _cube(stdname=stdname, longname=longname, varname=varname).metadata | ||
| for stdname, longname, varname in zip(stdnames, longnames, varnames) | ||
| ] | ||
| if usage == "scrambled": | ||
| expected_metadatas = _scramble(expected_metadatas) | ||
|
stephenworsley marked this conversation as resolved.
Outdated
|
||
|
|
||
| # Apply operation | ||
| results = equalise_cubes(test_cubes, **kwargs) | ||
|
|
||
| # Assert result | ||
| assert [cube.metadata for cube in results] == expected_metadatas | ||
|
|
||
|
|
||
| class TestEqualiseAttributes: | ||
| # Test the 'equalise_attributes' operation. | ||
| def test_calling(self, usage, mocker): | ||
| patch = mocker.patch("iris.util.equalise_attributes") | ||
| test_cubes = [_cube()] | ||
| kwargs, expected_metadatas = _usage_common( | ||
| usage, "equalise_attributes", test_cubes | ||
| ) | ||
|
|
||
| # Apply operation | ||
| equalise_cubes(test_cubes, **kwargs) | ||
|
|
||
| expected_calls = 0 if usage == "off" else 1 | ||
| assert len(patch.call_args_list) == expected_calls | ||
|
|
||
| def test_basic_function(self, usage): | ||
| test_cubes = [_cube(att_a=10, att_b=1), _cube(att_a=10, att_b=2)] | ||
| kwargs, expected_metadatas = _usage_common( | ||
| usage, "equalise_attributes", test_cubes | ||
| ) | ||
|
|
||
| # Calculate expected results | ||
| if usage != "off": | ||
| # result cube metadata should all be the same, with no varname | ||
| meta = _cube(att_a=10).metadata | ||
| expected_metadatas = [meta, meta] | ||
|
|
||
| # Apply operation | ||
| results = equalise_cubes(test_cubes, **kwargs) | ||
|
|
||
| # Assert result | ||
| assert [cube.metadata for cube in results] == expected_metadatas | ||
|
|
||
| def test_operation_in_groups(self, usage): | ||
| # Check that it acts independently within groups (as defined, here, by naming) | ||
| test_cubes = [ | ||
| _cube(longname="a", att_a=10, att_b=1), | ||
| _cube(longname="a", att_a=10, att_b=2), | ||
| _cube(longname="b", att_a=10, att_b=1), | ||
| _cube(longname="b", att_a=10, att_b=1), | ||
| ] | ||
| kwargs, expected_metadatas = _usage_common( | ||
| usage, "equalise_attributes", test_cubes | ||
| ) | ||
|
|
||
| # Calculate expected results | ||
| if usage != "off": | ||
| # result cube metadata should all be the same, with no varname | ||
| expected_metadatas = [ | ||
| # the "a" cubes have lost att_b, but the "b" cubes retain it | ||
| _cube(longname="a", att_a=10).metadata, | ||
| _cube(longname="a", att_a=10).metadata, | ||
| _cube(longname="b", att_a=10, att_b=1).metadata, | ||
| _cube(longname="b", att_a=10, att_b=1).metadata, | ||
| ] | ||
| if usage == "scrambled": | ||
| _scramble(expected_metadatas) | ||
|
|
||
| # Apply operation | ||
| results = equalise_cubes(test_cubes, **kwargs) | ||
|
|
||
| # Assert result | ||
| assert [cube.metadata for cube in results] == expected_metadatas | ||
|
|
||
|
|
||
| class TestUnifyTimeUnits: | ||
| # Test the 'unify_time_units' operation. | ||
| def test_calling(self, usage, mocker): | ||
| patch = mocker.patch("iris.util.unify_time_units") | ||
| test_cubes = [_cube()] | ||
| kwargs, expected_metadatas = _usage_common( | ||
| usage, "unify_time_units", test_cubes | ||
| ) | ||
|
|
||
| # Apply operation | ||
| equalise_cubes(test_cubes, **kwargs) | ||
|
|
||
| expected_calls = 0 if usage == "off" else 1 | ||
| assert len(patch.call_args_list) == expected_calls | ||
|
|
||
| def _cube_timeunits(self, unit, **kwargs): | ||
| cube = _cube(**kwargs) | ||
| cube.add_dim_coord(DimCoord([0.0], standard_name="time", units=unit), 0) | ||
| return cube | ||
|
|
||
| def test_basic_function(self, usage): | ||
| if usage == "scrambled": | ||
| pytest.skip("scrambled mode not supported") | ||
| tu1, tu2 = [Unit(name) for name in ("days since 1970", "days since 1971")] | ||
| cu1, cu2 = self._cube_timeunits(tu1), self._cube_timeunits(tu2) | ||
| test_cubes = [cu1, cu2] | ||
| kwargs, expected_metadatas = _usage_common( | ||
| usage, "unify_time_units", test_cubes | ||
| ) | ||
|
|
||
| expected_units = [tu1, tu2 if usage == "off" else tu1] | ||
|
|
||
| # Apply operation | ||
| results = equalise_cubes(test_cubes, **kwargs) | ||
|
|
||
| # Assert result | ||
| assert [cube.coord("time").units for cube in results] == expected_units | ||
|
|
||
| def test_operation_in_groups(self, usage): | ||
| # Check that it acts independently within groups (as defined, here, by naming) | ||
| test_cubes = [ | ||
| _cube(longname="a", att_a=10, att_b=1), | ||
| _cube(longname="a", att_a=10, att_b=2), | ||
| _cube(longname="b", att_a=10, att_b=1), | ||
| _cube(longname="b", att_a=10, att_b=1), | ||
| ] | ||
| kwargs, expected_metadatas = _usage_common( | ||
| usage, "equalise_attributes", test_cubes | ||
| ) | ||
|
|
||
| # Calculate expected results | ||
| if usage != "off": | ||
| # result cube metadata should all be the same, with no varname | ||
| expected_metadatas = [ | ||
| # the "a" cubes have lost att_b, but the "b" cubes retain it | ||
| _cube(longname="a", att_a=10).metadata, | ||
| _cube(longname="a", att_a=10).metadata, | ||
| _cube(longname="b", att_a=10, att_b=1).metadata, | ||
| _cube(longname="b", att_a=10, att_b=1).metadata, | ||
| ] | ||
| if usage == "scrambled": | ||
| _scramble(expected_metadatas) | ||
|
|
||
| # Apply operation | ||
| results = equalise_cubes(test_cubes, **kwargs) | ||
|
|
||
| # Assert result | ||
| assert [cube.metadata for cube in results] == expected_metadatas | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.