diff --git a/docs/iris/src/conf.py b/docs/iris/src/conf.py index e3d9266b0b..7232d7c40e 100644 --- a/docs/iris/src/conf.py +++ b/docs/iris/src/conf.py @@ -269,6 +269,8 @@ def autolog(message): "http://schacon.github.com/git", "http://scitools.github.com/cartopy", "http://www.wmo.int/pages/prog/www/DPFS/documents/485_Vol_I_en_colour.pdf", + "https://software.ac.uk/how-cite-software", + "http://www.esrl.noaa.gov/psd/data/gridded/conventions/cdc_netcdf_standard.shtml", ] # list of sources to exclude from the build. diff --git a/lib/iris/common/resolve.py b/lib/iris/common/resolve.py index ad37247809..e772eeefce 100644 --- a/lib/iris/common/resolve.py +++ b/lib/iris/common/resolve.py @@ -230,7 +230,7 @@ def __init__(self, lhs=None, rhs=None): """ #: The ``lhs`` operand to be resolved into the resultant :class:`~iris.cube.Cube`. - self.lhs_cube = None # set in _call__ + self.lhs_cube = None # set in __call__ #: The ``rhs`` operand to be resolved into the resultant :class:`~iris.cube.Cube`. self.rhs_cube = None # set in __call__ @@ -294,6 +294,25 @@ def __init__(self, lhs=None, rhs=None): self(lhs, rhs) def __call__(self, lhs, rhs): + """ + Resolve the ``lhs`` :class:`~iris.cube.Cube` operand and ``rhs`` + :class:`~iris.cube.Cube` operand metadata. + + Involves determining all the common coordinate metadata shared between + the operands, and the metadata that is local to each operand. Given + the common metadata, the broadcast shape of the resultant resolved + :class:`~iris.cube.Cube`, which may be auto-transposed, can be + determined. + + Args: + + * lhs: + The left-hand-side :class:`~iris.cube.Cube` operand. + + * rhs: + The right-hand-side :class:`~iris.cube.Cube` operand. + + """ from iris.cube import Cube emsg = ( @@ -338,11 +357,31 @@ def __call__(self, lhs, rhs): return self def _as_compatible_cubes(self): + """ + Determine whether the ``src`` and ``tgt`` :class:`~iris.cube.Cube` can + be transposed and/or broadcast successfully together. + + If compatible, the ``_broadcast_shape`` of the resultant resolved cube is + calculated, and the ``_src_cube_resolved`` (transposed/broadcast ``src`` + cube) and ``_tgt_cube_resolved`` (same as the ``tgt`` cube) are + calculated. + + An exception will be raised if the ``src`` and ``tgt`` cannot be + broadcast, even after a suitable transpose has been performed. + + .. note:: + + Requires that **all** ``src`` cube dimensions have been mapped + successfully to an appropriate ``tgt`` cube dimension. + + """ from iris.cube import Cube src_cube = self._src_cube tgt_cube = self._tgt_cube + assert src_cube.ndim == len(self.mapping) + # Use the mapping to calculate the new src cube shape. new_src_shape = [1] * tgt_cube.ndim for src_dim, tgt_dim in self.mapping.items(): @@ -430,6 +469,40 @@ def _aux_coverage( common_aux_metadata, common_scalar_metadata, ): + """ + Determine the dimensions covered by each of the local and common + auxiliary coordinates of the provided :class:`~iris.cube.Cube`. + + The cube dimensions not covered by any of the auxiliary coordinates is + also determined; these are known as `free` dimensions. + + The scalar coordinates local to the cube are also determined. + + Args: + + * cube: + The :class:`~iris.cube.Cube` to be analysed for coverage. + + * cube_items_aux: + The list of associated :class:`~iris.common.resolve._Item` metadata + for each auxiliary coordinate owned by the cube. + + * cube_items_scalar: + The list of associated :class:`~iris.common.resolve._Item` metadata + for each scalar coordinate owned by the cube. + + * common_aux_metadata: + The list of common auxiliary coordinate metadata shared by both + the LHS and RHS cube operands being resolved. + + * common_scalar_metadata: + The list of common scalar coordinate metadata shared by both + the LHS and RHS cube operands being resolved. + + Returns: + :class:`~iris.common.resolve._AuxCoverage` + + """ common_items_aux = [] common_items_scalar = [] local_items_aux = [] @@ -465,7 +538,33 @@ def _aux_coverage( dims_free=sorted(dims_free), ) - def _aux_mapping(self, src_coverage, tgt_coverage): + @staticmethod + def _aux_mapping(src_coverage, tgt_coverage): + """ + Establish the mapping of dimensions from the ``src`` to ``tgt`` + :class:`~iris.cube.Cube` using the auxiliary coordinate metadata + common between each of the operands. + + The ``src`` to ``tgt`` common auxiliary coordinate mapping is held by + the :attr:`~iris.common.resolve.Resolve.mapping`. + + Args: + + * src_coverage: + The :class:`~iris.common.resolve._DimCoverage` of the ``src`` + :class:`~iris.cube.Cube` i.e., map from the common ``src`` + dimensions. + + * tgt_coverage: + The :class:`~iris.common.resolve._DimCoverage` of the ``tgt`` + :class:`~iris.cube.Cube` i.e., map to the common ``tgt`` + dimensions. + + Returns: + Dictionary of ``src`` to ``tgt`` dimension mapping. + + """ + mapping = {} for tgt_item in tgt_coverage.common_items_aux: # Search for a src aux metadata match. tgt_metadata = tgt_item.metadata @@ -484,7 +583,7 @@ def _aux_mapping(self, src_coverage, tgt_coverage): tgt_dims = tgt_item.dims if len(src_dims) == len(tgt_dims): for src_dim, tgt_dim in zip(src_dims, tgt_dims): - self.mapping[src_dim] = tgt_dim + mapping[src_dim] = tgt_dim logger.debug(f"{src_dim}->{tgt_dim}") else: # This situation can only occur due to a systemic internal @@ -504,9 +603,26 @@ def _aux_mapping(self, src_coverage, tgt_coverage): tgt_item.dims, ) ) + return mapping @staticmethod def _categorise_items(cube): + """ + Inspect the provided :class:`~iris.cube.Cube` and group its + coordinates and associated metadata into dimension, auxiliary and + scalar categories. + + Args: + + * cube: + The :class:`~iris.cube.Cube` that will have its coordinates and + metadata grouped into their associated dimension, auxiliary and + scalar categories. + + Returns: + :class:`~iris.common.resolve._CategoryItems` + + """ category = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) # Categorise the dim coordinates of the cube. @@ -530,15 +646,40 @@ def _categorise_items(cube): return category @staticmethod - def _create_prepared_item(coord, dims, src=None, tgt=None): - if src is not None and tgt is not None: - combined = src.combine(tgt) + def _create_prepared_item( + coord, dims, src_metadata=None, tgt_metadata=None + ): + """ + Convenience method that creates a :class:`~iris.common.resolve._PreparedItem` + containing the data and metadata required to construct and attach a coordinate + to the resultant resolved cube. + + Args: + + * coord: + The coordinate with the ``points`` and ``bounds`` to be extracted. + + * dims: + The dimensions that the ``coord`` spans on the resulting resolved :class:`~iris.cube.Cube`. + + * src_metadata: + The coordinate metadata from the ``src`` :class:`~iris.cube.Cube`. + + * tgt_metadata: + The coordinate metadata from the ``tgt`` :class:`~iris.cube.Cube`. + + Returns: + The :class:`~iris.common.resolve._PreparedItem`. + + """ + if src_metadata is not None and tgt_metadata is not None: + combined = src_metadata.combine(tgt_metadata) else: - combined = src or tgt + combined = src_metadata or tgt_metadata if not isinstance(dims, Iterable): dims = (dims,) prepared_metadata = _PreparedMetadata( - combined=combined, src=src, tgt=tgt + combined=combined, src=src_metadata, tgt=tgt_metadata ) bounds = coord.bounds result = _PreparedItem( @@ -573,6 +714,30 @@ def _show(items, heading): @staticmethod def _dim_coverage(cube, cube_items_dim, common_dim_metadata): + """ + Determine the dimensions covered by each of the local and common + dimension coordinates of the provided :class:`~iris.cube.Cube`. + + The cube dimensions not covered by any of the dimension coordinates is + also determined; these are known as `free` dimensions. + + Args: + + * cube: + The :class:`~iris.cube.Cube` to be analysed for coverage. + + * cube_items_dim: + The list of associated :class:`~iris.common.resolve._Item` metadata + for each dimension coordinate owned by the cube. + + * common_dim_metadata: + The list of common dimension coordinate metadata shared by both + the LHS and RHS cube operands being resolved. + + Returns: + :class:`~iris.common.resolve._DimCoverage` + + """ ndim = cube.ndim metadata = [None] * ndim coords = [None] * ndim @@ -599,13 +764,39 @@ def _dim_coverage(cube, cube_items_dim, common_dim_metadata): dims_free=sorted(dims_free), ) - def _dim_mapping(self, src_coverage, tgt_coverage): + @staticmethod + def _dim_mapping(src_coverage, tgt_coverage): + """ + Establish the mapping of dimensions from the ``src`` to ``tgt`` + :class:`~iris.cube.Cube` using the dimension coordinate metadata + common between each of the operands. + + The ``src`` to ``tgt`` common dimension coordinate mapping is held by + the :attr:`~iris.common.resolve.Resolve.mapping`. + + Args: + + * src_coverage: + The :class:`~iris.common.resolve._DimCoverage` of the ``src`` + :class:`~iris.cube.Cube` i.e., map from the common ``src`` + dimensions. + + * tgt_coverage: + The :class:`~iris.common.resolve._DimCoverage` of the ``tgt`` + :class:`~iris.cube.Cube` i.e., map to the common ``tgt`` + dimensions. + + Returns: + Dictionary of ``src`` to ``tgt`` dimension mapping. + + """ + mapping = {} for tgt_dim in tgt_coverage.dims_common: # Search for a src dim metadata match. tgt_metadata = tgt_coverage.metadata[tgt_dim] try: src_dim = src_coverage.metadata.index(tgt_metadata) - self.mapping[src_dim] = tgt_dim + mapping[src_dim] = tgt_dim logger.debug(f"{src_dim}->{tgt_dim}") except ValueError: # This exception can only occur due to a systemic internal @@ -621,9 +812,10 @@ def _dim_mapping(self, src_coverage, tgt_coverage): src_coverage.cube.name(), tgt_coverage.cube.name(), tgt_metadata, - tuple([tgt_dim]), + (tgt_dim,), ) ) + return mapping def _free_mapping( self, @@ -632,6 +824,57 @@ def _free_mapping( src_aux_coverage, tgt_aux_coverage, ): + """ + Attempt to update the :attr:`~iris.common.resolve.Resolve.mapping` with + ``src`` to ``tgt`` :class:`~iris.cube.Cube` mappings from unmapped ``src`` + dimensions that are free from coordinate metadata coverage to ``tgt`` + dimensions that have local metadata coverage (i.e., is not common between + the ``src`` and ``tgt``) or dimensions that are free from coordinate + metadata coverage. + + If the ``src`` :class:`~iris.cube.Cube` does not have any free dimensions, + the attempt to map unmapped ``tgt`` dimensions that have local metadata + coverage to ``src`` dimensions that are free from coordinate metadata + coverage. + + An exception will be raised if there are any ``src`` :class:`~iris.cube.Cube` + dimensions not mapped to an associated ``tgt`` dimension. + + Args: + + * src_dim_coverage: + The :class:`~iris.common.resolve.._DimCoverage` of the ``src`` + :class:`~iris.cube.Cube`. + + * tgt_dim_coverage: + The :class:`~iris.common.resolve.._DimCoverage` of the ``tgt`` + :class:`~iris.cube.Cube`. + + * src_aux_coverage: + The :class:`~iris.common.resolve._AuxCoverage` of the ``src`` + :class:`~iris.cube.Cube`. + + * tgt_aux_coverage: + The :class:`~iris.common.resolve._AuxCoverage` of the ``tgt`` + :class:`~iris.cube.Cube`. + + .. note:: + + All unmapped dimensions with an extend >1 are mapped before those + with an extent of 1, as such dimensions cannot be broadcast. It + is important to map specific non-broadcastable dimensions before + generic broadcastable dimensions otherwise we are open to failing to + map all the src dimensions as a generic src broadcast dimension has + been mapped to the only tgt dimension that a specific non-broadcastable + dimension can be mapped to. + + .. note:: + + A local dimension cannot be mapped to another local dimension, + by definition, otherwise this dimension would be classed as a + common dimension. + + """ src_cube = src_dim_coverage.cube tgt_cube = tgt_dim_coverage.cube src_ndim = src_cube.ndim @@ -663,11 +906,16 @@ def _free_mapping( tgt_shape = tgt_cube.shape src_max, tgt_max = max(src_shape), max(tgt_shape) - def assign_mapping(extent, unmapped_local_items, free_items=None): + def _assign_mapping(extent, unmapped_local_items, free_items=None): result = None if free_items is None: free_items = [] if extent == 1: + # Map to the first available unmapped local dimension or + # the first available free dimension. + # Dimension shape doesn't matter here as the extent is 1, + # therefore broadcasting will take care of any discrepency + # between src and tgt dimension extent. if unmapped_local_items: result, _ = unmapped_local_items.pop(0) elif free_items: @@ -680,10 +928,10 @@ def _filter(items): ) def _pop(item, items): - result, _ = item + dim, _ = item index = items.index(item) items.pop(index) - return result + return dim items = _filter(unmapped_local_items) if items: @@ -700,11 +948,12 @@ def _pop(item, items): (dim, tgt_shape[dim]) for dim in tgt_unmapped_local ] tgt_free_items = [(dim, tgt_shape[dim]) for dim in tgt_free] + # Sort by decreasing src dimension extent and increasing src dimension + # as we want broadcast src dimensions to be mapped last. + src_key_func = lambda dim: (src_max - src_shape[dim], dim) - for src_dim in sorted( - src_free, key=lambda dim: (src_max - src_shape[dim], dim) - ): - tgt_dim = assign_mapping( + for src_dim in sorted(src_free, key=src_key_func): + tgt_dim = _assign_mapping( src_shape[src_dim], tgt_unmapped_local_items, tgt_free_items, @@ -725,11 +974,12 @@ def _pop(item, items): src_unmapped_local_items = [ (dim, src_shape[dim]) for dim in src_unmapped_local ] + # Sort by decreasing tgt dimension extent and increasing tgt dimension + # as we want broadcast tgt dimensions to be mapped last. + tgt_key_func = lambda dim: (tgt_max - tgt_shape[dim], dim) - for tgt_dim in sorted( - tgt_free, key=lambda dim: (tgt_max - tgt_shape[dim], dim) - ): - src_dim = assign_mapping( + for tgt_dim in sorted(tgt_free, key=tgt_key_func): + src_dim = _assign_mapping( tgt_shape[tgt_dim], src_unmapped_local_items ) if src_dim is not None: @@ -758,6 +1008,17 @@ def _pop(item, items): logger.debug(f"mapping free dimensions gives, mapping={self.mapping}") def _metadata_coverage(self): + """ + Using the pre-categorised metadata of the cubes, determine the dimensions + covered by their associated dimension and auxiliary coordinates, and which + dimensions are free of metadata coverage. + + This coverage analysis clarifies how the dimensions covered by common + metadata are related, thus establishing a dimensional mapping between + the cubes. It also identifies the dimensions covered by metadata that + is local to each cube, and indeed which dimensions are free of metadata. + + """ # Determine the common dim coordinate metadata coverage. common_dim_metadata = [ item.metadata for item in self.category_common.items_dim @@ -798,6 +1059,37 @@ def _metadata_coverage(self): ) def _metadata_mapping(self): + """ + Ensure that each ``src`` :class:`~iris.cube.Cube` dimension is mapped to an associated + ``tgt`` :class:`~iris.cube.Cube` dimension using the common dim and aux coordinate metadata. + + If the common metadata does not result in a full mapping of ``src`` to ``tgt`` dimensions + then free dimensions are analysed to determine whether the mapping can be completed. + + Once the ``src`` has been mapped to the ``tgt``, the cubes are checked to ensure that they + will successfully broadcast, and the ``src`` :class:`~iris.cube.Cube` is transposed appropriately, + if necessary. + + The :attr:`~iris.common.resolve.Resolve._broadcast_shape` is set, along with the + :attr:`~iris.common.resolve.Resolve._src_cube_resolved` and :attr:`~iris.common.resolve.Resolve._tgt_cube_resolved`, + which are the broadcast/transposed ``src`` and ``tgt``. + + .. note:: + + An exception will be raised if a ``src`` dimension cannot be mapped to a ``tgt`` dimension. + + .. note:: + + An exception will be raised if the full mapped ``src`` :class:`~iris.cube.Cube` cannot be + broadcast or transposed with the ``tgt`` :class:`~iris.cube.Cube`. + + .. note:: + + The ``src`` and ``tgt`` may be swapped in the case where they both have equal dimensionality and + the ``tgt`` does have the same shape as the resolved broadcast shape (and the ``src`` does) or + the ``tgt`` has more free dimensions than the ``src``. + + """ # Initialise the state. self.mapping = {} @@ -819,7 +1111,9 @@ def _metadata_mapping(self): # Use the dim coordinates to fully map the # src cube dimensions to the tgt cube dimensions. - self._dim_mapping(src_dim_coverage, tgt_dim_coverage) + self.mapping.update( + self._dim_mapping(src_dim_coverage, tgt_dim_coverage) + ) logger.debug( f"mapping common dim coordinates gives, mapping={self.mapping}" ) @@ -827,7 +1121,9 @@ def _metadata_mapping(self): # If necessary, use the aux coordinates to fully map the # src cube dimensions to the tgt cube dimensions. if not self.mapped: - self._aux_mapping(src_aux_coverage, tgt_aux_coverage) + self.mapping.update( + self._aux_mapping(src_aux_coverage, tgt_aux_coverage) + ) logger.debug( f"mapping common aux coordinates, mapping={self.mapping}" ) @@ -886,6 +1182,12 @@ def _metadata_mapping(self): self._as_compatible_cubes() def _metadata_prepare(self): + """ + Populate the :attr:`~iris.common.resolve.Resolve.prepared_category` and + :attr:`~iris.common.resolve.Resolve.prepared_factories` with the necessary metadata to be constructed + and attached to the resulting resolved :class:`~iris.cube.Cube`. + + """ # Initialise the state. self.prepared_category = _CategoryItems( items_dim=[], items_aux=[], items_scalar=[] @@ -1053,6 +1355,41 @@ def _prepare_common_aux_payload( prepared_items, ignore_mismatch=None, ): + """ + Populate the ``prepared_items`` with a :class:`~iris.common.resolve._PreparedItem` containing + the necessary metadata for each auxiliary coordinate to be constructed and attached to the + resulting resolved :class:`~iris.cube.Cube`. + + .. note:: + + For mixed ``src`` and ``tgt`` coordinate types with matching metadata, an + :class:`~iris.coords.AuxCoord` will be nominated for construction. + + Args: + + * src_common_items: + The list of :attr:`~iris.common.resolve._AuxCoverage.common_items_aux` metadata + for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_common_items: + The list of :attr:`~iris.common.resolve._AuxCoverage.common_items_aux` metadata + for the ``tgt`` :class:`~iris.cube.Cube`. + + * prepared_items: + The list of :class:`~iris.common.resolve._PreparedItem` metadata that will be used + to construct the auxiliary coordinates that will be attached to the resulting + resolved :class:`~iris.cube.Cube`. + + Kwargs: + + * ignore_mismatch: + When ``False``, an exception will be raised if a difference is detected between corresponding + ``src`` and ``tgt`` coordinate ``points`` and/or ``bounds``. + When ``True``, the coverage metadata is ignored i.e., a coordinate will not be constructed and + added to the resulting resolved :class:`~iris.cube.Cube`. + Defaults to ``False``. + + """ from iris.coords import AuxCoord if ignore_mismatch is None: @@ -1115,6 +1452,30 @@ def _prepare_common_aux_payload( def _prepare_common_dim_payload( self, src_coverage, tgt_coverage, ignore_mismatch=None ): + """ + Populate the ``items_dim`` member of :attr:`~iris.common.resolve.Resolve.prepared_category_items` + with a :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata for + each :class:`~iris.coords.DimCoord` to be constructed and attached to the resulting resolved + :class:`~iris.cube.Cube`. + + Args: + + * src_coverage: + The :class:`~iris.common.resolve._DimCoverage` metadata for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_coverage: + The :class:`~iris.common.resolve._DimCoverage` metadata for the ``tgt`` :class:`~iris.cube.Cube`. + + Kwargs: + + * ignore_mismatch: + When ``False``, an exception will be raised if a difference is detected between corresponding + ``src`` and ``tgt`` :class:`~iris.coords.DimCoord` ``points`` and/or ``bounds``. + When ``True``, the coverage metadata is ignored i.e., a :class:`~iris.coords.DimCoord` will not + be constructed and added to the resulting resolved :class:`~iris.cube.Cube`. + Defaults to ``False``. + + """ from iris.coords import DimCoord if ignore_mismatch is None: @@ -1153,55 +1514,123 @@ def _prepare_common_dim_payload( ) self.prepared_category.items_dim.append(prepared_item) - def _prepare_factory_payload(self, cube, category_local, from_src=True): - def _get_prepared_item(metadata, from_src=True, from_local=False): - result = None - if from_local: - category = category_local - match = lambda item: item.metadata == metadata + def _get_prepared_item( + self, metadata, category_local, from_src=True, from_local=False + ): + """ + Find the :attr:`~iris.common.resolve._PreparedItem` from the + :attr:`~iris.common.resolve.Resolve.prepared_category` that matches the provided ``metadata``. + + Alternatively, the ``category_local`` is searched to find a :class:`~iris.common.resolve._Item` + with matching ``metadata`` from either the local ``src`` or ``tgt`` :class:`~iris.cube.Cube`. + If a match is found, then a new `~iris.common.resolve._PreparedItem` is created and added to + :attr:`~iris.common.resolve.Resolve.prepared_category` and returned. See ``from_local``. + + Args: + + * metadata: + The target metadata of the prepared (or local) item to retrieve. + + * category_local: + The :class:`~iris.common.resolve._CategoryItems` containing the + local metadata of either the ``src`` or ``tgt`` :class:`~iris.cube.Cube`. + See ``from_local``. + + Kwargs: + + * from_src: + Boolean stating whether the ``metadata`` is from the ``src`` (``True``) + or ``tgt`` :class:`~iris.cube.Cube`. + Defaults to ``True``. + + * from_local: + Boolean controlling whether the ``metadata`` is used to search the + ``category_local`` (``True``) or the :attr:`~iris.common.resolve.Resolve.prepared_category`. + Defaults to ``False``. + + Returns: + The :class:`~iris.common.resolve._PreparedItem` matching the provided ``metadata``. + + """ + result = None + + if from_local: + category = category_local + match = lambda item: item.metadata == metadata + else: + category = self.prepared_category + if from_src: + match = lambda item: item.metadata.src == metadata else: - category = self.prepared_category - if from_src: - match = lambda item: item.metadata.src == metadata + match = lambda item: item.metadata.tgt == metadata + + for member in category._fields: + category_items = getattr(category, member) + matched_items = tuple(filter(match, category_items)) + if matched_items: + if len(matched_items) > 1: + dmsg = ( + f"ignoring factory dependency {metadata}, multiple {'src' if from_src else 'tgt'} " + f"{'local' if from_local else 'prepared'} metadata matches" + ) + logger.debug(dmsg) else: - match = lambda item: item.metadata.tgt == metadata - for member in category._fields: - category_items = getattr(category, member) - matched_items = tuple(filter(match, category_items)) - if matched_items: - if len(matched_items) > 1: - dmsg = ( - f"ignoring factory dependency {metadata}, multiple {'src' if from_src else 'tgt'} " - f"{'local' if from_local else 'prepared'} metadata matches" - ) - logger.debug(dmsg) - else: - (item,) = matched_items - if from_local: - src = tgt = None - if from_src: - src = item.metadata - dims = tuple( - [self.mapping[dim] for dim in item.dims] - ) - else: - tgt = item.metadata - dims = item.dims - result = self._create_prepared_item( - item.coord, dims, src=src, tgt=tgt - ) - getattr(self.prepared_category, member).append( - result + (item,) = matched_items + if from_local: + src = tgt = None + if from_src: + src = item.metadata + dims = tuple( + [self.mapping[dim] for dim in item.dims] ) else: - result = item - break - return result + tgt = item.metadata + dims = item.dims + result = self._create_prepared_item( + item.coord, + dims, + src_metadata=src, + tgt_metadata=tgt, + ) + getattr(self.prepared_category, member).append(result) + else: + result = item + break + return result + + def _prepare_factory_payload(self, cube, category_local, from_src=True): + """ + Populate the :attr:`~iris.common.resolve.Resolve.prepared_factories` with a :class:`~iris.common.resolve._PreparedFactory` + containing the necessary metadata for each ``src`` and/or ``tgt`` auxiliary factory to be constructed and + attached to the resulting resolved :class:`~iris.cube.Cube`. + + .. note:: + + The required dependencies of an auxiliary factory may not all be available in the + :attr:`~iris.common.resolve.Resolve.prepared_category` and therefore this is a legitimate + reason to add the associated metadata of the local dependency to the ``prepared_category``. + + Args: + + * cube: + The :class:`~iris.cube.Cube` that may contain an auxiliary factory to be prepared. + + * category_local: + The :class:`~iris.common.resolve._CategoryItems` of all metadata local to the provided ``cube``. + Kwargs: + + * from_src: + Boolean stating whether the provided ``cube`` is either a ``src`` or ``tgt`` + :class:`~iris.cube.Cube` - used to retrieve the appropriate metadata from a + :class:`~iris.common.resolve._PreparedMetadata`. + + """ for factory in cube.aux_factories: container = type(factory) dependencies = {} prepared_item = None + found = True if tuple( filter( @@ -1222,18 +1651,24 @@ def _get_prepared_item(metadata, from_src=True, from_local=False): dependency_coord, ) in factory.dependencies.items(): metadata = dependency_coord.metadata - prepared_item = _get_prepared_item(metadata, from_src=from_src) + prepared_item = self._get_prepared_item( + metadata, category_local, from_src=from_src + ) if prepared_item is None: - prepared_item = _get_prepared_item( - metadata, from_src=from_src, from_local=True + prepared_item = self._get_prepared_item( + metadata, + category_local, + from_src=from_src, + from_local=True, ) if prepared_item is None: dmsg = f"cannot find matching {metadata} for {container} dependency {dependency_name}" logger.debug(dmsg) + found = False break dependencies[dependency_name] = prepared_item.metadata - if prepared_item is not None: + if found and prepared_item is not None: prepared_factory = _PreparedFactory( container=container, dependencies=dependencies ) @@ -1243,6 +1678,29 @@ def _get_prepared_item(metadata, from_src=True, from_local=False): logger.debug(dmsg) def _prepare_local_payload_aux(self, src_aux_coverage, tgt_aux_coverage): + """ + Populate the ``items_aux`` member of :attr:`~iris.common.resolve.Resolve.prepared_category_items` + with a :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata for each + ``src`` or ``tgt`` local auxiliary coordinate to be constructed and attached to the resulting + resolved :class:`~iris.cube.Cube`. + + .. note:: + + In general, lenient behaviour subscribes to the philosophy that it is easier to remove + metadata than it is to find then add metadata. To those ends, lenient behaviour supports + metadata richness by adding both local ``src`` and ``tgt`` auxiliary coordinates. + Alternatively, strict behaviour will only add a ``tgt`` local auxiliary coordinate that + spans dimensions not mapped to by the ``src`` e.g., extra ``tgt`` dimensions. + + Args: + + * src_aux_coverage: + The :class:`~iris.common.resolve.Resolve._AuxCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_aux_coverage: + The :class:~iris.common.resolve.Resolve._AuxCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + """ # Determine whether there are tgt dimensions not mapped to by an # associated src dimension, and thus may be covered by any local # tgt aux coordinates. @@ -1259,7 +1717,7 @@ def _prepare_local_payload_aux(self, src_aux_coverage, tgt_aux_coverage): if all([dim in mapped_src_dims for dim in item.dims]): tgt_dims = tuple([self.mapping[dim] for dim in item.dims]) prepared_item = self._create_prepared_item( - item.coord, tgt_dims, src=item.metadata + item.coord, tgt_dims, src_metadata=item.metadata ) self.prepared_category.items_aux.append(prepared_item) else: @@ -1281,7 +1739,7 @@ def _prepare_local_payload_aux(self, src_aux_coverage, tgt_aux_coverage): [dim in extra_tgt_dims for dim in tgt_dims] ): prepared_item = self._create_prepared_item( - item.coord, tgt_dims, tgt=item.metadata + item.coord, tgt_dims, tgt_metadata=item.metadata ) self.prepared_category.items_aux.append(prepared_item) else: @@ -1293,6 +1751,28 @@ def _prepare_local_payload_aux(self, src_aux_coverage, tgt_aux_coverage): logger.debug(dmsg) def _prepare_local_payload_dim(self, src_dim_coverage, tgt_dim_coverage): + """ + Populate the ``items_dim`` member of :attr:`~iris.common.resolve.Resolve.prepared_category_items` + with a :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata for each + ``src`` or ``tgt`` local :class:`~iris.coords.DimCoord` to be constructed and attached to the + resulting resolved :class:`~iris.cube.Cube`. + + .. note:: + + In general, a local coordinate will only be added if there is no other metadata competing + to describe the same dimension/s on the ``tgt`` :class:`~iris.cube.Cube`. Lenient behaviour + is more liberal, whereas strict behaviour will only add a local ``tgt`` coordinate covering + an unmapped "extra" ``tgt`` dimension/s. + + Args: + + * src_dim_coverage: + The :class:`~iris.common.resolve.Resolve._DimCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_dim_coverage: + The :class:`~iris.common.resolve.Resolve._DimCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + """ mapped_tgt_dims = self.mapping.values() # Determine whether there are tgt dimensions not mapped to by an @@ -1314,7 +1794,7 @@ def _prepare_local_payload_dim(self, src_dim_coverage, tgt_dim_coverage): metadata = src_dim_coverage.metadata[src_dim] coord = src_dim_coverage.coords[src_dim] prepared_item = self._create_prepared_item( - coord, tgt_dim, src=metadata + coord, tgt_dim, src_metadata=metadata ) self.prepared_category.items_dim.append(prepared_item) else: @@ -1347,13 +1827,36 @@ def _prepare_local_payload_dim(self, src_dim_coverage, tgt_dim_coverage): if metadata is not None: coord = tgt_dim_coverage.coords[tgt_dim] prepared_item = self._create_prepared_item( - coord, tgt_dim, tgt=metadata + coord, tgt_dim, tgt_metadata=metadata ) self.prepared_category.items_dim.append(prepared_item) def _prepare_local_payload_scalar( self, src_aux_coverage, tgt_aux_coverage ): + """ + Populate the ``items_scalar`` member of :attr:`~iris.common.resolve.Resolve.prepared_category_items` + with a :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata for each + ``src`` or ``tgt`` local scalar coordinate to be constructed and attached to the resulting + resolved :class:`~iris.cube.Cube`. + + .. note:: + + In general, lenient behaviour subscribes to the philosophy that it is easier to remove + metadata than it is to find then add metadata. To those ends, lenient behaviour supports + metadata richness by adding both local ``src`` and ``tgt`` scalar coordinates. + Alternatively, strict behaviour will only add a ``tgt`` local scalar coordinate when the + ``src`` is a scalar :class:`~iris.cube.Cube` with no local scalar coordinates. + + Args: + + * src_aux_coverage: + The :class:`~iris.common.resolve.Resolve._AuxCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_aux_coverage: + The :class:~iris.common.resolve.Resolve._AuxCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + """ # Add all local tgt scalar coordinates iff the src cube is a # scalar cube with no local src scalar coordinates. # Only for strict maths. @@ -1367,14 +1870,14 @@ def _prepare_local_payload_scalar( # Add any local src scalar coordinates, if available. for item in src_aux_coverage.local_items_scalar: prepared_item = self._create_prepared_item( - item.coord, item.dims, src=item.metadata + item.coord, item.dims, src_metadata=item.metadata ) self.prepared_category.items_scalar.append(prepared_item) # Add any local tgt scalar coordinates, if available. for item in tgt_aux_coverage.local_items_scalar: prepared_item = self._create_prepared_item( - item.coord, item.dims, tgt=item.metadata + item.coord, item.dims, tgt_metadata=item.metadata ) self.prepared_category.items_scalar.append(prepared_item) @@ -1385,6 +1888,27 @@ def _prepare_local_payload( tgt_dim_coverage, tgt_aux_coverage, ): + """ + Populate the :attr:`~iris.common.resolve.Resolve.prepared_category_items` with a + :class:`~iris.common.resolve._PreparedItem` containing the necessary metadata from the ``src`` + and/or ``tgt`` :class:`~iris.cube.Cube` for each coordinate to be constructed and attached + to the resulting resolved :class:`~iris.cube.Cube`. + + Args: + + * src_dim_coverage: + The :class:`~iris.common.resolve.Resolve._DimCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * src_aux_coverage: + The :class:`~iris.common.resolve.Resolve._AuxCoverage` for the ``src`` :class:`~iris.cube.Cube`. + + * tgt_dim_coverage: + The :class:`~iris.common.resolve.Resolve._DimCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + * tgt_aux_coverage: + The :class:~iris.common.resolve.Resolve._AuxCoverage` for the ``tgt`` :class:`~iris.cube.Cube`. + + """ # Add local src/tgt dim coordinates. self._prepare_local_payload_dim(src_dim_coverage, tgt_dim_coverage) @@ -1397,6 +1921,47 @@ def _prepare_local_payload( def _prepare_points_and_bounds( self, src_coord, tgt_coord, src_dims, tgt_dims, ignore_mismatch=None ): + """ + Compare the points and bounds of the ``src`` and ``tgt`` coordinates to ensure + that they are equivalent, taking into account broadcasting when appropriate. + + .. note:: + + An exception will be raised if the ``src`` and ``tgt`` coordinates cannot + be broadcast. + + .. note:: + + An exception will be raised if either the points or bounds are different, + however appropriate lenient behaviour concessions are applied. + + Args: + + * src_coord: + The ``src`` :class:`~iris.cube.Cube` coordinate with metadata matching + the ``tgt_coord``. + + * tgt_coord: + The ``tgt`` :class`~iris.cube.Cube` coordinate with metadata matching + the ``src_coord``. + + * src_dims: + The dimension/s of the ``src_coord`` attached to the ``src`` :class:`~iris.cube.Cube`. + + * tgt_dims: + The dimension/s of the ``tgt_coord`` attached to the ``tgt`` :class:`~iris.cube.Cube`. + + Kwargs: + + * ignore_mismatch: + For lenient behaviour only, don't raise an exception if there is a difference between + the ``src`` and ``tgt`` coordinate points or bounds. + Defaults to ``False``. + + Returns: + Tuple of equivalent ``points`` and ``bounds``, otherwise ``None``. + + """ from iris.util import array_equal if ignore_mismatch is None: @@ -1443,6 +2008,7 @@ def _prepare_points_and_bounds( tgt_broadcasting = tgt_shape != tgt_shape_broadcast if src_broadcasting and tgt_broadcasting: + # TBD: Extend capability to support attempting to broadcast two-way multi-dimensional coordinates. emsg = ( f"Cannot broadcast the coordinate {src_coord.name()!r} on " f"{self._src_cube_position} cube {self._src_cube.name()!r} and " diff --git a/lib/iris/tests/unit/common/resolve/__init__.py b/lib/iris/tests/unit/common/resolve/__init__.py new file mode 100644 index 0000000000..d0b189e59d --- /dev/null +++ b/lib/iris/tests/unit/common/resolve/__init__.py @@ -0,0 +1,6 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +"""Unit tests for the :mod:`iris.common.resolve` package.""" diff --git a/lib/iris/tests/unit/common/resolve/test_Resolve.py b/lib/iris/tests/unit/common/resolve/test_Resolve.py new file mode 100644 index 0000000000..94ec48de88 --- /dev/null +++ b/lib/iris/tests/unit/common/resolve/test_Resolve.py @@ -0,0 +1,4795 @@ +# Copyright Iris contributors +# +# This file is part of Iris and is released under the LGPL license. +# See COPYING and COPYING.LESSER in the root of the repository for full +# licensing details. +""" +Unit tests for the :class:`iris.common.resolve.Resolve`. + +""" + +# Import iris.tests first so that some things can be initialised before +# importing anything else. +import iris.tests as tests + +from collections import namedtuple +from copy import deepcopy + +from cf_units import Unit +import numpy as np +import unittest.mock as mock +from unittest.mock import sentinel + +from iris.common.lenient import LENIENT +from iris.common.metadata import CubeMetadata +from iris.common.resolve import ( + Resolve, + _AuxCoverage, + _CategoryItems, + _DimCoverage, + _Item, + _PreparedItem, + _PreparedFactory, + _PreparedMetadata, +) +from iris.coords import DimCoord +from iris.cube import Cube + + +class Test___init__(tests.IrisTest): + def setUp(self): + target = "iris.common.resolve.Resolve.__call__" + self.m_call = mock.MagicMock(return_value=sentinel.return_value) + _ = self.patch(target, new=self.m_call) + + def _assert_members_none(self, resolve): + self.assertIsNone(resolve.lhs_cube_resolved) + self.assertIsNone(resolve.rhs_cube_resolved) + self.assertIsNone(resolve.lhs_cube_category) + self.assertIsNone(resolve.rhs_cube_category) + self.assertIsNone(resolve.lhs_cube_category_local) + self.assertIsNone(resolve.rhs_cube_category_local) + self.assertIsNone(resolve.category_common) + self.assertIsNone(resolve.lhs_cube_dim_coverage) + self.assertIsNone(resolve.lhs_cube_aux_coverage) + self.assertIsNone(resolve.rhs_cube_dim_coverage) + self.assertIsNone(resolve.rhs_cube_aux_coverage) + self.assertIsNone(resolve.map_rhs_to_lhs) + self.assertIsNone(resolve.mapping) + self.assertIsNone(resolve.prepared_category) + self.assertIsNone(resolve.prepared_factories) + self.assertIsNone(resolve._broadcast_shape) + + def test_lhs_rhs_default(self): + resolve = Resolve() + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self._assert_members_none(resolve) + self.assertEqual(0, self.m_call.call_count) + + def test_lhs_rhs_provided(self): + m_lhs = sentinel.lhs + m_rhs = sentinel.rhs + resolve = Resolve(lhs=m_lhs, rhs=m_rhs) + # The lhs_cube and rhs_cube are only None due + # to __call__ being mocked. See Test___call__ + # for appropriate test coverage. + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self._assert_members_none(resolve) + self.assertEqual(1, self.m_call.call_count) + call_args = mock.call(m_lhs, m_rhs) + self.assertEqual(call_args, self.m_call.call_args) + + +class Test___call__(tests.IrisTest): + def setUp(self): + self.m_lhs = mock.MagicMock(spec=Cube) + self.m_rhs = mock.MagicMock(spec=Cube) + target = "iris.common.resolve.Resolve.{method}" + method = target.format(method="_metadata_resolve") + self.m_metadata_resolve = self.patch(method) + method = target.format(method="_metadata_coverage") + self.m_metadata_coverage = self.patch(method) + method = target.format(method="_metadata_mapping") + self.m_metadata_mapping = self.patch(method) + method = target.format(method="_metadata_prepare") + self.m_metadata_prepare = self.patch(method) + + def test_lhs_not_cube(self): + emsg = "'LHS' argument to be a 'Cube'" + with self.assertRaisesRegex(TypeError, emsg): + _ = Resolve(rhs=self.m_rhs) + + def test_rhs_not_cube(self): + emsg = "'RHS' argument to be a 'Cube'" + with self.assertRaisesRegex(TypeError, emsg): + _ = Resolve(lhs=self.m_lhs) + + def _assert_called_metadata_methods(self): + call_args = mock.call() + self.assertEqual(1, self.m_metadata_resolve.call_count) + self.assertEqual(call_args, self.m_metadata_resolve.call_args) + self.assertEqual(1, self.m_metadata_coverage.call_count) + self.assertEqual(call_args, self.m_metadata_coverage.call_args) + self.assertEqual(1, self.m_metadata_mapping.call_count) + self.assertEqual(call_args, self.m_metadata_mapping.call_args) + self.assertEqual(1, self.m_metadata_prepare.call_count) + self.assertEqual(call_args, self.m_metadata_prepare.call_args) + + def test_map_rhs_to_lhs__less_than(self): + self.m_lhs.ndim = 2 + self.m_rhs.ndim = 1 + resolve = Resolve(lhs=self.m_lhs, rhs=self.m_rhs) + self.assertEqual(self.m_lhs, resolve.lhs_cube) + self.assertEqual(self.m_rhs, resolve.rhs_cube) + self.assertTrue(resolve.map_rhs_to_lhs) + self._assert_called_metadata_methods() + + def test_map_rhs_to_lhs__equal(self): + self.m_lhs.ndim = 2 + self.m_rhs.ndim = 2 + resolve = Resolve(lhs=self.m_lhs, rhs=self.m_rhs) + self.assertEqual(self.m_lhs, resolve.lhs_cube) + self.assertEqual(self.m_rhs, resolve.rhs_cube) + self.assertTrue(resolve.map_rhs_to_lhs) + self._assert_called_metadata_methods() + + def test_map_lhs_to_rhs(self): + self.m_lhs.ndim = 2 + self.m_rhs.ndim = 3 + resolve = Resolve(lhs=self.m_lhs, rhs=self.m_rhs) + self.assertEqual(self.m_lhs, resolve.lhs_cube) + self.assertEqual(self.m_rhs, resolve.rhs_cube) + self.assertFalse(resolve.map_rhs_to_lhs) + self._assert_called_metadata_methods() + + +class Test__categorise_items(tests.IrisTest): + def setUp(self): + self.coord_dims = {} + # configure dim coords + coord = mock.Mock(metadata=sentinel.dim_metadata1) + self.dim_coords = [coord] + self.coord_dims[coord] = sentinel.dims1 + # configure aux and scalar coords + self.aux_coords = [] + pairs = [ + (sentinel.aux_metadata2, sentinel.dims2), + (sentinel.aux_metadata3, sentinel.dims3), + (sentinel.scalar_metadata4, None), + (sentinel.scalar_metadata5, None), + (sentinel.scalar_metadata6, None), + ] + for metadata, dims in pairs: + coord = mock.Mock(metadata=metadata) + self.aux_coords.append(coord) + self.coord_dims[coord] = dims + func = lambda coord: self.coord_dims[coord] + self.cube = mock.Mock( + aux_coords=self.aux_coords, + dim_coords=self.dim_coords, + coord_dims=func, + ) + + def test(self): + result = Resolve._categorise_items(self.cube) + self.assertIsInstance(result, _CategoryItems) + self.assertEqual(1, len(result.items_dim)) + # check dim coords + for item in result.items_dim: + self.assertIsInstance(item, _Item) + (coord,) = self.dim_coords + dims = self.coord_dims[coord] + expected = [_Item(metadata=coord.metadata, coord=coord, dims=dims)] + self.assertEqual(expected, result.items_dim) + # check aux coords + self.assertEqual(2, len(result.items_aux)) + for item in result.items_aux: + self.assertIsInstance(item, _Item) + expected_aux, expected_scalar = [], [] + for coord in self.aux_coords: + dims = self.coord_dims[coord] + item = _Item(metadata=coord.metadata, coord=coord, dims=dims) + if dims: + expected_aux.append(item) + else: + expected_scalar.append(item) + self.assertEqual(expected_aux, result.items_aux) + # check scalar coords + self.assertEqual(3, len(result.items_scalar)) + for item in result.items_scalar: + self.assertIsInstance(item, _Item) + self.assertEqual(expected_scalar, result.items_scalar) + + +class Test__metadata_resolve(tests.IrisTest): + def setUp(self): + self.target = "iris.common.resolve.Resolve._categorise_items" + self.m_lhs_cube = sentinel.lhs_cube + self.m_rhs_cube = sentinel.rhs_cube + + @staticmethod + def _create_items(pairs): + # this wrapper (hack) is necessary in order to support mocking + # the "name" method (callable) of the metadata, as "name" is already + # part of the mock API - this is always troublesome in mock-world. + Wrapper = namedtuple("Wrapper", ("name", "value")) + result = [] + for name, dims in pairs: + metadata = Wrapper(name=lambda: str(name), value=name) + coord = mock.Mock(metadata=metadata) + item = _Item(metadata=metadata, coord=coord, dims=dims) + result.append(item) + return result + + def test_metadata_same(self): + category = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) + # configure dim coords + pairs = [(sentinel.dim_metadata1, sentinel.dims1)] + category.items_dim.extend(self._create_items(pairs)) + # configure aux coords + pairs = [ + (sentinel.aux_metadata1, sentinel.dims2), + (sentinel.aux_metadata2, sentinel.dims3), + ] + category.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + pairs = [ + (sentinel.scalar_metadata1, None), + (sentinel.scalar_metadata2, None), + (sentinel.scalar_metadata3, None), + ] + category.items_scalar.extend(self._create_items(pairs)) + + side_effect = (category, category) + mocker = self.patch(self.target, side_effect=side_effect) + + resolve = Resolve() + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self.assertIsNone(resolve.lhs_cube_category) + self.assertIsNone(resolve.rhs_cube_category) + self.assertIsNone(resolve.lhs_cube_category_local) + self.assertIsNone(resolve.rhs_cube_category_local) + self.assertIsNone(resolve.category_common) + + # require to explicitly configure cubes + resolve.lhs_cube = self.m_lhs_cube + resolve.rhs_cube = self.m_rhs_cube + resolve._metadata_resolve() + + self.assertEqual(mocker.call_count, 2) + calls = [mock.call(self.m_lhs_cube), mock.call(self.m_rhs_cube)] + self.assertEqual(calls, mocker.call_args_list) + + self.assertEqual(category, resolve.lhs_cube_category) + self.assertEqual(category, resolve.rhs_cube_category) + expected = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) + self.assertEqual(expected, resolve.lhs_cube_category_local) + self.assertEqual(expected, resolve.rhs_cube_category_local) + self.assertEqual(category, resolve.category_common) + + def test_metadata_overlap(self): + # configure the lhs cube category + category_lhs = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # configure dim coords + pairs = [ + (sentinel.dim_metadata1, sentinel.dims1), + (sentinel.dim_metadata2, sentinel.dims2), + ] + category_lhs.items_dim.extend(self._create_items(pairs)) + # configure aux coords + pairs = [ + (sentinel.aux_metadata1, sentinel.dims3), + (sentinel.aux_metadata2, sentinel.dims4), + ] + category_lhs.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + pairs = [ + (sentinel.scalar_metadata1, None), + (sentinel.scalar_metadata2, None), + ] + category_lhs.items_scalar.extend(self._create_items(pairs)) + + # configure the rhs cube category + category_rhs = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # configure dim coords + category_rhs.items_dim.append(category_lhs.items_dim[0]) + pairs = [(sentinel.dim_metadata200, sentinel.dims2)] + category_rhs.items_dim.extend(self._create_items(pairs)) + # configure aux coords + category_rhs.items_aux.append(category_lhs.items_aux[0]) + pairs = [(sentinel.aux_metadata200, sentinel.dims4)] + category_rhs.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + category_rhs.items_scalar.append(category_lhs.items_scalar[0]) + pairs = [(sentinel.scalar_metadata200, None)] + category_rhs.items_scalar.extend(self._create_items(pairs)) + + side_effect = (category_lhs, category_rhs) + mocker = self.patch(self.target, side_effect=side_effect) + + resolve = Resolve() + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self.assertIsNone(resolve.lhs_cube_category) + self.assertIsNone(resolve.rhs_cube_category) + self.assertIsNone(resolve.lhs_cube_category_local) + self.assertIsNone(resolve.rhs_cube_category_local) + self.assertIsNone(resolve.category_common) + + # require to explicitly configure cubes + resolve.lhs_cube = self.m_lhs_cube + resolve.rhs_cube = self.m_rhs_cube + resolve._metadata_resolve() + + self.assertEqual(2, mocker.call_count) + calls = [mock.call(self.m_lhs_cube), mock.call(self.m_rhs_cube)] + self.assertEqual(calls, mocker.call_args_list) + + self.assertEqual(category_lhs, resolve.lhs_cube_category) + self.assertEqual(category_rhs, resolve.rhs_cube_category) + + items_dim = [category_lhs.items_dim[1]] + items_aux = [category_lhs.items_aux[1]] + items_scalar = [category_lhs.items_scalar[1]] + expected = _CategoryItems( + items_dim=items_dim, items_aux=items_aux, items_scalar=items_scalar + ) + self.assertEqual(expected, resolve.lhs_cube_category_local) + + items_dim = [category_rhs.items_dim[1]] + items_aux = [category_rhs.items_aux[1]] + items_scalar = [category_rhs.items_scalar[1]] + expected = _CategoryItems( + items_dim=items_dim, items_aux=items_aux, items_scalar=items_scalar + ) + self.assertEqual(expected, resolve.rhs_cube_category_local) + + items_dim = [category_lhs.items_dim[0]] + items_aux = [category_lhs.items_aux[0]] + items_scalar = [category_lhs.items_scalar[0]] + expected = _CategoryItems( + items_dim=items_dim, items_aux=items_aux, items_scalar=items_scalar + ) + self.assertEqual(expected, resolve.category_common) + + def test_metadata_different(self): + # configure the lhs cube category + category_lhs = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # configure dim coords + pairs = [ + (sentinel.dim_metadata1, sentinel.dims1), + (sentinel.dim_metadata2, sentinel.dims2), + ] + category_lhs.items_dim.extend(self._create_items(pairs)) + # configure aux coords + pairs = [ + (sentinel.aux_metadata1, sentinel.dims3), + (sentinel.aux_metadata2, sentinel.dims4), + ] + category_lhs.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + pairs = [ + (sentinel.scalar_metadata1, None), + (sentinel.scalar_metadata2, None), + ] + category_lhs.items_scalar.extend(self._create_items(pairs)) + + # configure the rhs cube category + category_rhs = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # configure dim coords + pairs = [ + (sentinel.dim_metadata100, sentinel.dims1), + (sentinel.dim_metadata200, sentinel.dims2), + ] + category_rhs.items_dim.extend(self._create_items(pairs)) + # configure aux coords + pairs = [ + (sentinel.aux_metadata100, sentinel.dims3), + (sentinel.aux_metadata200, sentinel.dims4), + ] + category_rhs.items_aux.extend(self._create_items(pairs)) + # configure scalar coords + pairs = [ + (sentinel.scalar_metadata100, None), + (sentinel.scalar_metadata200, None), + ] + category_rhs.items_scalar.extend(self._create_items(pairs)) + + side_effect = (category_lhs, category_rhs) + mocker = self.patch(self.target, side_effect=side_effect) + + resolve = Resolve() + self.assertIsNone(resolve.lhs_cube) + self.assertIsNone(resolve.rhs_cube) + self.assertIsNone(resolve.lhs_cube_category) + self.assertIsNone(resolve.rhs_cube_category) + self.assertIsNone(resolve.lhs_cube_category_local) + self.assertIsNone(resolve.rhs_cube_category_local) + self.assertIsNone(resolve.category_common) + + # first require to explicitly lhs/rhs configure cubes + resolve.lhs_cube = self.m_lhs_cube + resolve.rhs_cube = self.m_rhs_cube + resolve._metadata_resolve() + + self.assertEqual(2, mocker.call_count) + calls = [mock.call(self.m_lhs_cube), mock.call(self.m_rhs_cube)] + self.assertEqual(calls, mocker.call_args_list) + + self.assertEqual(category_lhs, resolve.lhs_cube_category) + self.assertEqual(category_rhs, resolve.rhs_cube_category) + self.assertEqual(category_lhs, resolve.lhs_cube_category_local) + self.assertEqual(category_rhs, resolve.rhs_cube_category_local) + expected = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) + self.assertEqual(expected, resolve.category_common) + + +class Test__dim_coverage(tests.IrisTest): + def setUp(self): + self.ndim = 4 + self.cube = mock.Mock(ndim=self.ndim) + self.items = [] + parts = [ + (sentinel.metadata0, sentinel.coord0, (0,)), + (sentinel.metadata1, sentinel.coord1, (1,)), + (sentinel.metadata2, sentinel.coord2, (2,)), + (sentinel.metadata3, sentinel.coord3, (3,)), + ] + column_parts = [x for x in zip(*parts)] + self.metadata, self.coords, self.dims = [list(x) for x in column_parts] + self.dims = [dim for dim, in self.dims] + for metadata, coord, dims in parts: + item = _Item(metadata=metadata, coord=coord, dims=dims) + self.items.append(item) + + def test_coverage_no_local_no_common_all_free(self): + items = [] + common = [] + result = Resolve._dim_coverage(self.cube, items, common) + self.assertIsInstance(result, _DimCoverage) + self.assertEqual(self.cube, result.cube) + expected = [None] * self.ndim + self.assertEqual(expected, result.metadata) + self.assertEqual(expected, result.coords) + self.assertEqual([], result.dims_common) + self.assertEqual([], result.dims_local) + expected = list(range(self.ndim)) + self.assertEqual(expected, result.dims_free) + + def test_coverage_all_local_no_common_no_free(self): + common = [] + result = Resolve._dim_coverage(self.cube, self.items, common) + self.assertIsInstance(result, _DimCoverage) + self.assertEqual(self.cube, result.cube) + self.assertEqual(self.metadata, result.metadata) + self.assertEqual(self.coords, result.coords) + self.assertEqual([], result.dims_common) + self.assertEqual(self.dims, result.dims_local) + self.assertEqual([], result.dims_free) + + def test_coverage_no_local_all_common_no_free(self): + result = Resolve._dim_coverage(self.cube, self.items, self.metadata) + self.assertIsInstance(result, _DimCoverage) + self.assertEqual(self.cube, result.cube) + self.assertEqual(self.metadata, result.metadata) + self.assertEqual(self.coords, result.coords) + self.assertEqual(self.dims, result.dims_common) + self.assertEqual([], result.dims_local) + self.assertEqual([], result.dims_free) + + def test_coverage_mixed(self): + common = [self.items[1].metadata, self.items[2].metadata] + self.items.pop(0) + self.items.pop(-1) + metadata, coord, dims = sentinel.metadata100, sentinel.coord100, (0,) + self.items.append(_Item(metadata=metadata, coord=coord, dims=dims)) + result = Resolve._dim_coverage(self.cube, self.items, common) + self.assertIsInstance(result, _DimCoverage) + self.assertEqual(self.cube, result.cube) + expected = [ + metadata, + self.items[0].metadata, + self.items[1].metadata, + None, + ] + self.assertEqual(expected, result.metadata) + expected = [coord, self.items[0].coord, self.items[1].coord, None] + self.assertEqual(expected, result.coords) + self.assertEqual([1, 2], result.dims_common) + self.assertEqual([0], result.dims_local) + self.assertEqual([3], result.dims_free) + + +class Test__aux_coverage(tests.IrisTest): + def setUp(self): + self.ndim = 4 + self.cube = mock.Mock(ndim=self.ndim) + # configure aux coords + self.items_aux = [] + aux_parts = [ + (sentinel.aux_metadata0, sentinel.aux_coord0, (0,)), + (sentinel.aux_metadata1, sentinel.aux_coord1, (1,)), + (sentinel.aux_metadata23, sentinel.aux_coord23, (2, 3)), + ] + column_aux_parts = [x for x in zip(*aux_parts)] + self.aux_metadata, self.aux_coords, self.aux_dims = [ + list(x) for x in column_aux_parts + ] + for metadata, coord, dims in aux_parts: + item = _Item(metadata=metadata, coord=coord, dims=dims) + self.items_aux.append(item) + # configure scalar coords + self.items_scalar = [] + scalar_parts = [ + (sentinel.scalar_metadata0, sentinel.scalar_coord0, ()), + (sentinel.scalar_metadata1, sentinel.scalar_coord1, ()), + (sentinel.scalar_metadata2, sentinel.scalar_coord2, ()), + ] + column_scalar_parts = [x for x in zip(*scalar_parts)] + self.scalar_metadata, self.scalar_coords, self.scalar_dims = [ + list(x) for x in column_scalar_parts + ] + for metadata, coord, dims in scalar_parts: + item = _Item(metadata=metadata, coord=coord, dims=dims) + self.items_scalar.append(item) + + def test_coverage_no_local_no_common_all_free(self): + items_aux, items_scalar = [], [] + common_aux, common_scalar = [], [] + result = Resolve._aux_coverage( + self.cube, items_aux, items_scalar, common_aux, common_scalar + ) + self.assertIsInstance(result, _AuxCoverage) + self.assertEqual(self.cube, result.cube) + self.assertEqual([], result.common_items_aux) + self.assertEqual([], result.common_items_scalar) + self.assertEqual([], result.local_items_aux) + self.assertEqual([], result.local_items_scalar) + self.assertEqual([], result.dims_common) + self.assertEqual([], result.dims_local) + expected = list(range(self.ndim)) + self.assertEqual(expected, result.dims_free) + + def test_coverage_all_local_no_common_no_free(self): + common_aux, common_scalar = [], [] + result = Resolve._aux_coverage( + self.cube, + self.items_aux, + self.items_scalar, + common_aux, + common_scalar, + ) + self.assertIsInstance(result, _AuxCoverage) + self.assertEqual(self.cube, result.cube) + expected = [] + self.assertEqual(expected, result.common_items_aux) + self.assertEqual(expected, result.common_items_scalar) + self.assertEqual(self.items_aux, result.local_items_aux) + self.assertEqual(self.items_scalar, result.local_items_scalar) + self.assertEqual([], result.dims_common) + expected = list(range(self.ndim)) + self.assertEqual(expected, result.dims_local) + self.assertEqual([], result.dims_free) + + def test_coverage_no_local_all_common_no_free(self): + result = Resolve._aux_coverage( + self.cube, + self.items_aux, + self.items_scalar, + self.aux_metadata, + self.scalar_metadata, + ) + self.assertIsInstance(result, _AuxCoverage) + self.assertEqual(self.cube, result.cube) + self.assertEqual(self.items_aux, result.common_items_aux) + self.assertEqual(self.items_scalar, result.common_items_scalar) + self.assertEqual([], result.local_items_aux) + self.assertEqual([], result.local_items_scalar) + expected = list(range(self.ndim)) + self.assertEqual(expected, result.dims_common) + self.assertEqual([], result.dims_local) + self.assertEqual([], result.dims_free) + + def test_coverage_mixed(self): + common_aux = [self.items_aux[-1].metadata] + common_scalar = [self.items_scalar[1].metadata] + self.items_aux.pop(1) + result = Resolve._aux_coverage( + self.cube, + self.items_aux, + self.items_scalar, + common_aux, + common_scalar, + ) + self.assertIsInstance(result, _AuxCoverage) + self.assertEqual(self.cube, result.cube) + expected = [self.items_aux[-1]] + self.assertEqual(expected, result.common_items_aux) + expected = [self.items_scalar[1]] + self.assertEqual(expected, result.common_items_scalar) + expected = [self.items_aux[0]] + self.assertEqual(expected, result.local_items_aux) + expected = [self.items_scalar[0], self.items_scalar[2]] + self.assertEqual(expected, result.local_items_scalar) + self.assertEqual([2, 3], result.dims_common) + self.assertEqual([0], result.dims_local) + self.assertEqual([1], result.dims_free) + + +class Test__metadata_coverage(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.m_lhs_cube = sentinel.lhs_cube + self.resolve.lhs_cube = self.m_lhs_cube + self.m_rhs_cube = sentinel.rhs_cube + self.resolve.rhs_cube = self.m_rhs_cube + self.m_items_dim_metadata = sentinel.items_dim_metadata + self.m_items_aux_metadata = sentinel.items_aux_metadata + self.m_items_scalar_metadata = sentinel.items_scalar_metadata + items_dim = [mock.Mock(metadata=self.m_items_dim_metadata)] + items_aux = [mock.Mock(metadata=self.m_items_aux_metadata)] + items_scalar = [mock.Mock(metadata=self.m_items_scalar_metadata)] + category = _CategoryItems( + items_dim=items_dim, items_aux=items_aux, items_scalar=items_scalar + ) + self.resolve.category_common = category + self.m_items_dim = sentinel.items_dim + self.m_items_aux = sentinel.items_aux + self.m_items_scalar = sentinel.items_scalar + category = _CategoryItems( + items_dim=self.m_items_dim, + items_aux=self.m_items_aux, + items_scalar=self.m_items_scalar, + ) + self.resolve.lhs_cube_category = category + self.resolve.rhs_cube_category = category + target = "iris.common.resolve.Resolve._dim_coverage" + self.m_lhs_cube_dim_coverage = sentinel.lhs_cube_dim_coverage + self.m_rhs_cube_dim_coverage = sentinel.rhs_cube_dim_coverage + side_effect = ( + self.m_lhs_cube_dim_coverage, + self.m_rhs_cube_dim_coverage, + ) + self.mocker_dim_coverage = self.patch(target, side_effect=side_effect) + target = "iris.common.resolve.Resolve._aux_coverage" + self.m_lhs_cube_aux_coverage = sentinel.lhs_cube_aux_coverage + self.m_rhs_cube_aux_coverage = sentinel.rhs_cube_aux_coverage + side_effect = ( + self.m_lhs_cube_aux_coverage, + self.m_rhs_cube_aux_coverage, + ) + self.mocker_aux_coverage = self.patch(target, side_effect=side_effect) + + def test(self): + self.resolve._metadata_coverage() + self.assertEqual(2, self.mocker_dim_coverage.call_count) + calls = [ + mock.call( + self.m_lhs_cube, self.m_items_dim, [self.m_items_dim_metadata] + ), + mock.call( + self.m_rhs_cube, self.m_items_dim, [self.m_items_dim_metadata] + ), + ] + self.assertEqual(calls, self.mocker_dim_coverage.call_args_list) + self.assertEqual(2, self.mocker_aux_coverage.call_count) + calls = [ + mock.call( + self.m_lhs_cube, + self.m_items_aux, + self.m_items_scalar, + [self.m_items_aux_metadata], + [self.m_items_scalar_metadata], + ), + mock.call( + self.m_rhs_cube, + self.m_items_aux, + self.m_items_scalar, + [self.m_items_aux_metadata], + [self.m_items_scalar_metadata], + ), + ] + self.assertEqual(calls, self.mocker_aux_coverage.call_args_list) + self.assertEqual( + self.m_lhs_cube_dim_coverage, self.resolve.lhs_cube_dim_coverage + ) + self.assertEqual( + self.m_rhs_cube_dim_coverage, self.resolve.rhs_cube_dim_coverage + ) + self.assertEqual( + self.m_lhs_cube_aux_coverage, self.resolve.lhs_cube_aux_coverage + ) + self.assertEqual( + self.m_rhs_cube_aux_coverage, self.resolve.rhs_cube_aux_coverage + ) + + +class Test__dim_mapping(tests.IrisTest): + def setUp(self): + self.ndim = 3 + Wrapper = namedtuple("Wrapper", ("name",)) + cube = Wrapper(name=lambda: sentinel.name) + self.src_coverage = _DimCoverage( + cube=cube, + metadata=[], + coords=None, + dims_common=None, + dims_local=None, + dims_free=None, + ) + self.tgt_coverage = _DimCoverage( + cube=cube, + metadata=[], + coords=None, + dims_common=[], + dims_local=None, + dims_free=None, + ) + self.metadata = [ + sentinel.metadata_0, + sentinel.metadata_1, + sentinel.metadata_2, + ] + self.dummy = [sentinel.dummy_0, sentinel.dummy_1, sentinel.dummy_2] + + def test_no_mapping(self): + self.src_coverage.metadata.extend(self.metadata) + self.tgt_coverage.metadata.extend(self.dummy) + result = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + self.assertEqual(dict(), result) + + def test_full_mapping(self): + self.src_coverage.metadata.extend(self.metadata) + self.tgt_coverage.metadata.extend(self.metadata) + dims_common = list(range(self.ndim)) + self.tgt_coverage.dims_common.extend(dims_common) + result = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 0, 1: 1, 2: 2} + self.assertEqual(expected, result) + + def test_transpose_mapping(self): + self.src_coverage.metadata.extend(self.metadata[::-1]) + self.tgt_coverage.metadata.extend(self.metadata) + dims_common = list(range(self.ndim)) + self.tgt_coverage.dims_common.extend(dims_common) + result = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 2, 1: 1, 2: 0} + self.assertEqual(expected, result) + + def test_partial_mapping__transposed(self): + self.src_coverage.metadata.extend(self.metadata) + self.metadata[1] = sentinel.nope + self.tgt_coverage.metadata.extend(self.metadata[::-1]) + dims_common = [0, 2] + self.tgt_coverage.dims_common.extend(dims_common) + result = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 2, 2: 0} + self.assertEqual(expected, result) + + def test_bad_metadata_mapping(self): + self.src_coverage.metadata.extend(self.metadata) + self.metadata[0] = sentinel.bad + self.tgt_coverage.metadata.extend(self.metadata) + dims_common = [0] + self.tgt_coverage.dims_common.extend(dims_common) + emsg = "Failed to map common dim coordinate metadata" + with self.assertRaisesRegex(ValueError, emsg): + _ = Resolve._dim_mapping(self.src_coverage, self.tgt_coverage) + + +class Test__aux_mapping(tests.IrisTest): + def setUp(self): + self.ndim = 3 + Wrapper = namedtuple("Wrapper", ("name",)) + cube = Wrapper(name=lambda: sentinel.name) + self.src_coverage = _AuxCoverage( + cube=cube, + common_items_aux=[], + common_items_scalar=None, + local_items_aux=None, + local_items_scalar=None, + dims_common=None, + dims_local=None, + dims_free=None, + ) + self.tgt_coverage = _AuxCoverage( + cube=cube, + common_items_aux=[], + common_items_scalar=None, + local_items_aux=None, + local_items_scalar=None, + dims_common=None, + dims_local=None, + dims_free=None, + ) + self.items = [ + _Item( + metadata=sentinel.metadata0, coord=sentinel.coord0, dims=[0] + ), + _Item( + metadata=sentinel.metadata1, coord=sentinel.coord1, dims=[1] + ), + _Item( + metadata=sentinel.metadata2, coord=sentinel.coord2, dims=[2] + ), + ] + + def _copy(self, items): + # Due to a bug in python 3.6.x, performing a deepcopy of a mock.sentinel + # will yield an object that is not equivalent to its parent, so this + # is a work-around until we drop support for python 3.6.x. + import sys + + version = sys.version_info + major, minor = version.major, version.minor + result = deepcopy(items) + if major == 3 and minor <= 6: + for i, item in enumerate(items): + result[i] = result[i]._replace(metadata=item.metadata) + return result + + def test_no_mapping(self): + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + self.assertEqual(dict(), result) + + def test_full_mapping(self): + self.src_coverage.common_items_aux.extend(self.items) + self.tgt_coverage.common_items_aux.extend(self.items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 0, 1: 1, 2: 2} + self.assertEqual(expected, result) + + def test_transpose_mapping(self): + self.src_coverage.common_items_aux.extend(self.items) + items = self._copy(self.items) + items[0].dims[0] = 2 + items[2].dims[0] = 0 + self.tgt_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 2, 1: 1, 2: 0} + self.assertEqual(expected, result) + + def test_partial_mapping__transposed(self): + _ = self.items.pop(1) + self.src_coverage.common_items_aux.extend(self.items) + items = self._copy(self.items) + items[0].dims[0] = 2 + items[1].dims[0] = 0 + self.tgt_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 2, 2: 0} + self.assertEqual(expected, result) + + def test_mapping__match_multiple_src_metadata(self): + items = self._copy(self.items) + _ = self.items.pop(1) + self.src_coverage.common_items_aux.extend(self.items) + items[1] = items[0] + self.tgt_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 0, 2: 2} + self.assertEqual(expected, result) + + def test_mapping__skip_match_multiple_src_metadata(self): + items = self._copy(self.items) + _ = self.items.pop(1) + self.tgt_coverage.common_items_aux.extend(self.items) + items[1] = items[0]._replace(dims=[1]) + self.src_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {2: 2} + self.assertEqual(expected, result) + + def test_mapping__skip_different_rank(self): + items = self._copy(self.items) + self.src_coverage.common_items_aux.extend(self.items) + items[2] = items[2]._replace(dims=[1, 2]) + self.tgt_coverage.common_items_aux.extend(items) + result = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + expected = {0: 0, 1: 1} + self.assertEqual(expected, result) + + def test_bad_metadata_mapping(self): + self.src_coverage.common_items_aux.extend(self.items) + items = self._copy(self.items) + items[0] = items[0]._replace(metadata=sentinel.bad) + self.tgt_coverage.common_items_aux.extend(items) + emsg = "Failed to map common aux coordinate metadata" + with self.assertRaisesRegex(ValueError, emsg): + _ = Resolve._aux_mapping(self.src_coverage, self.tgt_coverage) + + +class Test_mapped(tests.IrisTest): + def test_mapping_none(self): + resolve = Resolve() + self.assertIsNone(resolve.mapping) + self.assertIsNone(resolve.mapped) + + def test_mapped__src_cube_lhs(self): + resolve = Resolve() + lhs = mock.Mock(ndim=2) + rhs = mock.Mock(ndim=3) + resolve.lhs_cube = lhs + resolve.rhs_cube = rhs + resolve.map_rhs_to_lhs = False + resolve.mapping = {0: 0, 1: 1} + self.assertTrue(resolve.mapped) + + def test_mapped__src_cube_rhs(self): + resolve = Resolve() + lhs = mock.Mock(ndim=3) + rhs = mock.Mock(ndim=2) + resolve.lhs_cube = lhs + resolve.rhs_cube = rhs + resolve.map_rhs_to_lhs = True + resolve.mapping = {0: 0, 1: 1} + self.assertTrue(resolve.mapped) + + def test_partial_mapping(self): + resolve = Resolve() + lhs = mock.Mock(ndim=3) + rhs = mock.Mock(ndim=2) + resolve.lhs_cube = lhs + resolve.rhs_cube = rhs + resolve.map_rhs_to_lhs = True + resolve.mapping = {0: 0} + self.assertFalse(resolve.mapped) + + +class Test__free_mapping(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Wrapper", ("name", "ndim", "shape")) + self.src_dim_coverage = dict( + cube=None, + metadata=None, + coords=None, + dims_common=None, + dims_local=None, + dims_free=[], + ) + self.tgt_dim_coverage = deepcopy(self.src_dim_coverage) + self.src_aux_coverage = dict( + cube=None, + common_items_aux=None, + common_items_scalar=None, + local_items_aux=None, + local_items_scalar=None, + dims_common=None, + dims_local=None, + dims_free=[], + ) + self.tgt_aux_coverage = deepcopy(self.src_aux_coverage) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.resolve.mapping = {} + + def _make_args(self): + args = dict( + src_dim_coverage=_DimCoverage(**self.src_dim_coverage), + tgt_dim_coverage=_DimCoverage(**self.tgt_dim_coverage), + src_aux_coverage=_AuxCoverage(**self.src_aux_coverage), + tgt_aux_coverage=_AuxCoverage(**self.tgt_aux_coverage), + ) + return args + + def test_mapping_no_dims_free(self): + ndim = 4 + shape = tuple(range(ndim)) + cube = self.Cube(name=lambda: "name", ndim=ndim, shape=shape) + self.src_dim_coverage["cube"] = cube + self.tgt_dim_coverage["cube"] = cube + args = self._make_args() + emsg = "Insufficient matching coordinate metadata" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._free_mapping(**args) + + def _make_coverage(self, name, shape, dims_free): + if name == "src": + dim_coverage = self.src_dim_coverage + aux_coverage = self.src_aux_coverage + else: + dim_coverage = self.tgt_dim_coverage + aux_coverage = self.tgt_aux_coverage + ndim = len(shape) + cube = self.Cube(name=lambda: name, ndim=ndim, shape=shape) + dim_coverage["cube"] = cube + dim_coverage["dims_free"].extend(dims_free) + aux_coverage["cube"] = cube + aux_coverage["dims_free"].extend(dims_free) + + def test_mapping_src_free_to_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 4 + # state f l c l state f c f + # coord d d d a coord a d d + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_local__broadcast_src_first(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 1 3 4 + # state f l c l state f c f + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (1, 3, 4) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_local__broadcast_src_last(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 1 + # state f l c l state f c f + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 1) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_local__broadcast_src_both(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 1 3 1 + # state f l c l state f c f + # coord d d d a coord a d d + # bcast ^ ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->1 1->2 2->3 + src_shape = (1, 3, 1) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 1, 1: 2, 2: 3} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_free(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 4 + # state f f c f state f c f + # coord d d d a coord a d d + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 0, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_free__broadcast_src_first(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 1 3 4 + # state f f c f state f c f + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->1 + src_shape = (1, 3, 4) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 0, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_free__broadcast_src_last(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 1 + # state f f c f state f c f + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->1 + src_shape = (2, 3, 1) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 0, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt_free__broadcast_src_both(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 1 3 1 + # state f f c f state f c f + # coord d d d a coord a d d + # bcast ^ ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->1 + src_shape = (1, 3, 1) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 0, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_src_free_to_tgt__fail(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 5 + # state f f c f state f c f + # coord d d d a coord a d d + # fail ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->? + src_shape = (2, 3, 5) + src_free = [0, 2] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [0, 1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + emsg = "Insufficient matching coordinate metadata to resolve cubes" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._free_mapping(**args) + + def test_mapping_tgt_free_to_src_local(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 2) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_tgt_free_to_src_local__broadcast_tgt_first(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 1 3 2 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 1, 3, 2) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_tgt_free_to_src_local__broadcast_tgt_last(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 1 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # bcast ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->3 1->2 2->1 + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 1) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 3, 1: 2, 2: 1} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_tgt_free_to_src_local__broadcast_tgt_both(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 1 3 1 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # bcast ^ ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->1 1->2 2->3 + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 1, 3, 1) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + self.resolve._free_mapping(**args) + expected = {0: 1, 1: 2, 2: 3} + self.assertEqual(expected, self.resolve.mapping) + + def test_mapping_tgt_free_to_src_no_free__fail(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: -> src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 5 shape 2 3 4 + # state l f c f state l c l + # coord d d d a coord a d d + # fail ^ + # + # src-to-tgt mapping: + # before 1->2 + # after 0->0 1->2 2->? + src_shape = (2, 3, 4) + src_free = [] + self._make_coverage("src", src_shape, src_free) + tgt_shape = (2, 4, 3, 5) + tgt_free = [1, 3] + self._make_coverage("tgt", tgt_shape, tgt_free) + self.resolve.mapping = {1: 2} + args = self._make_args() + emsg = "Insufficient matching coordinate metadata to resolve cubes" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._free_mapping(**args) + + +class Test__src_cube(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.rhs_cube = self.expected + self.assertEqual(self.expected, self.resolve._src_cube) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.lhs_cube = self.expected + self.assertEqual(self.expected, self.resolve._src_cube) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._src_cube + + +class Test__src_cube_position(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.assertEqual("RHS", self.resolve._src_cube_position) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.assertEqual("LHS", self.resolve._src_cube_position) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._src_cube_position + + +class Test__src_cube_resolved__getter(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.rhs_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve._src_cube_resolved) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.lhs_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve._src_cube_resolved) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._src_cube_resolved + + +class Test__src_cube_resolved__setter(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve._src_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve.rhs_cube_resolved) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve._src_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve.lhs_cube_resolved) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._src_cube_resolved = self.expected + + +class Test__tgt_cube(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.rhs_cube = self.expected + self.assertEqual(self.expected, self.resolve._tgt_cube) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.lhs_cube = self.expected + self.assertEqual(self.expected, self.resolve._tgt_cube) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._tgt_cube + + +class Test__tgt_cube_position(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.assertEqual("RHS", self.resolve._tgt_cube_position) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.assertEqual("LHS", self.resolve._tgt_cube_position) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._tgt_cube_position + + +class Test__tgt_cube_resolved__getter(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.rhs_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve._tgt_cube_resolved) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.lhs_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve._tgt_cube_resolved) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._tgt_cube_resolved + + +class Test__tgt_cube_resolved__setter(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + self.expected = sentinel.cube + + def test_rhs_cube(self): + self.resolve.map_rhs_to_lhs = False + self.resolve._tgt_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve.rhs_cube_resolved) + + def test_lhs_cube(self): + self.resolve.map_rhs_to_lhs = True + self.resolve._tgt_cube_resolved = self.expected + self.assertEqual(self.expected, self.resolve.lhs_cube_resolved) + + def test_fail__no_map_rhs_to_lhs(self): + with self.assertRaises(AssertionError): + self.resolve._tgt_cube_resolved = self.expected + + +class Test_shape(tests.IrisTest): + def setUp(self): + self.resolve = Resolve() + + def test_no_shape(self): + self.assertIsNone(self.resolve.shape) + + def test_shape(self): + expected = sentinel.shape + self.resolve._broadcast_shape = expected + self.assertEqual(expected, self.resolve.shape) + + +class Test__as_compatible_cubes(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple( + "Wrapper", + ( + "name", + "ndim", + "shape", + "metadata", + "core_data", + "coord_dims", + "dim_coords", + "aux_coords", + "aux_factories", + ), + ) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.resolve.mapping = {} + self.mocker = self.patch("iris.cube.Cube") + self.args = dict( + name=None, + ndim=None, + shape=None, + metadata=None, + core_data=None, + coord_dims=None, + dim_coords=None, + aux_coords=None, + aux_factories=None, + ) + + def _make_cube(self, name, shape, transpose_shape=None): + self.args["name"] = lambda: name + ndim = len(shape) + self.args["ndim"] = ndim + self.args["shape"] = shape + if name == "src": + self.args["metadata"] = sentinel.metadata + self.reshape = sentinel.reshape + m_reshape = mock.Mock(return_value=self.reshape) + self.transpose = mock.Mock( + shape=transpose_shape, reshape=m_reshape + ) + m_transpose = mock.Mock(return_value=self.transpose) + self.data = mock.Mock( + shape=shape, transpose=m_transpose, reshape=m_reshape + ) + m_copy = mock.Mock(return_value=self.data) + m_core_data = mock.Mock(copy=m_copy) + self.args["core_data"] = mock.Mock(return_value=m_core_data) + self.args["coord_dims"] = mock.Mock(side_effect=([0], [ndim - 1])) + self.dim_coord = sentinel.dim_coord + self.aux_coord = sentinel.aux_coord + self.aux_factory = sentinel.aux_factory + self.args["dim_coords"] = [self.dim_coord] + self.args["aux_coords"] = [self.aux_coord] + self.args["aux_factories"] = [self.aux_factory] + cube = self.Cube(**self.args) + self.resolve.rhs_cube = cube + self.cube = mock.Mock() + self.mocker.return_value = self.cube + else: + cube = self.Cube(**self.args) + self.resolve.lhs_cube = cube + + def test_incomplete_src_to_tgt_mapping__fail(self): + src_shape = (1, 2) + self._make_cube("src", src_shape) + tgt_shape = (3, 4) + self._make_cube("tgt", tgt_shape) + with self.assertRaises(AssertionError): + self.resolve._as_compatible_cubes() + + def test_incompatible_shapes__fail(self): + # key: (state) c=common, f=free + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 2 3 4 shape 2 3 5 + # state f c c c state c c c + # fail ^ fail ^ + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + src_shape = (2, 3, 5) + self._make_cube("src", src_shape) + tgt_shape = (2, 2, 3, 4) + self._make_cube("tgt", tgt_shape) + self.resolve.mapping = {0: 1, 1: 2, 2: 3} + emsg = "Cannot resolve cubes" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._as_compatible_cubes() + + def test_incompatible_shapes__fail_broadcast(self): + # key: (state) c=common, f=free + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 2 4 3 2 shape 2 3 5 + # state f c c c state c c c + # fail ^ fail ^ + # + # src-to-tgt mapping: + # 0->3, 1->2, 2->1 + src_shape = (2, 3, 5) + self._make_cube("src", src_shape) + tgt_shape = (2, 4, 3, 2) + self._make_cube("tgt", tgt_shape) + self.resolve.mapping = {0: 3, 1: 2, 2: 1} + emsg = "Cannot resolve cubes" + with self.assertRaisesRegex(ValueError, emsg): + self.resolve._as_compatible_cubes() + + def _check_compatible(self, broadcast_shape): + self.assertEqual( + self.resolve.lhs_cube, self.resolve._tgt_cube_resolved + ) + self.assertEqual(self.cube, self.resolve._src_cube_resolved) + self.assertEqual(broadcast_shape, self.resolve._broadcast_shape) + self.assertEqual(1, self.mocker.call_count) + self.assertEqual(self.args["metadata"], self.cube.metadata) + self.assertEqual(2, self.resolve.rhs_cube.coord_dims.call_count) + self.assertEqual( + [mock.call(self.dim_coord), mock.call(self.aux_coord)], + self.resolve.rhs_cube.coord_dims.call_args_list, + ) + self.assertEqual(1, self.cube.add_dim_coord.call_count) + self.assertEqual( + [mock.call(self.dim_coord, [self.resolve.mapping[0]])], + self.cube.add_dim_coord.call_args_list, + ) + self.assertEqual(1, self.cube.add_aux_coord.call_count) + self.assertEqual( + [mock.call(self.aux_coord, [self.resolve.mapping[2]])], + self.cube.add_aux_coord.call_args_list, + ) + self.assertEqual(1, self.cube.add_aux_factory.call_count) + self.assertEqual( + [mock.call(self.aux_factory)], + self.cube.add_aux_factory.call_args_list, + ) + + def test_compatible(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 2 + # shape 4 3 2 shape 4 3 2 + # state c c c state c c c + # coord d a + # + # src-to-tgt mapping: + # 0->0, 1->1, 2->2 + src_shape = (4, 3, 2) + self._make_cube("src", src_shape) + tgt_shape = (4, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 0, 1: 1, 2: 2} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=tgt_shape) + self.assertEqual([mock.call(self.data)], self.mocker.call_args_list) + + def test_compatible__transpose(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 2 + # shape 4 3 2 shape 2 3 4 + # state c c c state c c c + # coord d a + # + # src-to-tgt mapping: + # 0->2, 1->1, 2->0 + src_shape = (2, 3, 4) + self._make_cube("src", src_shape, transpose_shape=(4, 3, 2)) + tgt_shape = (4, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 2, 1: 1, 2: 0} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=tgt_shape) + self.assertEqual(1, self.data.transpose.call_count) + self.assertEqual( + [mock.call([2, 1, 0])], self.data.transpose.call_args_list + ) + self.assertEqual( + [mock.call(self.transpose)], self.mocker.call_args_list + ) + + def test_compatible__reshape(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state f c c c state c c c + # coord d a + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + src_shape = (4, 3, 2) + self._make_cube("src", src_shape) + tgt_shape = (5, 4, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 1, 1: 2, 2: 3} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=tgt_shape) + self.assertEqual(1, self.data.reshape.call_count) + self.assertEqual( + [mock.call((1,) + src_shape)], self.data.reshape.call_args_list + ) + self.assertEqual([mock.call(self.reshape)], self.mocker.call_args_list) + + def test_compatible__transpose_reshape(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 2 3 4 + # state f c c c state c c c + # coord d a + # + # src-to-tgt mapping: + # 0->3, 1->2, 2->1 + src_shape = (2, 3, 4) + transpose_shape = (4, 3, 2) + self._make_cube("src", src_shape, transpose_shape=transpose_shape) + tgt_shape = (5, 4, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 3, 1: 2, 2: 1} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=tgt_shape) + self.assertEqual(1, self.data.transpose.call_count) + self.assertEqual( + [mock.call([2, 1, 0])], self.data.transpose.call_args_list + ) + self.assertEqual(1, self.data.reshape.call_count) + self.assertEqual( + [mock.call((1,) + transpose_shape)], + self.data.reshape.call_args_list, + ) + self.assertEqual([mock.call(self.reshape)], self.mocker.call_args_list) + + def test_compatible__broadcast(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 2 + # shape 1 3 2 shape 4 1 2 + # state c c c state c c c + # coord d a + # bcast ^ bcast ^ + # + # src-to-tgt mapping: + # 0->0, 1->1, 2->2 + src_shape = (4, 1, 2) + self._make_cube("src", src_shape) + tgt_shape = (1, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 0, 1: 1, 2: 2} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=(4, 3, 2)) + self.assertEqual([mock.call(self.data)], self.mocker.call_args_list) + + def test_compatible__broadcast_transpose_reshape(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 1 3 2 shape 2 1 4 + # state f c c c state c c c + # coord d a + # bcast ^ bcast ^ + # + # src-to-tgt mapping: + # 0->3, 1->2, 2->1 + src_shape = (2, 1, 4) + transpose_shape = (4, 1, 2) + self._make_cube("src", src_shape) + tgt_shape = (5, 1, 3, 2) + self._make_cube("tgt", tgt_shape) + mapping = {0: 3, 1: 2, 2: 1} + self.resolve.mapping = mapping + self.resolve._as_compatible_cubes() + self._check_compatible(broadcast_shape=(5, 4, 3, 2)) + self.assertEqual(1, self.data.transpose.call_count) + self.assertEqual( + [mock.call([2, 1, 0])], self.data.transpose.call_args_list + ) + self.assertEqual(1, self.data.reshape.call_count) + self.assertEqual( + [mock.call((1,) + transpose_shape)], + self.data.reshape.call_args_list, + ) + self.assertEqual([mock.call(self.reshape)], self.mocker.call_args_list) + + +class Test__metadata_mapping(tests.IrisTest): + def setUp(self): + self.ndim = sentinel.ndim + self.src_cube = mock.Mock(ndim=self.ndim) + self.src_dim_coverage = mock.Mock(dims_free=[]) + self.src_aux_coverage = mock.Mock(dims_free=[]) + self.tgt_cube = mock.Mock(ndim=self.ndim) + self.tgt_dim_coverage = mock.Mock(dims_free=[]) + self.tgt_aux_coverage = mock.Mock(dims_free=[]) + self.resolve = Resolve() + self.map_rhs_to_lhs = True + self.resolve.map_rhs_to_lhs = self.map_rhs_to_lhs + self.resolve.rhs_cube = self.src_cube + self.resolve.rhs_cube_dim_coverage = self.src_dim_coverage + self.resolve.rhs_cube_aux_coverage = self.src_aux_coverage + self.resolve.lhs_cube = self.tgt_cube + self.resolve.lhs_cube_dim_coverage = self.tgt_dim_coverage + self.resolve.lhs_cube_aux_coverage = self.tgt_aux_coverage + self.resolve.mapping = {} + self.shape = sentinel.shape + self.resolve._broadcast_shape = self.shape + self.resolve._src_cube_resolved = mock.Mock(shape=self.shape) + self.resolve._tgt_cube_resolved = mock.Mock(shape=self.shape) + self.m_dim_mapping = self.patch( + "iris.common.resolve.Resolve._dim_mapping", return_value={} + ) + self.m_aux_mapping = self.patch( + "iris.common.resolve.Resolve._aux_mapping", return_value={} + ) + self.m_free_mapping = self.patch( + "iris.common.resolve.Resolve._free_mapping" + ) + self.m_as_compatible_cubes = self.patch( + "iris.common.resolve.Resolve._as_compatible_cubes" + ) + self.mapping = {0: 1, 1: 2, 2: 3} + + def test_mapped__dim_coords(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state f c c c state c c c + # coord d d d coord d d d + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + self.src_cube.ndim = 3 + self.m_dim_mapping.return_value = self.mapping + self.resolve._metadata_mapping() + self.assertEqual(self.mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(0, self.m_aux_mapping.call_count) + self.assertEqual(0, self.m_free_mapping.call_count) + self.assertEqual(1, self.m_as_compatible_cubes.call_count) + + def test_mapped__aux_coords(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state f c c c state c c c + # coord a a a coord a a a + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + self.src_cube.ndim = 3 + self.m_aux_mapping.return_value = self.mapping + self.resolve._metadata_mapping() + self.assertEqual(self.mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(1, self.m_aux_mapping.call_count) + expected = [mock.call(self.src_aux_coverage, self.tgt_aux_coverage)] + self.assertEqual(expected, self.m_aux_mapping.call_args_list) + self.assertEqual(0, self.m_free_mapping.call_count) + self.assertEqual(1, self.m_as_compatible_cubes.call_count) + + def test_mapped__dim_and_aux_coords(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state f c c c state c c c + # coord d a d coord d a d + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + dim_mapping = {0: 1, 2: 3} + aux_mapping = {1: 2} + self.src_cube.ndim = 3 + self.m_dim_mapping.return_value = dim_mapping + self.m_aux_mapping.return_value = aux_mapping + self.resolve._metadata_mapping() + self.assertEqual(self.mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(1, self.m_aux_mapping.call_count) + expected = [mock.call(self.src_aux_coverage, self.tgt_aux_coverage)] + self.assertEqual(expected, self.m_aux_mapping.call_args_list) + self.assertEqual(0, self.m_free_mapping.call_count) + self.assertEqual(1, self.m_as_compatible_cubes.call_count) + + def test_mapped__dim_coords_and_free_dims(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state l f c c state f c c + # coord d d d coord d d + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + dim_mapping = {1: 2, 2: 3} + free_mapping = {0: 1} + self.src_cube.ndim = 3 + self.m_dim_mapping.return_value = dim_mapping + side_effect = lambda a, b, c, d: self.resolve.mapping.update( + free_mapping + ) + self.m_free_mapping.side_effect = side_effect + self.resolve._metadata_mapping() + self.assertEqual(self.mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(1, self.m_aux_mapping.call_count) + expected = [mock.call(self.src_aux_coverage, self.tgt_aux_coverage)] + self.assertEqual(expected, self.m_aux_mapping.call_args_list) + self.assertEqual(1, self.m_free_mapping.call_count) + expected = [ + mock.call( + self.src_dim_coverage, + self.tgt_dim_coverage, + self.src_aux_coverage, + self.tgt_aux_coverage, + ) + ] + self.assertEqual(expected, self.m_free_mapping.call_args_list) + self.assertEqual(1, self.m_as_compatible_cubes.call_count) + + def test_mapped__dim_coords_with_broadcast_flip(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 4 dims 0 1 2 4 + # shape 1 4 3 2 shape 5 4 3 2 + # state c c c c state c c c c + # coord d d d d coord d d d d + # + # src-to-tgt mapping: + # 0->0, 1->1, 2->2, 3->3 + mapping = {0: 0, 1: 1, 2: 2, 3: 3} + self.src_cube.ndim = 4 + self.tgt_cube.ndim = 4 + self.m_dim_mapping.return_value = mapping + broadcast_shape = (5, 4, 3, 2) + self.resolve._broadcast_shape = broadcast_shape + self.resolve._src_cube_resolved.shape = broadcast_shape + self.resolve._tgt_cube_resolved.shape = (1, 4, 3, 2) + self.resolve._metadata_mapping() + self.assertEqual(mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(0, self.m_aux_mapping.call_count) + self.assertEqual(0, self.m_free_mapping.call_count) + self.assertEqual(2, self.m_as_compatible_cubes.call_count) + self.assertEqual(not self.map_rhs_to_lhs, self.resolve.map_rhs_to_lhs) + + def test_mapped__dim_coords_free_flip_with_free_flip(self): + # key: (state) c=common, f=free, l=local + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 2 + # shape 4 3 2 shape 4 3 2 + # state f f c state l l c + # coord d coord d d d + # + # src-to-tgt mapping: + # 0->0, 1->1, 2->2 + dim_mapping = {2: 2} + free_mapping = {0: 0, 1: 1} + mapping = {0: 0, 1: 1, 2: 2} + self.src_cube.ndim = 3 + self.tgt_cube.ndim = 3 + self.m_dim_mapping.return_value = dim_mapping + side_effect = lambda a, b, c, d: self.resolve.mapping.update( + free_mapping + ) + self.m_free_mapping.side_effect = side_effect + self.tgt_dim_coverage.dims_free = [0, 1] + self.tgt_aux_coverage.dims_free = [0, 1] + self.resolve._metadata_mapping() + self.assertEqual(mapping, self.resolve.mapping) + self.assertEqual(1, self.m_dim_mapping.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual(expected, self.m_dim_mapping.call_args_list) + self.assertEqual(1, self.m_aux_mapping.call_count) + expected = [mock.call(self.src_aux_coverage, self.tgt_aux_coverage)] + self.assertEqual(expected, self.m_aux_mapping.call_args_list) + self.assertEqual(1, self.m_free_mapping.call_count) + expected = [ + mock.call( + self.src_dim_coverage, + self.tgt_dim_coverage, + self.src_aux_coverage, + self.tgt_aux_coverage, + ) + ] + self.assertEqual(expected, self.m_free_mapping.call_args_list) + self.assertEqual(2, self.m_as_compatible_cubes.call_count) + + +class Test__prepare_common_dim_payload(tests.IrisTest): + def setUp(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state l c c c state c c c + # coord d d d coord d d d + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + self.points = (sentinel.points_0, sentinel.points_1, sentinel.points_2) + self.bounds = (sentinel.bounds_0, sentinel.bounds_1, sentinel.bounds_2) + self.pb_0 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[0])), + mock.Mock(copy=mock.Mock(return_value=self.bounds[0])), + ) + self.pb_1 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[1])), + None, + ) + self.pb_2 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[2])), + mock.Mock(copy=mock.Mock(return_value=self.bounds[2])), + ) + side_effect = (self.pb_0, self.pb_1, self.pb_2) + self.m_prepare_points_and_bounds = self.patch( + "iris.common.resolve.Resolve._prepare_points_and_bounds", + side_effect=side_effect, + ) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.mapping = {0: 1, 1: 2, 2: 3} + self.resolve.mapping = self.mapping + self.metadata_combined = ( + sentinel.combined_0, + sentinel.combined_1, + sentinel.combined_2, + ) + self.src_metadata = mock.Mock( + combine=mock.Mock(side_effect=self.metadata_combined) + ) + metadata = [self.src_metadata] * len(self.mapping) + self.src_coords = [ + sentinel.src_coord_0, + sentinel.src_coord_1, + sentinel.src_coord_2, + ] + self.src_dims_common = [0, 1, 2] + self.container = DimCoord + self.src_dim_coverage = _DimCoverage( + cube=None, + metadata=metadata, + coords=self.src_coords, + dims_common=self.src_dims_common, + dims_local=[], + dims_free=[], + ) + self.tgt_metadata = [ + sentinel.tgt_metadata_0, + sentinel.tgt_metadata_1, + sentinel.tgt_metadata_2, + sentinel.tgt_metadata_3, + ] + self.tgt_coords = [ + sentinel.tgt_coord_0, + sentinel.tgt_coord_1, + sentinel.tgt_coord_2, + sentinel.tgt_coord_3, + ] + self.tgt_dims_common = [1, 2, 3] + self.tgt_dim_coverage = _DimCoverage( + cube=None, + metadata=self.tgt_metadata, + coords=self.tgt_coords, + dims_common=self.tgt_dims_common, + dims_local=[], + dims_free=[], + ) + + def _check(self, ignore_mismatch=None, bad_points=None): + if bad_points is None: + bad_points = False + self.resolve._prepare_common_dim_payload( + self.src_dim_coverage, + self.tgt_dim_coverage, + ignore_mismatch=ignore_mismatch, + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + if not bad_points: + self.assertEqual(3, len(self.resolve.prepared_category.items_dim)) + expected = [ + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[0], + src=self.src_metadata, + tgt=self.tgt_metadata[self.mapping[0]], + ), + points=self.points[0], + bounds=self.bounds[0], + dims=(self.mapping[0],), + container=self.container, + ), + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[1], + src=self.src_metadata, + tgt=self.tgt_metadata[self.mapping[1]], + ), + points=self.points[1], + bounds=None, + dims=(self.mapping[1],), + container=self.container, + ), + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[2], + src=self.src_metadata, + tgt=self.tgt_metadata[self.mapping[2]], + ), + points=self.points[2], + bounds=self.bounds[2], + dims=(self.mapping[2],), + container=self.container, + ), + ] + self.assertEqual( + expected, self.resolve.prepared_category.items_dim + ) + else: + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + self.assertEqual(3, self.m_prepare_points_and_bounds.call_count) + if ignore_mismatch is None: + ignore_mismatch = False + expected = [ + mock.call( + self.src_coords[0], + self.tgt_coords[self.mapping[0]], + 0, + 1, + ignore_mismatch=ignore_mismatch, + ), + mock.call( + self.src_coords[1], + self.tgt_coords[self.mapping[1]], + 1, + 2, + ignore_mismatch=ignore_mismatch, + ), + mock.call( + self.src_coords[2], + self.tgt_coords[self.mapping[2]], + 2, + 3, + ignore_mismatch=ignore_mismatch, + ), + ] + self.assertEqual( + expected, self.m_prepare_points_and_bounds.call_args_list + ) + if not bad_points: + self.assertEqual(3, self.src_metadata.combine.call_count) + expected = [ + mock.call(metadata) for metadata in self.tgt_metadata[1:] + ] + self.assertEqual( + expected, self.src_metadata.combine.call_args_list + ) + + def test__default_ignore_mismatch(self): + self._check() + + def test__not_ignore_mismatch(self): + self._check(ignore_mismatch=False) + + def test__ignore_mismatch(self): + self._check(ignore_mismatch=True) + + def test__bad_points(self): + side_effect = [(None, None)] * len(self.mapping) + self.m_prepare_points_and_bounds.side_effect = side_effect + self._check(bad_points=True) + + +class Test__prepare_common_aux_payload(tests.IrisTest): + def setUp(self): + # key: (state) c=common, f=free + # (coord) a=aux, d=dim + # + # tgt: <- src: + # dims 0 1 2 3 dims 0 1 2 + # shape 5 4 3 2 shape 4 3 2 + # state l c c c state c c c + # coord a a a coord a a a + # + # src-to-tgt mapping: + # 0->1, 1->2, 2->3 + self.points = (sentinel.points_0, sentinel.points_1, sentinel.points_2) + self.bounds = (sentinel.bounds_0, sentinel.bounds_1, sentinel.bounds_2) + self.pb_0 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[0])), + mock.Mock(copy=mock.Mock(return_value=self.bounds[0])), + ) + self.pb_1 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[1])), + None, + ) + self.pb_2 = ( + mock.Mock(copy=mock.Mock(return_value=self.points[2])), + mock.Mock(copy=mock.Mock(return_value=self.bounds[2])), + ) + side_effect = (self.pb_0, self.pb_1, self.pb_2) + self.m_prepare_points_and_bounds = self.patch( + "iris.common.resolve.Resolve._prepare_points_and_bounds", + side_effect=side_effect, + ) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.mapping = {0: 1, 1: 2, 2: 3} + self.resolve.mapping = self.mapping + self.resolve.map_rhs_to_lhs = True + self.metadata_combined = ( + sentinel.combined_0, + sentinel.combined_1, + sentinel.combined_2, + ) + self.src_metadata = [ + mock.Mock( + combine=mock.Mock(return_value=self.metadata_combined[0]) + ), + mock.Mock( + combine=mock.Mock(return_value=self.metadata_combined[1]) + ), + mock.Mock( + combine=mock.Mock(return_value=self.metadata_combined[2]) + ), + ] + self.src_coords = [ + sentinel.src_coord_0, + sentinel.src_coord_1, + sentinel.src_coord_2, + ] + self.src_dims = [(dim,) for dim in self.mapping.keys()] + self.src_common_items = [ + _Item(*item) + for item in zip(self.src_metadata, self.src_coords, self.src_dims) + ] + self.tgt_metadata = [sentinel.tgt_metadata_0] + self.src_metadata + self.tgt_coords = [ + sentinel.tgt_coord_0, + sentinel.tgt_coord_1, + sentinel.tgt_coord_2, + sentinel.tgt_coord_3, + ] + self.tgt_dims = [None] + [(dim,) for dim in self.mapping.values()] + self.tgt_common_items = [ + _Item(*item) + for item in zip(self.tgt_metadata, self.tgt_coords, self.tgt_dims) + ] + self.container = type(self.src_coords[0]) + + def _check(self, ignore_mismatch=None, bad_points=None): + if bad_points is None: + bad_points = False + prepared_items = [] + self.resolve._prepare_common_aux_payload( + self.src_common_items, + self.tgt_common_items, + prepared_items, + ignore_mismatch=ignore_mismatch, + ) + if not bad_points: + self.assertEqual(3, len(prepared_items)) + expected = [ + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[0], + src=self.src_metadata[0], + tgt=self.tgt_metadata[self.mapping[0]], + ), + points=self.points[0], + bounds=self.bounds[0], + dims=self.tgt_dims[self.mapping[0]], + container=self.container, + ), + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[1], + src=self.src_metadata[1], + tgt=self.tgt_metadata[self.mapping[1]], + ), + points=self.points[1], + bounds=None, + dims=self.tgt_dims[self.mapping[1]], + container=self.container, + ), + _PreparedItem( + metadata=_PreparedMetadata( + combined=self.metadata_combined[2], + src=self.src_metadata[2], + tgt=self.tgt_metadata[self.mapping[2]], + ), + points=self.points[2], + bounds=self.bounds[2], + dims=self.tgt_dims[self.mapping[2]], + container=self.container, + ), + ] + self.assertEqual(expected, prepared_items) + else: + self.assertEqual(0, len(prepared_items)) + self.assertEqual(3, self.m_prepare_points_and_bounds.call_count) + if ignore_mismatch is None: + ignore_mismatch = False + expected = [ + mock.call( + self.src_coords[0], + self.tgt_coords[self.mapping[0]], + self.src_dims[0], + self.tgt_dims[self.mapping[0]], + ignore_mismatch=ignore_mismatch, + ), + mock.call( + self.src_coords[1], + self.tgt_coords[self.mapping[1]], + self.src_dims[1], + self.tgt_dims[self.mapping[1]], + ignore_mismatch=ignore_mismatch, + ), + mock.call( + self.src_coords[2], + self.tgt_coords[self.mapping[2]], + self.src_dims[2], + self.tgt_dims[self.mapping[2]], + ignore_mismatch=ignore_mismatch, + ), + ] + self.assertEqual( + expected, self.m_prepare_points_and_bounds.call_args_list + ) + if not bad_points: + for src_metadata, tgt_metadata in zip( + self.src_metadata, self.tgt_metadata[1:] + ): + self.assertEqual(1, src_metadata.combine.call_count) + expected = [mock.call(tgt_metadata)] + self.assertEqual(expected, src_metadata.combine.call_args_list) + + def test__default_ignore_mismatch(self): + self._check() + + def test__not_ignore_mismatch(self): + self._check(ignore_mismatch=False) + + def test__ignore_mismatch(self): + self._check(ignore_mismatch=True) + + def test__bad_points(self): + side_effect = [(None, None)] * len(self.mapping) + self.m_prepare_points_and_bounds.side_effect = side_effect + self._check(bad_points=True) + + def test__no_tgt_metadata_match(self): + item = self.tgt_common_items[0] + tgt_common_items = [item] * len(self.tgt_common_items) + prepared_items = [] + self.resolve._prepare_common_aux_payload( + self.src_common_items, tgt_common_items, prepared_items + ) + self.assertEqual(0, len(prepared_items)) + + def test__multi_tgt_metadata_match(self): + item = self.tgt_common_items[1] + tgt_common_items = [item] * len(self.tgt_common_items) + prepared_items = [] + self.resolve._prepare_common_aux_payload( + self.src_common_items, tgt_common_items, prepared_items + ) + self.assertEqual(0, len(prepared_items)) + + +class Test__prepare_points_and_bounds(tests.IrisTest): + def setUp(self): + self.Coord = namedtuple( + "Coord", + [ + "name", + "points", + "bounds", + "metadata", + "ndim", + "shape", + "has_bounds", + ], + ) + self.Cube = namedtuple("Cube", ["name", "shape"]) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.src_name = sentinel.src_name + self.src_points = sentinel.src_points + self.src_bounds = sentinel.src_bounds + self.src_metadata = sentinel.src_metadata + self.src_items = dict( + name=lambda: self.src_name, + points=self.src_points, + bounds=self.src_bounds, + metadata=self.src_metadata, + ndim=None, + shape=None, + has_bounds=None, + ) + self.tgt_name = sentinel.tgt_name + self.tgt_points = sentinel.tgt_points + self.tgt_bounds = sentinel.tgt_bounds + self.tgt_metadata = sentinel.tgt_metadata + self.tgt_items = dict( + name=lambda: self.tgt_name, + points=self.tgt_points, + bounds=self.tgt_bounds, + metadata=self.tgt_metadata, + ndim=None, + shape=None, + has_bounds=None, + ) + self.m_array_equal = self.patch( + "iris.util.array_equal", side_effect=(True, True) + ) + + def test_coord_ndim_unequal__tgt_ndim_greater(self): + self.src_items["ndim"] = 1 + src_coord = self.Coord(**self.src_items) + self.tgt_items["ndim"] = 10 + tgt_coord = self.Coord(**self.tgt_items) + points, bounds = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims=None, tgt_dims=None + ) + self.assertEqual(self.tgt_points, points) + self.assertEqual(self.tgt_bounds, bounds) + + def test_coord_ndim_unequal__src_ndim_greater(self): + self.src_items["ndim"] = 10 + src_coord = self.Coord(**self.src_items) + self.tgt_items["ndim"] = 1 + tgt_coord = self.Coord(**self.tgt_items) + points, bounds = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims=None, tgt_dims=None + ) + self.assertEqual(self.src_points, points) + self.assertEqual(self.src_bounds, bounds) + + def test_coord_ndim_equal__shape_unequal_with_src_broadcasting(self): + # key: (state) c=common, f=free + # (coord) x=coord + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 9 9 shape 1 9 + # state c c state c c + # coord x-x coord x-x + # bcast ^ + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + broadcast_shape = (9, 9) + ndim = len(broadcast_shape) + self.resolve.mapping = mapping + self.resolve._broadcast_shape = broadcast_shape + src_shape = (1, 9) + src_dims = tuple(mapping.keys()) + self.resolve.rhs_cube = self.Cube(name=None, shape=src_shape) + self.src_items["ndim"] = ndim + self.src_items["shape"] = src_shape + src_coord = self.Coord(**self.src_items) + tgt_shape = broadcast_shape + tgt_dims = tuple(mapping.values()) + self.resolve.lhs_cube = self.Cube(name=None, shape=tgt_shape) + self.tgt_items["ndim"] = ndim + self.tgt_items["shape"] = tgt_shape + tgt_coord = self.Coord(**self.tgt_items) + points, bounds = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims, tgt_dims + ) + self.assertEqual(self.tgt_points, points) + self.assertEqual(self.tgt_bounds, bounds) + + def test_coord_ndim_equal__shape_unequal_with_tgt_broadcasting(self): + # key: (state) c=common, f=free + # (coord) x=coord + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 1 9 shape 9 9 + # state c c state c c + # coord x-x coord x-x + # bcast ^ + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + broadcast_shape = (9, 9) + ndim = len(broadcast_shape) + self.resolve.mapping = mapping + self.resolve._broadcast_shape = broadcast_shape + src_shape = broadcast_shape + src_dims = tuple(mapping.keys()) + self.resolve.rhs_cube = self.Cube(name=None, shape=src_shape) + self.src_items["ndim"] = ndim + self.src_items["shape"] = src_shape + src_coord = self.Coord(**self.src_items) + tgt_shape = (1, 9) + tgt_dims = tuple(mapping.values()) + self.resolve.lhs_cube = self.Cube(name=None, shape=tgt_shape) + self.tgt_items["ndim"] = ndim + self.tgt_items["shape"] = tgt_shape + tgt_coord = self.Coord(**self.tgt_items) + points, bounds = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims, tgt_dims + ) + self.assertEqual(self.src_points, points) + self.assertEqual(self.src_bounds, bounds) + + def test_coord_ndim_equal__shape_unequal_with_unsupported_broadcasting( + self, + ): + # key: (state) c=common, f=free + # (coord) x=coord + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 1 9 shape 9 1 + # state c c state c c + # coord x-x coord x-x + # bcast ^ bcast ^ + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + broadcast_shape = (9, 9) + ndim = len(broadcast_shape) + self.resolve.mapping = mapping + self.resolve._broadcast_shape = broadcast_shape + src_shape = (9, 1) + src_dims = tuple(mapping.keys()) + self.resolve.rhs_cube = self.Cube( + name=lambda: sentinel.src_cube, shape=src_shape + ) + self.src_items["ndim"] = ndim + self.src_items["shape"] = src_shape + src_coord = self.Coord(**self.src_items) + tgt_shape = (1, 9) + tgt_dims = tuple(mapping.values()) + self.resolve.lhs_cube = self.Cube( + name=lambda: sentinel.tgt_cube, shape=tgt_shape + ) + self.tgt_items["ndim"] = ndim + self.tgt_items["shape"] = tgt_shape + tgt_coord = self.Coord(**self.tgt_items) + emsg = "Cannot broadcast" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds( + src_coord, tgt_coord, src_dims, tgt_dims + ) + + def _populate( + self, src_points, tgt_points, src_bounds=None, tgt_bounds=None + ): + # key: (state) c=common, f=free + # (coord) x=coord + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state f c state f c + # coord x coord x + # + # src-to-tgt mapping: + # 0->0, 1->1 + shape = (2, 3) + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + self.resolve.map_rhs_to_lhs = True + self.resolve.rhs_cube = self.Cube( + name=lambda: sentinel.src_cube, shape=None + ) + self.resolve.lhs_cube = self.Cube( + name=lambda: sentinel.tgt_cube, shape=None + ) + ndim = 1 + src_dims = 1 + self.src_items["ndim"] = ndim + self.src_items["shape"] = (shape[src_dims],) + self.src_items["points"] = src_points + self.src_items["bounds"] = src_bounds + self.src_items["has_bounds"] = lambda: src_bounds is not None + src_coord = self.Coord(**self.src_items) + tgt_dims = 1 + self.tgt_items["ndim"] = ndim + self.tgt_items["shape"] = (shape[mapping[tgt_dims]],) + self.tgt_items["points"] = tgt_points + self.tgt_items["bounds"] = tgt_bounds + self.tgt_items["has_bounds"] = lambda: tgt_bounds is not None + tgt_coord = self.Coord(**self.tgt_items) + args = dict( + src_coord=src_coord, + tgt_coord=tgt_coord, + src_dims=src_dims, + tgt_dims=tgt_dims, + ) + return args + + def test_coord_ndim_and_shape_equal__points_equal_with_no_bounds(self): + args = self._populate(self.src_points, self.src_points) + points, bounds = self.resolve._prepare_points_and_bounds(**args) + self.assertEqual(self.src_points, points) + self.assertIsNone(bounds) + self.assertEqual(1, self.m_array_equal.call_count) + expected = [mock.call(self.src_points, self.src_points, withnans=True)] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_src_bounds_only( + self, + ): + args = self._populate( + self.src_points, self.src_points, src_bounds=self.src_bounds + ) + points, bounds = self.resolve._prepare_points_and_bounds(**args) + self.assertEqual(self.src_points, points) + self.assertEqual(self.src_bounds, bounds) + self.assertEqual(1, self.m_array_equal.call_count) + expected = [mock.call(self.src_points, self.src_points, withnans=True)] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_tgt_bounds_only( + self, + ): + args = self._populate( + self.src_points, self.src_points, tgt_bounds=self.tgt_bounds + ) + points, bounds = self.resolve._prepare_points_and_bounds(**args) + self.assertEqual(self.src_points, points) + self.assertEqual(self.tgt_bounds, bounds) + self.assertEqual(1, self.m_array_equal.call_count) + expected = [mock.call(self.src_points, self.src_points, withnans=True)] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_src_bounds_only_strict( + self, + ): + args = self._populate( + self.src_points, self.src_points, src_bounds=self.src_bounds + ) + with LENIENT.context(maths=False): + emsg = f"Coordinate {self.src_name} has bounds" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_equal_with_tgt_bounds_only_strict( + self, + ): + args = self._populate( + self.src_points, self.src_points, tgt_bounds=self.tgt_bounds + ) + with LENIENT.context(maths=False): + emsg = f"Coordinate {self.tgt_name} has bounds" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_equal_with_bounds_equal(self): + args = self._populate( + self.src_points, + self.src_points, + src_bounds=self.src_bounds, + tgt_bounds=self.src_bounds, + ) + points, bounds = self.resolve._prepare_points_and_bounds(**args) + self.assertEqual(self.src_points, points) + self.assertEqual(self.src_bounds, bounds) + self.assertEqual(2, self.m_array_equal.call_count) + expected = [ + mock.call(self.src_points, self.src_points, withnans=True), + mock.call(self.src_bounds, self.src_bounds, withnans=True), + ] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_bounds_different( + self, + ): + self.m_array_equal.side_effect = (True, False) + args = self._populate( + self.src_points, + self.src_points, + src_bounds=self.src_bounds, + tgt_bounds=self.tgt_bounds, + ) + emsg = f"Coordinate {self.src_name} has different bounds" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_equal_with_bounds_different_ignore_mismatch( + self, + ): + self.m_array_equal.side_effect = (True, False) + args = self._populate( + self.src_points, + self.src_points, + src_bounds=self.src_bounds, + tgt_bounds=self.tgt_bounds, + ) + points, bounds = self.resolve._prepare_points_and_bounds( + **args, ignore_mismatch=True + ) + self.assertEqual(self.src_points, points) + self.assertIsNone(bounds) + self.assertEqual(2, self.m_array_equal.call_count) + expected = [ + mock.call(self.src_points, self.src_points, withnans=True), + mock.call(self.src_bounds, self.tgt_bounds, withnans=True), + ] + self.assertEqual(expected, self.m_array_equal.call_args_list) + + def test_coord_ndim_and_shape_equal__points_equal_with_bounds_different_strict( + self, + ): + self.m_array_equal.side_effect = (True, False) + args = self._populate( + self.src_points, + self.src_points, + src_bounds=self.src_bounds, + tgt_bounds=self.tgt_bounds, + ) + with LENIENT.context(maths=False): + emsg = f"Coordinate {self.src_name} has different bounds" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_different(self): + self.m_array_equal.side_effect = (False,) + args = self._populate(self.src_points, self.tgt_points) + emsg = f"Coordinate {self.src_name} has different points" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + def test_coord_ndim_and_shape_equal__points_different_ignore_mismatch( + self, + ): + self.m_array_equal.side_effect = (False,) + args = self._populate(self.src_points, self.tgt_points) + points, bounds = self.resolve._prepare_points_and_bounds( + **args, ignore_mismatch=True + ) + self.assertIsNone(points) + self.assertIsNone(bounds) + + def test_coord_ndim_and_shape_equal__points_different_strict(self): + self.m_array_equal.side_effect = (False,) + args = self._populate(self.src_points, self.tgt_points) + with LENIENT.context(maths=False): + emsg = f"Coordinate {self.src_name} has different points" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve._prepare_points_and_bounds(**args) + + +class Test__create_prepared_item(tests.IrisTest): + def setUp(self): + Coord = namedtuple("Coord", ["points", "bounds"]) + self.points_value = sentinel.points + self.points = mock.Mock(copy=mock.Mock(return_value=self.points_value)) + self.bounds_value = sentinel.bounds + self.bounds = mock.Mock(copy=mock.Mock(return_value=self.bounds_value)) + self.coord = Coord(points=self.points, bounds=self.bounds) + self.container = type(self.coord) + self.combined = sentinel.combined + self.src = mock.Mock(combine=mock.Mock(return_value=self.combined)) + self.tgt = sentinel.tgt + + def _check(self, src=None, tgt=None): + dims = 0 + if src is not None and tgt is not None: + combined = self.combined + else: + combined = src or tgt + result = Resolve._create_prepared_item( + self.coord, dims, src_metadata=src, tgt_metadata=tgt + ) + self.assertIsInstance(result, _PreparedItem) + self.assertIsInstance(result.metadata, _PreparedMetadata) + expected = _PreparedMetadata(combined=combined, src=src, tgt=tgt) + self.assertEqual(expected, result.metadata) + self.assertEqual(self.points_value, result.points) + self.assertEqual(1, self.points.copy.call_count) + self.assertEqual([mock.call()], self.points.copy.call_args_list) + self.assertEqual(self.bounds_value, result.bounds) + self.assertEqual(1, self.bounds.copy.call_count) + self.assertEqual([mock.call()], self.bounds.copy.call_args_list) + self.assertEqual((dims,), result.dims) + self.assertEqual(self.container, result.container) + + def test__no_metadata(self): + self._check() + + def test__src_metadata_only(self): + self._check(src=self.src) + + def test__tgt_metadata_only(self): + self._check(tgt=self.tgt) + + def test__combine_metadata(self): + self._check(src=self.src, tgt=self.tgt) + + +class Test__prepare_local_payload_dim(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Cube", ["ndim"]) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.resolve.map_rhs_to_lhs = True + self.src_coverage = dict( + cube=None, + metadata=[], + coords=[], + dims_common=None, + dims_local=[], + dims_free=None, + ) + self.tgt_coverage = deepcopy(self.src_coverage) + self.prepared_item = sentinel.prepared_item + self.m_create_prepared_item = self.patch( + "iris.common.resolve.Resolve._create_prepared_item", + return_value=self.prepared_item, + ) + + def test_src_no_local_with_tgt_no_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c c state c c + # coord d d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_no_local_with_tgt_no_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c c state c c + # coord d d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_local_with_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c l + # coord d d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + self.src_coverage["dims_local"] = (1,) + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["dims_local"] = (1,) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_local_with_tgt_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c l + # coord d d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + self.src_coverage["dims_local"] = (1,) + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["dims_local"] = (1,) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_local_with_tgt_free(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c f state c l + # coord d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_dim = 1 + self.src_coverage["dims_local"] = (src_dim,) + src_metadata = sentinel.src_metadata + self.src_coverage["metadata"] = [None, src_metadata] + src_coord = sentinel.src_coord + self.src_coverage["coords"] = [None, src_coord] + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_dim)) + self.assertEqual( + self.prepared_item, self.resolve.prepared_category.items_dim[0] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + expected = [ + mock.call(src_coord, mapping[src_dim], src_metadata=src_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_free__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c f state c l + # coord d coord d d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_dim = 1 + self.src_coverage["dims_local"] = (src_dim,) + src_metadata = sentinel.src_metadata + self.src_coverage["metadata"] = [None, src_metadata] + src_coord = sentinel.src_coord + self.src_coverage["coords"] = [None, src_coord] + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_free_with_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c f + # coord d d coord d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_dim = 1 + self.tgt_coverage["dims_local"] = (tgt_dim,) + tgt_metadata = sentinel.tgt_metadata + self.tgt_coverage["metadata"] = [None, tgt_metadata] + tgt_coord = sentinel.tgt_coord + self.tgt_coverage["coords"] = [None, tgt_coord] + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_dim)) + self.assertEqual( + self.prepared_item, self.resolve.prepared_category.items_dim[0] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + expected = [mock.call(tgt_coord, tgt_dim, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_free_with_tgt_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c f + # coord d d coord d + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_dim = 1 + self.tgt_coverage["dims_local"] = (tgt_dim,) + tgt_metadata = sentinel.tgt_metadata + self.tgt_coverage["metadata"] = [None, tgt_metadata] + tgt_coord = sentinel.tgt_coord + self.tgt_coverage["coords"] = [None, tgt_coord] + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_dim)) + + def test_src_no_local_with_tgt_local__extra_dims(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 + # shape 4 2 3 shape 2 3 + # state l c c state c c + # coord d d d coord d d + # + # src-to-tgt mapping: + # 0->1, 1->2 + mapping = {0: 1, 1: 2} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=3) + tgt_dim = 0 + self.tgt_coverage["dims_local"] = (tgt_dim,) + tgt_metadata = sentinel.tgt_metadata + self.tgt_coverage["metadata"] = [tgt_metadata, None, None] + tgt_coord = sentinel.tgt_coord + self.tgt_coverage["coords"] = [tgt_coord, None, None] + tgt_coverage = _DimCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_dim)) + self.assertEqual( + self.prepared_item, self.resolve.prepared_category.items_dim[0] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + expected = [mock.call(tgt_coord, tgt_dim, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_no_local_with_tgt_local__extra_dims_strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 + # shape 4 2 3 shape 2 3 + # state l c c state c c + # coord d d d coord d d + # + # src-to-tgt mapping: + # 0->1, 1->2 + mapping = {0: 1, 1: 2} + self.resolve.mapping = mapping + src_coverage = _DimCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=3) + tgt_dim = 0 + self.tgt_coverage["dims_local"] = (tgt_dim,) + tgt_metadata = sentinel.tgt_metadata + self.tgt_coverage["metadata"] = [tgt_metadata, None, None] + tgt_coord = sentinel.tgt_coord + self.tgt_coverage["coords"] = [tgt_coord, None, None] + tgt_coverage = _DimCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_dim(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_dim)) + self.assertEqual( + self.prepared_item, self.resolve.prepared_category.items_dim[0] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + expected = [mock.call(tgt_coord, tgt_dim, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + +class Test__prepare_local_payload_aux(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Cube", ["ndim"]) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.resolve.map_rhs_to_lhs = True + self.src_coverage = dict( + cube=None, + common_items_aux=None, + common_items_scalar=None, + local_items_aux=[], + local_items_scalar=None, + dims_common=None, + dims_local=[], + dims_free=None, + ) + self.tgt_coverage = deepcopy(self.src_coverage) + self.src_prepared_item = sentinel.src_prepared_item + self.tgt_prepared_item = sentinel.tgt_prepared_item + self.m_create_prepared_item = self.patch( + "iris.common.resolve.Resolve._create_prepared_item", + side_effect=(self.src_prepared_item, self.tgt_prepared_item), + ) + + def test_src_no_local_with_tgt_no_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c c state c c + # coord a a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_no_local_with_tgt_no_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c c state c c + # coord a a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_local_with_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c l + # coord a a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_dims = (1,) + src_item = _Item(metadata=src_metadata, coord=src_coord, dims=src_dims) + self.src_coverage["local_items_aux"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (1,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(2, len(self.resolve.prepared_category.items_aux)) + expected = [self.src_prepared_item, self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [ + mock.call(src_coord, tgt_dims, src_metadata=src_metadata), + mock.call(tgt_coord, tgt_dims, tgt_metadata=tgt_metadata), + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c l + # coord a a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_dims = (1,) + src_item = _Item(metadata=src_metadata, coord=src_coord, dims=src_dims) + self.src_coverage["local_items_aux"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (1,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_local_with_tgt_free(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c f state c l + # coord a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_dims = (1,) + src_item = _Item(metadata=src_metadata, coord=src_coord, dims=src_dims) + self.src_coverage["local_items_aux"].append(src_item) + self.src_coverage["dims_local"].extend(src_dims) + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_aux)) + expected = [self.src_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [mock.call(src_coord, src_dims, src_metadata=src_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_free__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c f state c l + # coord a coord a a + # + # src-to-tgt mapping: + # 0->0, 1->1 + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_dims = (1,) + src_item = _Item(metadata=src_metadata, coord=src_coord, dims=src_dims) + self.src_coverage["local_items_aux"].append(src_item) + self.src_coverage["dims_local"].extend(src_dims) + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_free_with_tgt_local(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c f + # coord a a coord a + # + # src-to-tgt mapping: + # 0->0, 1->1 + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (1,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + self.tgt_coverage["dims_local"].extend(tgt_dims) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_aux)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [mock.call(tgt_coord, tgt_dims, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_free_with_tgt_local__strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 dims 0 1 + # shape 2 3 shape 2 3 + # state c l state c f + # coord a a coord a + # + # src-to-tgt mapping: + # 0->0, 1->1 + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + mapping = {0: 0, 1: 1} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=2) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (1,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + self.tgt_coverage["dims_local"].extend(tgt_dims) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_aux)) + + def test_src_no_local_with_tgt_local__extra_dims(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 + # shape 4 2 3 shape 2 3 + # state l c c state c c + # coord a a a coord a a + # + # src-to-tgt mapping: + # 0->1, 1->2 + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + mapping = {0: 1, 1: 2} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=3) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (0,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + self.tgt_coverage["dims_local"].extend(tgt_dims) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_aux)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [mock.call(tgt_coord, tgt_dims, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_no_local_with_tgt_local__extra_dims_strict(self): + # key: (state) c=common, f=free, l=local + # (coord) d=dim + # + # tgt: <- src: + # dims 0 1 2 dims 0 1 + # shape 4 2 3 shape 2 3 + # state l c c state c c + # coord a a a coord a a + # + # src-to-tgt mapping: + # 0->1, 1->2 + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + mapping = {0: 1, 1: 2} + self.resolve.mapping = mapping + src_coverage = _AuxCoverage(**self.src_coverage) + self.tgt_coverage["cube"] = self.Cube(ndim=3) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_dims = (0,) + tgt_item = _Item(metadata=tgt_metadata, coord=tgt_coord, dims=tgt_dims) + self.tgt_coverage["local_items_aux"].append(tgt_item) + self.tgt_coverage["dims_local"].extend(tgt_dims) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=True): + self.resolve._prepare_local_payload_aux(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_aux)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_aux) + expected = [mock.call(tgt_coord, tgt_dims, tgt_metadata=tgt_metadata)] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + +class Test__prepare_local_payload_scalar(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Cube", ["ndim"]) + self.resolve = Resolve() + self.resolve.prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + self.src_coverage = dict( + cube=None, + common_items_aux=None, + common_items_scalar=None, + local_items_aux=None, + local_items_scalar=[], + dims_common=None, + dims_local=[], + dims_free=None, + ) + self.tgt_coverage = deepcopy(self.src_coverage) + self.src_prepared_item = sentinel.src_prepared_item + self.tgt_prepared_item = sentinel.tgt_prepared_item + self.m_create_prepared_item = self.patch( + "iris.common.resolve.Resolve._create_prepared_item", + side_effect=(self.src_prepared_item, self.tgt_prepared_item), + ) + self.src_dims = () + self.tgt_dims = () + + def test_src_no_local_with_tgt_no_local(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_no_local__strict(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_no_local__src_scalar_cube(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_no_local__src_scalar_cube_strict(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_local_with_tgt_no_local(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.src_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(src_coord, self.src_dims, src_metadata=src_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_no_local__strict(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_local_with_tgt_no_local__src_scalar_cube(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.src_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(src_coord, self.src_dims, src_metadata=src_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_no_local__src_scalar_cube_strict(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_local(self): + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_no_local_with_tgt_local__strict(self): + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_no_local_with_tgt_local__src_scalar_cube(self): + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_no_local_with_tgt_local__src_scalar_cube_strict(self): + self.m_create_prepared_item.side_effect = (self.tgt_prepared_item,) + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(1, len(self.resolve.prepared_category.items_scalar)) + expected = [self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_local(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(2, len(self.resolve.prepared_category.items_scalar)) + expected = [self.src_prepared_item, self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(src_coord, self.src_dims, src_metadata=src_metadata), + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata), + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_local__strict(self): + ndim = 2 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + def test_src_local_with_tgt_local__src_scalar_cube(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + self.resolve._prepare_local_payload_scalar(src_coverage, tgt_coverage) + self.assertEqual(2, len(self.resolve.prepared_category.items_scalar)) + expected = [self.src_prepared_item, self.tgt_prepared_item] + self.assertEqual(expected, self.resolve.prepared_category.items_scalar) + expected = [ + mock.call(src_coord, self.src_dims, src_metadata=src_metadata), + mock.call(tgt_coord, self.tgt_dims, tgt_metadata=tgt_metadata), + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_src_local_with_tgt_local__src_scalar_cube_strict(self): + ndim = 0 + self.src_coverage["cube"] = self.Cube(ndim=ndim) + src_metadata = sentinel.src_metadata + src_coord = sentinel.src_coord + src_item = _Item( + metadata=src_metadata, coord=src_coord, dims=self.src_dims + ) + self.src_coverage["local_items_scalar"].append(src_item) + src_coverage = _AuxCoverage(**self.src_coverage) + tgt_metadata = sentinel.tgt_metadata + tgt_coord = sentinel.tgt_coord + tgt_item = _Item( + metadata=tgt_metadata, coord=tgt_coord, dims=self.tgt_dims + ) + self.tgt_coverage["local_items_scalar"].append(tgt_item) + tgt_coverage = _AuxCoverage(**self.tgt_coverage) + with LENIENT.context(maths=False): + self.resolve._prepare_local_payload_scalar( + src_coverage, tgt_coverage + ) + self.assertEqual(0, len(self.resolve.prepared_category.items_scalar)) + + +class Test__prepare_local_payload(tests.IrisTest): + def test(self): + src_dim_coverage = sentinel.src_dim_coverage + src_aux_coverage = sentinel.src_aux_coverage + tgt_dim_coverage = sentinel.tgt_dim_coverage + tgt_aux_coverage = sentinel.tgt_aux_coverage + root = "iris.common.resolve.Resolve" + m_prepare_dim = self.patch(f"{root}._prepare_local_payload_dim") + m_prepare_aux = self.patch(f"{root}._prepare_local_payload_aux") + m_prepare_scalar = self.patch(f"{root}._prepare_local_payload_scalar") + resolve = Resolve() + resolve._prepare_local_payload( + src_dim_coverage, + src_aux_coverage, + tgt_dim_coverage, + tgt_aux_coverage, + ) + self.assertEqual(1, m_prepare_dim.call_count) + expected = [mock.call(src_dim_coverage, tgt_dim_coverage)] + self.assertEqual(expected, m_prepare_dim.call_args_list) + self.assertEqual(1, m_prepare_aux.call_count) + expected = [mock.call(src_aux_coverage, tgt_aux_coverage)] + self.assertEqual(expected, m_prepare_aux.call_args_list) + self.assertEqual(1, m_prepare_scalar.call_count) + expected = [mock.call(src_aux_coverage, tgt_aux_coverage)] + self.assertEqual(expected, m_prepare_scalar.call_args_list) + + +class Test__metadata_prepare(tests.IrisTest): + def setUp(self): + self.src_cube = sentinel.src_cube + self.src_category_local = sentinel.src_category_local + self.src_dim_coverage = sentinel.src_dim_coverage + self.src_aux_coverage = mock.Mock( + common_items_aux=sentinel.src_aux_coverage_common_items_aux, + common_items_scalar=sentinel.src_aux_coverage_common_items_scalar, + ) + self.tgt_cube = sentinel.tgt_cube + self.tgt_category_local = sentinel.tgt_category_local + self.tgt_dim_coverage = sentinel.tgt_dim_coverage + self.tgt_aux_coverage = mock.Mock( + common_items_aux=sentinel.tgt_aux_coverage_common_items_aux, + common_items_scalar=sentinel.tgt_aux_coverage_common_items_scalar, + ) + self.resolve = Resolve() + root = "iris.common.resolve.Resolve" + self.m_prepare_common_dim_payload = self.patch( + f"{root}._prepare_common_dim_payload" + ) + self.m_prepare_common_aux_payload = self.patch( + f"{root}._prepare_common_aux_payload" + ) + self.m_prepare_local_payload = self.patch( + f"{root}._prepare_local_payload" + ) + self.m_prepare_factory_payload = self.patch( + f"{root}._prepare_factory_payload" + ) + + def _check(self): + self.assertIsNone(self.resolve.prepared_category) + self.assertIsNone(self.resolve.prepared_factories) + self.resolve._metadata_prepare() + expected = _CategoryItems(items_dim=[], items_aux=[], items_scalar=[]) + self.assertEqual(expected, self.resolve.prepared_category) + self.assertEqual([], self.resolve.prepared_factories) + self.assertEqual(1, self.m_prepare_common_dim_payload.call_count) + expected = [mock.call(self.src_dim_coverage, self.tgt_dim_coverage)] + self.assertEqual( + expected, self.m_prepare_common_dim_payload.call_args_list + ) + self.assertEqual(2, self.m_prepare_common_aux_payload.call_count) + expected = [ + mock.call( + self.src_aux_coverage.common_items_aux, + self.tgt_aux_coverage.common_items_aux, + [], + ), + mock.call( + self.src_aux_coverage.common_items_scalar, + self.tgt_aux_coverage.common_items_scalar, + [], + ignore_mismatch=True, + ), + ] + self.assertEqual( + expected, self.m_prepare_common_aux_payload.call_args_list + ) + self.assertEqual(1, self.m_prepare_local_payload.call_count) + expected = [ + mock.call( + self.src_dim_coverage, + self.src_aux_coverage, + self.tgt_dim_coverage, + self.tgt_aux_coverage, + ) + ] + self.assertEqual(expected, self.m_prepare_local_payload.call_args_list) + self.assertEqual(2, self.m_prepare_factory_payload.call_count) + expected = [ + mock.call(self.tgt_cube, self.tgt_category_local, from_src=False), + mock.call(self.src_cube, self.src_category_local), + ] + self.assertEqual( + expected, self.m_prepare_factory_payload.call_args_list + ) + + def test_map_rhs_to_lhs__true(self): + self.resolve.map_rhs_to_lhs = True + self.resolve.rhs_cube = self.src_cube + self.resolve.rhs_cube_category_local = self.src_category_local + self.resolve.rhs_cube_dim_coverage = self.src_dim_coverage + self.resolve.rhs_cube_aux_coverage = self.src_aux_coverage + self.resolve.lhs_cube = self.tgt_cube + self.resolve.lhs_cube_category_local = self.tgt_category_local + self.resolve.lhs_cube_dim_coverage = self.tgt_dim_coverage + self.resolve.lhs_cube_aux_coverage = self.tgt_aux_coverage + self._check() + + def test_map_rhs_to_lhs__false(self): + self.resolve.map_rhs_to_lhs = False + self.resolve.lhs_cube = self.src_cube + self.resolve.lhs_cube_category_local = self.src_category_local + self.resolve.lhs_cube_dim_coverage = self.src_dim_coverage + self.resolve.lhs_cube_aux_coverage = self.src_aux_coverage + self.resolve.rhs_cube = self.tgt_cube + self.resolve.rhs_cube_category_local = self.tgt_category_local + self.resolve.rhs_cube_dim_coverage = self.tgt_dim_coverage + self.resolve.rhs_cube_aux_coverage = self.tgt_aux_coverage + self._check() + + +class Test__prepare_factory_payload(tests.IrisTest): + def setUp(self): + self.Cube = namedtuple("Cube", ["aux_factories"]) + self.Coord = namedtuple("Coord", ["metadata"]) + self.Factory_T1 = namedtuple( + "Factory_T1", ["dependencies"] + ) # dummy factory type + self.container_T1 = type(self.Factory_T1(None)) + self.Factory_T2 = namedtuple( + "Factory_T2", ["dependencies"] + ) # dummy factory type + self.container_T2 = type(self.Factory_T2(None)) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.resolve.prepared_factories = [] + self.m_get_prepared_item = self.patch( + "iris.common.resolve.Resolve._get_prepared_item" + ) + self.category_local = sentinel.category_local + self.from_src = sentinel.from_src + + def test_no_factory(self): + cube = self.Cube(aux_factories=[]) + self.resolve._prepare_factory_payload(cube, self.category_local) + self.assertEqual(0, len(self.resolve.prepared_factories)) + + def test_skip_factory__already_prepared(self): + aux_factory = self.Factory_T1(dependencies=None) + aux_factories = [aux_factory] + cube = self.Cube(aux_factories=aux_factories) + prepared_factories = [ + _PreparedFactory(container=self.container_T1, dependencies=None), + _PreparedFactory(container=self.container_T2, dependencies=None), + ] + self.resolve.prepared_factories.extend(prepared_factories) + self.resolve._prepare_factory_payload(cube, self.category_local) + self.assertEqual(prepared_factories, self.resolve.prepared_factories) + + def test_factory__dependency_already_prepared(self): + coord_a = self.Coord(metadata=sentinel.coord_a_metadata) + coord_b = self.Coord(metadata=sentinel.coord_b_metadata) + coord_c = self.Coord(metadata=sentinel.coord_c_metadata) + side_effect = (coord_a, coord_b, coord_c) + self.m_get_prepared_item.side_effect = side_effect + dependencies = dict(name_a=coord_a, name_b=coord_b, name_c=coord_c) + aux_factory = self.Factory_T1(dependencies=dependencies) + aux_factories = [aux_factory] + cube = self.Cube(aux_factories=aux_factories) + self.resolve._prepare_factory_payload( + cube, self.category_local, from_src=self.from_src + ) + self.assertEqual(1, len(self.resolve.prepared_factories)) + prepared_dependencies = { + name: coord.metadata for name, coord in dependencies.items() + } + expected = [ + _PreparedFactory( + container=self.container_T1, dependencies=prepared_dependencies + ) + ] + self.assertEqual(expected, self.resolve.prepared_factories) + self.assertEqual(len(side_effect), self.m_get_prepared_item.call_count) + expected = [ + mock.call( + coord_a.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_b.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_c.metadata, self.category_local, from_src=self.from_src + ), + ] + actual = self.m_get_prepared_item.call_args_list + for call in expected: + self.assertIn(call, actual) + + def test_factory__dependency_local_not_prepared(self): + coord_a = self.Coord(metadata=sentinel.coord_a_metadata) + coord_b = self.Coord(metadata=sentinel.coord_b_metadata) + coord_c = self.Coord(metadata=sentinel.coord_c_metadata) + side_effect = (None, coord_a, None, coord_b, None, coord_c) + self.m_get_prepared_item.side_effect = side_effect + dependencies = dict(name_a=coord_a, name_b=coord_b, name_c=coord_c) + aux_factory = self.Factory_T1(dependencies=dependencies) + aux_factories = [aux_factory] + cube = self.Cube(aux_factories=aux_factories) + self.resolve._prepare_factory_payload( + cube, self.category_local, from_src=self.from_src + ) + self.assertEqual(1, len(self.resolve.prepared_factories)) + prepared_dependencies = { + name: coord.metadata for name, coord in dependencies.items() + } + expected = [ + _PreparedFactory( + container=self.container_T1, dependencies=prepared_dependencies + ) + ] + self.assertEqual(expected, self.resolve.prepared_factories) + self.assertEqual(len(side_effect), self.m_get_prepared_item.call_count) + expected = [ + mock.call( + coord_a.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_b.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_c.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_a.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + mock.call( + coord_b.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + mock.call( + coord_c.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + ] + actual = self.m_get_prepared_item.call_args_list + for call in expected: + self.assertIn(call, actual) + + def test_factory__dependency_not_found(self): + coord_a = self.Coord(metadata=sentinel.coord_a_metadata) + coord_b = self.Coord(metadata=sentinel.coord_b_metadata) + coord_c = self.Coord(metadata=sentinel.coord_c_metadata) + side_effect = (None, None) + self.m_get_prepared_item.side_effect = side_effect + dependencies = dict(name_a=coord_a, name_b=coord_b, name_c=coord_c) + aux_factory = self.Factory_T1(dependencies=dependencies) + aux_factories = [aux_factory] + cube = self.Cube(aux_factories=aux_factories) + self.resolve._prepare_factory_payload( + cube, self.category_local, from_src=self.from_src + ) + self.assertEqual(0, len(self.resolve.prepared_factories)) + self.assertEqual(len(side_effect), self.m_get_prepared_item.call_count) + expected = [ + mock.call( + coord_a.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_b.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_c.metadata, self.category_local, from_src=self.from_src + ), + mock.call( + coord_a.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + mock.call( + coord_b.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + mock.call( + coord_c.metadata, + self.category_local, + from_src=self.from_src, + from_local=True, + ), + ] + actual = self.m_get_prepared_item.call_args_list + for call in actual: + self.assertIn(call, expected) + + +class Test__get_prepared_item(tests.IrisTest): + def setUp(self): + PreparedItem = namedtuple("PreparedItem", ["metadata"]) + self.resolve = Resolve() + self.prepared_dim_metadata_src = sentinel.prepared_dim_metadata_src + self.prepared_dim_metadata_tgt = sentinel.prepared_dim_metadata_tgt + self.prepared_items_dim = PreparedItem( + metadata=_PreparedMetadata( + combined=None, + src=self.prepared_dim_metadata_src, + tgt=self.prepared_dim_metadata_tgt, + ) + ) + self.prepared_aux_metadata_src = sentinel.prepared_aux_metadata_src + self.prepared_aux_metadata_tgt = sentinel.prepared_aux_metadata_tgt + self.prepared_items_aux = PreparedItem( + metadata=_PreparedMetadata( + combined=None, + src=self.prepared_aux_metadata_src, + tgt=self.prepared_aux_metadata_tgt, + ) + ) + self.prepared_scalar_metadata_src = ( + sentinel.prepared_scalar_metadata_src + ) + self.prepared_scalar_metadata_tgt = ( + sentinel.prepared_scalar_metadata_tgt + ) + self.prepared_items_scalar = PreparedItem( + metadata=_PreparedMetadata( + combined=None, + src=self.prepared_scalar_metadata_src, + tgt=self.prepared_scalar_metadata_tgt, + ) + ) + self.resolve.prepared_category = _CategoryItems( + items_dim=[self.prepared_items_dim], + items_aux=[self.prepared_items_aux], + items_scalar=[self.prepared_items_scalar], + ) + self.resolve.mapping = {0: 10} + self.m_create_prepared_item = self.patch( + "iris.common.resolve.Resolve._create_prepared_item" + ) + self.local_dim_metadata = sentinel.local_dim_metadata + self.local_aux_metadata = sentinel.local_aux_metadata + self.local_scalar_metadata = sentinel.local_scalar_metadata + self.local_coord = sentinel.local_coord + self.local_coord_dims = (0,) + self.local_items_dim = _Item( + metadata=self.local_dim_metadata, + coord=self.local_coord, + dims=self.local_coord_dims, + ) + self.local_items_aux = _Item( + metadata=self.local_aux_metadata, + coord=self.local_coord, + dims=self.local_coord_dims, + ) + self.local_items_scalar = _Item( + metadata=self.local_scalar_metadata, + coord=self.local_coord, + dims=self.local_coord_dims, + ) + self.category_local = _CategoryItems( + items_dim=[self.local_items_dim], + items_aux=[self.local_items_aux], + items_scalar=[self.local_items_scalar], + ) + + def test_missing_prepared_coord__from_src(self): + metadata = sentinel.missing + category_local = None + result = self.resolve._get_prepared_item(metadata, category_local) + self.assertIsNone(result) + + def test_missing_prepared_coord__from_tgt(self): + metadata = sentinel.missing + category_local = None + result = self.resolve._get_prepared_item( + metadata, category_local, from_src=False + ) + self.assertIsNone(result) + + def test_get_prepared_dim_coord__from_src(self): + metadata = self.prepared_dim_metadata_src + category_local = None + result = self.resolve._get_prepared_item(metadata, category_local) + self.assertEqual(self.prepared_items_dim, result) + + def test_get_prepared_dim_coord__from_tgt(self): + metadata = self.prepared_dim_metadata_tgt + category_local = None + result = self.resolve._get_prepared_item( + metadata, category_local, from_src=False + ) + self.assertEqual(self.prepared_items_dim, result) + + def test_get_prepared_aux_coord__from_src(self): + metadata = self.prepared_aux_metadata_src + category_local = None + result = self.resolve._get_prepared_item(metadata, category_local) + self.assertEqual(self.prepared_items_aux, result) + + def test_get_prepared_aux_coord__from_tgt(self): + metadata = self.prepared_aux_metadata_tgt + category_local = None + result = self.resolve._get_prepared_item( + metadata, category_local, from_src=False + ) + self.assertEqual(self.prepared_items_aux, result) + + def test_get_prepared_scalar_coord__from_src(self): + metadata = self.prepared_scalar_metadata_src + category_local = None + result = self.resolve._get_prepared_item(metadata, category_local) + self.assertEqual(self.prepared_items_scalar, result) + + def test_get_prepared_scalar_coord__from_tgt(self): + metadata = self.prepared_scalar_metadata_tgt + category_local = None + result = self.resolve._get_prepared_item( + metadata, category_local, from_src=False + ) + self.assertEqual(self.prepared_items_scalar, result) + + def test_missing_local_coord__from_src(self): + metadata = sentinel.missing + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_local=True + ) + self.assertIsNone(result) + + def test_missing_local_coord__from_tgt(self): + metadata = sentinel.missing + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_src=False, from_local=True + ) + self.assertIsNone(result) + + def test_get_local_dim_coord__from_src(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_dim_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_dim)) + self.assertEqual(expected, self.resolve.prepared_category.items_dim[1]) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = (self.resolve.mapping[self.local_coord_dims[0]],) + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=metadata, + tgt_metadata=None, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_dim_coord__from_tgt(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_dim_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_src=False, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_dim)) + self.assertEqual(expected, self.resolve.prepared_category.items_dim[1]) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = self.local_coord_dims + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=None, + tgt_metadata=metadata, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_aux_coord__from_src(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_aux_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_aux)) + self.assertEqual(expected, self.resolve.prepared_category.items_aux[1]) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = (self.resolve.mapping[self.local_coord_dims[0]],) + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=metadata, + tgt_metadata=None, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_aux_coord__from_tgt(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_aux_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_src=False, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_aux)) + self.assertEqual(expected, self.resolve.prepared_category.items_aux[1]) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = self.local_coord_dims + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=None, + tgt_metadata=metadata, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_scalar_coord__from_src(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_scalar_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_scalar)) + self.assertEqual( + expected, self.resolve.prepared_category.items_scalar[1] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = (self.resolve.mapping[self.local_coord_dims[0]],) + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=metadata, + tgt_metadata=None, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + def test_get_local_scalar_coord__from_tgt(self): + created_local_item = sentinel.created_local_item + self.m_create_prepared_item.return_value = created_local_item + metadata = self.local_scalar_metadata + result = self.resolve._get_prepared_item( + metadata, self.category_local, from_src=False, from_local=True + ) + expected = created_local_item + self.assertEqual(expected, result) + self.assertEqual(2, len(self.resolve.prepared_category.items_scalar)) + self.assertEqual( + expected, self.resolve.prepared_category.items_scalar[1] + ) + self.assertEqual(1, self.m_create_prepared_item.call_count) + dims = self.local_coord_dims + expected = [ + mock.call( + self.local_coord, + dims, + src_metadata=None, + tgt_metadata=metadata, + ) + ] + self.assertEqual(expected, self.m_create_prepared_item.call_args_list) + + +class Test_cube(tests.IrisTest): + def setUp(self): + self.shape = (2, 3) + self.data = np.zeros(np.multiply(*self.shape), dtype=np.int8).reshape( + self.shape + ) + self.bad_data = np.zeros(np.multiply(*self.shape), dtype=np.int8) + self.resolve = Resolve() + self.resolve.map_rhs_to_lhs = True + self.resolve._broadcast_shape = self.shape + self.cube_metadata = CubeMetadata( + standard_name="air_temperature", + long_name="air temp", + var_name="airT", + units=Unit("K"), + attributes={}, + cell_methods=(), + ) + lhs_cube = Cube(self.data) + lhs_cube.metadata = self.cube_metadata + self.resolve.lhs_cube = lhs_cube + rhs_cube = Cube(self.data) + rhs_cube.metadata = self.cube_metadata + self.resolve.rhs_cube = rhs_cube + self.m_add_dim_coord = self.patch("iris.cube.Cube.add_dim_coord") + self.m_add_aux_coord = self.patch("iris.cube.Cube.add_aux_coord") + self.m_add_aux_factory = self.patch("iris.cube.Cube.add_aux_factory") + self.m_coord = self.patch("iris.cube.Cube.coord") + # + # prepared coordinates + # + prepared_category = _CategoryItems( + items_dim=[], items_aux=[], items_scalar=[] + ) + # prepared dim coordinates + self.prepared_dim_0_metadata = _PreparedMetadata( + combined=sentinel.prepared_dim_0_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_dim_0_points = sentinel.prepared_dim_0_points + self.prepared_dim_0_bounds = sentinel.prepared_dim_0_bounds + self.prepared_dim_0_dims = (0,) + self.prepared_dim_0_coord = mock.Mock(metadata=None) + self.prepared_dim_0_container = mock.Mock( + return_value=self.prepared_dim_0_coord + ) + self.prepared_dim_0 = _PreparedItem( + metadata=self.prepared_dim_0_metadata, + points=self.prepared_dim_0_points, + bounds=self.prepared_dim_0_bounds, + dims=self.prepared_dim_0_dims, + container=self.prepared_dim_0_container, + ) + prepared_category.items_dim.append(self.prepared_dim_0) + self.prepared_dim_1_metadata = _PreparedMetadata( + combined=sentinel.prepared_dim_1_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_dim_1_points = sentinel.prepared_dim_1_points + self.prepared_dim_1_bounds = sentinel.prepared_dim_1_bounds + self.prepared_dim_1_dims = (1,) + self.prepared_dim_1_coord = mock.Mock(metadata=None) + self.prepared_dim_1_container = mock.Mock( + return_value=self.prepared_dim_1_coord + ) + self.prepared_dim_1 = _PreparedItem( + metadata=self.prepared_dim_1_metadata, + points=self.prepared_dim_1_points, + bounds=self.prepared_dim_1_bounds, + dims=self.prepared_dim_1_dims, + container=self.prepared_dim_1_container, + ) + prepared_category.items_dim.append(self.prepared_dim_1) + + # prepared auxiliary coordinates + self.prepared_aux_0_metadata = _PreparedMetadata( + combined=sentinel.prepared_aux_0_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_aux_0_points = sentinel.prepared_aux_0_points + self.prepared_aux_0_bounds = sentinel.prepared_aux_0_bounds + self.prepared_aux_0_dims = (0,) + self.prepared_aux_0_coord = mock.Mock(metadata=None) + self.prepared_aux_0_container = mock.Mock( + return_value=self.prepared_aux_0_coord + ) + self.prepared_aux_0 = _PreparedItem( + metadata=self.prepared_aux_0_metadata, + points=self.prepared_aux_0_points, + bounds=self.prepared_aux_0_bounds, + dims=self.prepared_aux_0_dims, + container=self.prepared_aux_0_container, + ) + prepared_category.items_aux.append(self.prepared_aux_0) + self.prepared_aux_1_metadata = _PreparedMetadata( + combined=sentinel.prepared_aux_1_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_aux_1_points = sentinel.prepared_aux_1_points + self.prepared_aux_1_bounds = sentinel.prepared_aux_1_bounds + self.prepared_aux_1_dims = (1,) + self.prepared_aux_1_coord = mock.Mock(metadata=None) + self.prepared_aux_1_container = mock.Mock( + return_value=self.prepared_aux_1_coord + ) + self.prepared_aux_1 = _PreparedItem( + metadata=self.prepared_aux_1_metadata, + points=self.prepared_aux_1_points, + bounds=self.prepared_aux_1_bounds, + dims=self.prepared_aux_1_dims, + container=self.prepared_aux_1_container, + ) + prepared_category.items_aux.append(self.prepared_aux_1) + + # prepare scalar coordinates + self.prepared_scalar_0_metadata = _PreparedMetadata( + combined=sentinel.prepared_scalar_0_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_scalar_0_points = sentinel.prepared_scalar_0_points + self.prepared_scalar_0_bounds = sentinel.prepared_scalar_0_bounds + self.prepared_scalar_0_dims = () + self.prepared_scalar_0_coord = mock.Mock(metadata=None) + self.prepared_scalar_0_container = mock.Mock( + return_value=self.prepared_scalar_0_coord + ) + self.prepared_scalar_0 = _PreparedItem( + metadata=self.prepared_scalar_0_metadata, + points=self.prepared_scalar_0_points, + bounds=self.prepared_scalar_0_bounds, + dims=self.prepared_scalar_0_dims, + container=self.prepared_scalar_0_container, + ) + prepared_category.items_scalar.append(self.prepared_scalar_0) + self.prepared_scalar_1_metadata = _PreparedMetadata( + combined=sentinel.prepared_scalar_1_metadata_combined, + src=None, + tgt=None, + ) + self.prepared_scalar_1_points = sentinel.prepared_scalar_1_points + self.prepared_scalar_1_bounds = sentinel.prepared_scalar_1_bounds + self.prepared_scalar_1_dims = () + self.prepared_scalar_1_coord = mock.Mock(metadata=None) + self.prepared_scalar_1_container = mock.Mock( + return_value=self.prepared_scalar_1_coord + ) + self.prepared_scalar_1 = _PreparedItem( + metadata=self.prepared_scalar_1_metadata, + points=self.prepared_scalar_1_points, + bounds=self.prepared_scalar_1_bounds, + dims=self.prepared_scalar_1_dims, + container=self.prepared_scalar_1_container, + ) + prepared_category.items_scalar.append(self.prepared_scalar_1) + # + # prepared factories + # + prepared_factories = [] + self.aux_factory = sentinel.aux_factory + self.prepared_factory_container = mock.Mock( + return_value=self.aux_factory + ) + self.prepared_factory_metadata_a = _PreparedMetadata( + combined=sentinel.prepared_factory_metadata_a_combined, + src=None, + tgt=None, + ) + self.prepared_factory_metadata_b = _PreparedMetadata( + combined=sentinel.prepared_factory_metadata_b_combined, + src=None, + tgt=None, + ) + self.prepared_factory_metadata_c = _PreparedMetadata( + combined=sentinel.prepared_factory_metadata_c_combined, + src=None, + tgt=None, + ) + self.prepared_factory_dependencies = dict( + name_a=self.prepared_factory_metadata_a, + name_b=self.prepared_factory_metadata_b, + name_c=self.prepared_factory_metadata_c, + ) + self.prepared_factory = _PreparedFactory( + container=self.prepared_factory_container, + dependencies=self.prepared_factory_dependencies, + ) + prepared_factories.append(self.prepared_factory) + self.prepared_factory_side_effect = ( + sentinel.prepared_factory_coord_a, + sentinel.prepared_factory_coord_b, + sentinel.prepared_factory_coord_c, + ) + self.m_coord.side_effect = self.prepared_factory_side_effect + self.resolve.prepared_category = prepared_category + self.resolve.prepared_factories = prepared_factories + + def test_no_resolved_shape(self): + self.resolve._broadcast_shape = None + data = None + emsg = "Cannot resolve resultant cube, as no candidate cubes have been provided" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve.cube(data) + + def test_bad_data_shape(self): + emsg = "Cannot resolve resultant cube, as the provided data must have shape" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve.cube(self.bad_data) + + def test_bad_data_shape__inplace(self): + self.resolve.lhs_cube = Cube(self.bad_data) + emsg = "Cannot resolve resultant cube in-place" + with self.assertRaisesRegex(ValueError, emsg): + _ = self.resolve.cube(self.data, in_place=True) + + def _check(self): + # check dim coordinate 0 + self.assertEqual(1, self.prepared_dim_0.container.call_count) + expected = [ + mock.call( + self.prepared_dim_0_points, bounds=self.prepared_dim_0_bounds + ) + ] + self.assertEqual( + expected, self.prepared_dim_0.container.call_args_list + ) + self.assertEqual( + self.prepared_dim_0_coord.metadata, + self.prepared_dim_0_metadata.combined, + ) + # check dim coordinate 1 + self.assertEqual(1, self.prepared_dim_1.container.call_count) + expected = [ + mock.call( + self.prepared_dim_1_points, bounds=self.prepared_dim_1_bounds + ) + ] + self.assertEqual( + expected, self.prepared_dim_1.container.call_args_list + ) + self.assertEqual( + self.prepared_dim_1_coord.metadata, + self.prepared_dim_1_metadata.combined, + ) + # check add_dim_coord + self.assertEqual(2, self.m_add_dim_coord.call_count) + expected = [ + mock.call(self.prepared_dim_0_coord, self.prepared_dim_0_dims), + mock.call(self.prepared_dim_1_coord, self.prepared_dim_1_dims), + ] + self.assertEqual(expected, self.m_add_dim_coord.call_args_list) + + # check aux coordinate 0 + self.assertEqual(1, self.prepared_aux_0.container.call_count) + expected = [ + mock.call( + self.prepared_aux_0_points, bounds=self.prepared_aux_0_bounds + ) + ] + self.assertEqual( + expected, self.prepared_aux_0.container.call_args_list + ) + self.assertEqual( + self.prepared_aux_0_coord.metadata, + self.prepared_aux_0_metadata.combined, + ) + # check aux coordinate 1 + self.assertEqual(1, self.prepared_aux_1.container.call_count) + expected = [ + mock.call( + self.prepared_aux_1_points, bounds=self.prepared_aux_1_bounds + ) + ] + self.assertEqual( + expected, self.prepared_aux_1.container.call_args_list + ) + self.assertEqual( + self.prepared_aux_1_coord.metadata, + self.prepared_aux_1_metadata.combined, + ) + # check scalar coordinate 0 + self.assertEqual(1, self.prepared_scalar_0.container.call_count) + expected = [ + mock.call( + self.prepared_scalar_0_points, + bounds=self.prepared_scalar_0_bounds, + ) + ] + self.assertEqual( + expected, self.prepared_scalar_0.container.call_args_list + ) + self.assertEqual( + self.prepared_scalar_0_coord.metadata, + self.prepared_scalar_0_metadata.combined, + ) + # check scalar coordinate 1 + self.assertEqual(1, self.prepared_scalar_1.container.call_count) + expected = [ + mock.call( + self.prepared_scalar_1_points, + bounds=self.prepared_scalar_1_bounds, + ) + ] + self.assertEqual( + expected, self.prepared_scalar_1.container.call_args_list + ) + self.assertEqual( + self.prepared_scalar_1_coord.metadata, + self.prepared_scalar_1_metadata.combined, + ) + # check add_aux_coord + self.assertEqual(4, self.m_add_aux_coord.call_count) + expected = [ + mock.call(self.prepared_aux_0_coord, self.prepared_aux_0_dims), + mock.call(self.prepared_aux_1_coord, self.prepared_aux_1_dims), + mock.call( + self.prepared_scalar_0_coord, self.prepared_scalar_0_dims + ), + mock.call( + self.prepared_scalar_1_coord, self.prepared_scalar_1_dims + ), + ] + self.assertEqual(expected, self.m_add_aux_coord.call_args_list) + + # check auxiliary factories + self.assertEqual(1, self.m_add_aux_factory.call_count) + expected = [mock.call(self.aux_factory)] + self.assertEqual(expected, self.m_add_aux_factory.call_args_list) + self.assertEqual(1, self.prepared_factory_container.call_count) + expected = [ + mock.call( + **{ + name: value + for name, value in zip( + sorted(self.prepared_factory_dependencies.keys()), + self.prepared_factory_side_effect, + ) + } + ) + ] + self.assertEqual( + expected, self.prepared_factory_container.call_args_list + ) + self.assertEqual(3, self.m_coord.call_count) + expected = [ + mock.call(self.prepared_factory_metadata_a.combined), + mock.call(self.prepared_factory_metadata_b.combined), + mock.call(self.prepared_factory_metadata_c.combined), + ] + self.assertEqual(expected, self.m_coord.call_args_list) + + def test_resolve(self): + result = self.resolve.cube(self.data) + self.assertEqual(self.cube_metadata, result.metadata) + self._check() + self.assertIsNot(self.resolve.lhs_cube, result) + + def test_resolve__inplace(self): + result = self.resolve.cube(self.data, in_place=True) + self.assertEqual(self.cube_metadata, result.metadata) + self._check() + self.assertIs(self.resolve.lhs_cube, result) + + +if __name__ == "__main__": + tests.main()