class ClassNotFoundError(CISError):
    pass class GriddedCollocator(Collocator):

    def __init__(self, fill_value=None, var_name='', var_long_name='', var_units='',
                 missing_data_for_missing_sample=False, extrapolate=False):
        super(GriddedCollocator, self).__init__(fill_value, var_name, var_long_name, var_units,
                                                         missing_data_for_missing_sample)
        self.extrapolate = 'extrapolate' if extrapolate else 'mask'

    @staticmethod
    def _check_for_valid_kernel(kernel):
        from cis.exceptions import ClassNotFoundError

        if not (isinstance(kernel, gridded_gridded_nn) or isinstance(kernel, gridded_gridded_li)):
            raise ClassNotFoundError("Expected kernel of one of classes {}; found one of class {}".format(
                str([cis.utils.get_class_name(gridded_gridded_nn),
                     cis.utils.get_class_name(gridded_gridded_li)]),
                cis.utils.get_class_name(type(kernel))))

    def collocate(self, points, data, constraint, kernel):
        """
        This collocator takes two Iris cubes, and collocates from the data cube onto the grid of the 'points' cube. The
        collocator then returns another Iris cube.
        :param points: An Iris cube with the sampling grid to collocate onto.
        :param data: The Iris cube with the data to be collocated.
        :param constraint: None allowed yet, as this is unlikely to be required for gridded-gridded.
        :param kernel: The kernel to use, current options are gridded_gridded_nn and gridded_gridded_li.
        :return: An Iris cube with the collocated data.
        """
        self._check_for_valid_kernel(kernel)

        # Force the data longitude range to be the same as that of the sample grid.
        _fix_longitude_range(points.coords(), data)

        # Initialise variables used to create an output mask based on the sample data mask.
        sample_coord_lookup = {}  # Maps coordinate in sample data -> location in dimension order
        for idx, coord in enumerate(points.coords()):
            sample_coord_lookup[coord] = idx
        sample_coord_transpose_map = []  # For coords in both sample and data, contains the position in the sample
        other_coord_transpose_map = []  # For coords in data but not in sample, records that coord's position in data.
        repeat_size = 1
        output_mask = np.ma.nomask

        # Make a list of the coordinates we have, with each entry containing a list with the name of the coordinate and
        # the number of points along its axis. One is for the sample grid, which contains the points where we
        # interpolate too, and one is for the output grid, which will additionally contain any dimensions missing in the
        # sample grid.
        coord_names_and_sizes_for_sample_grid = []
        coord_names_and_sizes_for_output_grid = []
        for idx, coord in enumerate(data.coords(dim_coords=True)):
            # First try and find the coordinate in points, the sample grid. If an exception is thrown, it means that
            # name does not appear in the sample grid, and instead take the coordinate name and length from the original
            # data, as this is what we will be keeping.
            try:
                sample_coord = points.coords(coord.name())[0]
                coord_names_and_sizes_for_sample_grid.append([coord.name(), len(sample_coord.points)])
                # Find the index of the sample coordinate corresponding to the data coordinate.
                sample_coord_transpose_map.append(sample_coord_lookup[sample_coord])
            except IndexError:
                coord_names_and_sizes_for_output_grid.append([coord.name(), len(coord.points)])
                repeat_size *= len(coord.points)
                other_coord_transpose_map.append(idx)

        # Now we sort the sample coordinates so that they are in the same order as in the sample file,
        # rather than the order of the data file (that's the order we want the output dimensions).
        coord_names_and_sizes_for_sample_grid = [x[0] for x in sorted(zip(coord_names_and_sizes_for_sample_grid,
                                                                          sample_coord_transpose_map),
                                                                      key=lambda t: t[1])]

        # Adding the lists together in this way ensures that the coordinates not in the sample grid appear in the final
        # position, which is important for adding the points from the Iris interpolater to the new array. The data
        # returned from the Iris interpolater method will have dimensions of these missing coordinates, which needs
        # to be the final dimensions in the numpy array, as the iterator will give the position of the other dimensions.
        coord_names_and_sizes_for_output_grid = coord_names_and_sizes_for_sample_grid + \
                                                coord_names_and_sizes_for_output_grid

        # An array for the collocated data, with the correct shape
        output_shape = tuple(i[1] for i in coord_names_and_sizes_for_output_grid)
        new_data = np.zeros(output_shape)

        if self.missing_data_for_missing_sample:
            output_mask = self._make_output_mask(coord_names_and_sizes_for_sample_grid, output_shape,
                                                 points, repeat_size)

        # Now recreate the points cube, while ignoring any DimCoords in points that are not in the data cube
        new_dim_coord_list = []
        new_points_array_shape = []
        for i in range(0, len(coord_names_and_sizes_for_output_grid)):
            # Try and find the coordinate in the sample grid
            coord_found = points.coords(coord_names_and_sizes_for_output_grid[i][0])

            # If the coordinate exists in the sample grid then append the new coordinate to the list. Iris requires
            # this be given as a DimCoord object, along with a axis number, in a tuple pair.
            if len(coord_found) != 0:
                new_dim_coord_list.append((coord_found[0], len(new_dim_coord_list)))
                new_points_array_shape.append(coord_found[0].points.size)

        new_points_array = np.zeros(tuple(new_points_array_shape))

        # Use the new_data array to recreate points, without the DimCoords not in the data cube
        points = iris.cube.Cube(new_points_array, dim_coords_and_dims=new_dim_coord_list)

        output_cube = self._iris_interpolate(coord_names_and_sizes_for_output_grid,
                                             coord_names_and_sizes_for_sample_grid, data,
                                             kernel, output_mask, points, self.extrapolate)

        if not isinstance(output_cube, list):
            return GriddedDataList([output_cube])
        else:
            return output_cube

    @staticmethod
    def _make_output_mask(coord_names_and_sizes_for_sample_grid, output_shape, points, repeat_size):
        """ Creates a mask to apply to the output data based on the sample data mask. If there are coordinates in
        the data grid that are not in the sample grid, the same mask value is repeated for all values of the
        extra coordinates. If there are coordinates in the sample grid that are not in the data grid, a mask
        is not created since there is many to one correspondence between sample and output grid points.
        """
        output_mask = None

        # Construct the missing value mask from the sample data, if applicable.
        if len(coord_names_and_sizes_for_sample_grid) < len(points.dim_coords):
            # One or more axes collapsed so many sample points correspond to each output point.
            pass
        else:
            input_mask = np.ma.getmask(points.data)
            if input_mask is np.ma.nomask:
                # No sample data missing-value mask.
                pass
            else:
                # Fill in the remaining coordinates (those from the data which are not in the sample) by repeating
                # the constructed mask for each value of those coordinates
                output_mask = np.reshape(np.repeat(points.data.mask, repeat_size), output_shape)
        return output_mask

    @staticmethod
    def _iris_interpolate(coord_names_and_sizes_for_output_grid, coord_names_and_sizes_for_sample_grid, data, kernel,
                          output_mask, points, extrapolate):
        """ Collocates using iris.analysis.interpolate
        """
        coordinate_point_pairs = []
        for j in range(0, len(coord_names_and_sizes_for_sample_grid)):
            # For each coordinate make the list of tuple pair Iris requires, for example
            # [('latitude', -90), ('longitude, 0')]
            coordinate_point_pairs.append((coord_names_and_sizes_for_sample_grid[j][0],
                                           points.dim_coords[j].points))

        # The result here will be a cube with the correct dimensions for the output, so interpolated over all points
        # in coord_names_and_sizes_for_output_grid.
        output_cube = make_from_cube(data.interpolate(coordinate_point_pairs,
                                                      kernel.interpolater(extrapolation_mode=extrapolate)))

        # Iris outputs interpolated cubes with the dimensions in the order of the data grid, not the sample grid,
        # so we need to rearrange the order of the dimensions.
        output_coord_lookup = {}
        for idx, coord in enumerate(output_cube.dim_coords):
            output_coord_lookup[coord.name()] = idx
        transpose_map = [output_coord_lookup[coord[0]] for coord in coord_names_and_sizes_for_output_grid]
        output_cube.transpose(transpose_map)

        if isinstance(output_cube, list):
            for idx, data in enumerate(output_cube):
                output_cube[idx].data = cis.utils.apply_mask_to_numpy_array(data.data, output_mask)
        else:
            output_cube.data = cis.utils.apply_mask_to_numpy_array(output_cube.data, output_mask)
        return output_cube class gridded_gridded_nn(Kernel):
    def __init__(self):
        from iris.analysis import Nearest
        self.name = 'nearest'
        self.interpolater = Nearest

    def get_value(self, point, data):
        """Not needed for gridded/gridded collocation.
        """
        raise ValueError("gridded_gridded_nn kernel selected for use with collocator other than GriddedCollocator") class gridded_gridded_li(Kernel):
    def __init__(self):
        from iris.analysis import Linear
        self.name = 'bilinear'
        self.interpolater = Linear

    def get_value(self, point, data):
        """Not needed for gridded/gridded collocation.
        """
        raise ValueError("gridded_gridded_li kernel selected for use with collocator other than GriddedCollocator") class nn_p(nn_pressure):
    """Nearest neighbour pressure kernel - alias for nn_pressure.
    """
    pass def make_dummy_2d_cube():
    """
        Makes a dummy cube filled with random datapoints of shape 19x36
    """
    from iris.cube import Cube
    from iris.coords import DimCoord

    latitude = DimCoord(np.arange(-85., 105., 10.), standard_name='latitude', units='degrees')
    longitude = DimCoord(np.arange(0., 360., 10.), standard_name='longitude', units='degrees')
    cube = Cube(np.reshape(np.arange(19 * 36) + 1.0, (19, 36)),
                dim_coords_and_dims=[(latitude, 0), (longitude, 1)])

    return cube def make_dummy_2d_cube_with_small_offset_in_lat_and_lon():
    """
        Makes a dummy cube filled with random datapoints of shape 19x36
    """
    import numpy
    from iris.cube import Cube
    from iris.coords import DimCoord

    latitude = DimCoord(list(range(-84, 106, 10)), standard_name='latitude', units='degrees')
    longitude = DimCoord(list(range(1, 361, 10)), standard_name='longitude', units='degrees')
    cube = Cube(numpy.random.rand(19, 36), dim_coords_and_dims=[(latitude, 0), (longitude, 1)])

    return cube def make_dummy_2d_cube_with_small_offset_in_lat():
    """
        Makes a dummy cube filled with random datapoints of shape 19x36
    """
    import numpy
    from iris.cube import Cube
    from iris.coords import DimCoord

    latitude = DimCoord(list(range(-84, 106, 10)), standard_name='latitude', units='degrees')
    longitude = DimCoord(list(range(0, 360, 10)), standard_name='longitude', units='degrees')
    cube = Cube(numpy.random.rand(19, 36), dim_coords_and_dims=[(latitude, 0), (longitude, 1)])

    return cube def make_dummy_2d_cube_with_small_offset_in_lon():
    """
        Makes a dummy cube filled with random datapoints of shape 19x36
    """
    import numpy
    from iris.cube import Cube
    from iris.coords import DimCoord

    latitude = DimCoord(list(range(-85, 105, 10)), standard_name='latitude', units='degrees')
    longitude = DimCoord(list(range(1, 361, 10)), standard_name='longitude', units='degrees')
    cube = Cube(numpy.random.rand(19, 36), dim_coords_and_dims=[(latitude, 0), (longitude, 1)])

    return cube def make_list_with_2_dummy_2d_cubes_where_verticies_are_in_cell_centres():
    """
    Makes two dummy cubes where the cells intersect like:

        |--------|
        |        |
        |   |----|----|
        |   |    |    |
        |--------|    |
            |         |
            |---------|

    For the second cube the values contained are a checkerboard of 0s and 1s, starting with 0 for the first cell
    """
    import numpy
    from iris.cube import Cube
    from iris.coords import DimCoord

    latitude = DimCoord(list(range(0, 10, 2)), standard_name='latitude', units='degrees')
    longitude = DimCoord(list(range(0, 10, 2)), standard_name='longitude', units='degrees')
    cube1 = Cube(numpy.random.rand(5, 5), dim_coords_and_dims=[(latitude, 0), (longitude, 1)])

    checkerboard = numpy.zeros((5, 5))

    for i in range(0, 5):
        for j in range(0, 5):
            if (i + j) % 2 == 1:
                checkerboard[i, j] = 1

    latitude = DimCoord(numpy.arange(1, 11, 2), standard_name='latitude', units='degrees')
    longitude = DimCoord(numpy.arange(1, 11, 2), standard_name='longitude', units='degrees')
    cube2 = Cube(checkerboard, dim_coords_and_dims=[(latitude, 0), (longitude, 1)])

    return cube1, cube2 def make_mock_cube(lat_dim_length=5, lon_dim_length=3, lon_range=None, alt_dim_length=0, pres_dim_length=0,
                   time_dim_length=0,
                   horizontal_offset=0, altitude_offset=0, pressure_offset=0, time_offset=0, data_offset=0,
                   surf_pres_offset=0,
                   hybrid_ht_len=0, hybrid_pr_len=0, geopotential_height=False, dim_order=None, mask=False):
    """
    Makes a cube of any shape required, with coordinate offsets from the default available. If no arguments are
    given get a 5x3 cube of the form:
        array([[1,2,3],
               [4,5,6],
               [7,8,9],
               [10,11,12],
               [13,14,15]])
        and coordinates in latitude:
            array([ -10, -5, 0, 5, 10 ])
        longitude:
            array([ -5, 0, 5 ])
    :param lat_dim_length: Latitude grid length
    :param lon_dim_length: Longitude grid length
    :param alt_dim_length: Altitude grid length
    :param pres_dim_length: Pressure grid length
    :param time_dim_length: Time grid length
    :param horizontal_offset: Offset from the default grid, in degrees, in lat and lon
    :param altitude_offset: Offset from the default grid in altitude
    :param pressure_offset: Offset from the default grid in pressure
    :param time_offset: Offset from the default grid in time
    :param data_offset: Offset from the default data values
    :param surf_pres_offset: Offset for the optional surface pressure field
    :param hybrid_ht_len: Hybrid height grid length
    :param hybrid_pr_len: Hybrid pressure grid length
    :param geopotential_height: Include a geopotential height field when calcluting a hybrid pressure? (default False)
    :param dim_order: List of 'lat', 'lon', 'alt', 'pres', 'time' in the order in which the dimensions occur
    :param mask: A mask to apply to the data, this should be either a scalar or the same shape as the data
    :return: A cube with well defined data.
    """
    import iris
    from iris.aux_factory import HybridHeightFactory, HybridPressureFactory

    data_size = 1
    DIM_NAMES = ['lat', 'lon', 'alt', 'pres', 'time', 'hybrid_ht', 'hybrid_pr']
    dim_lengths = [lat_dim_length, lon_dim_length, alt_dim_length, pres_dim_length, time_dim_length, hybrid_ht_len,
                   hybrid_pr_len]
    lon_range = lon_range or (-5., 5.)

    if dim_order is None:
        dim_order = list(DIM_NAMES)

    if any([True for d in dim_order if d not in DIM_NAMES]):
        raise ValueError("dim_order contains unrecognised name")

    for idx, dim in enumerate(DIM_NAMES):
        if dim_lengths[idx] == 0 and dim in dim_order:
            del dim_order[dim_order.index(dim)]

    coord_map = {}
    for idx, dim in enumerate(dim_order):
        coord_map[dim] = dim_order.index(dim)
    coord_list = [None] * len(coord_map)

    if lat_dim_length:
        coord_list[coord_map['lat']] = (DimCoord(np.linspace(-10., 10., lat_dim_length) + horizontal_offset,
                                                 standard_name='latitude', units='degrees', var_name='lat'),
                                        coord_map['lat'])
        data_size *= lat_dim_length

    if lon_dim_length:
        coord_list[coord_map['lon']] = (
            DimCoord(np.linspace(lon_range[0], lon_range[1], lon_dim_length) + horizontal_offset,
                     standard_name='longitude', units='degrees', var_name='lon'), coord_map['lon'])
        data_size *= lon_dim_length

    if alt_dim_length:
        coord_list[coord_map['alt']] = (DimCoord(np.linspace(0., 7., alt_dim_length) + altitude_offset,
                                                 standard_name='altitude', units='metres', var_name='alt'),
                                        coord_map['alt'])
        data_size *= alt_dim_length

    if pres_dim_length:
        coord_list[coord_map['pres']] = (DimCoord(np.linspace(0., 7., pres_dim_length) + pressure_offset,
                                                  standard_name='air_pressure', units='hPa', var_name='pres'),
                                         coord_map['pres'])
        data_size *= pres_dim_length

    if time_dim_length:
        t0 = datetime.datetime(1984, 8, 27)
        times = np.array([t0 + datetime.timedelta(days=d + time_offset) for d in range(time_dim_length)])
        time_nums = convert_datetime_to_std_time(times)
        time_bounds = None
        if time_dim_length == 1:
            time_bounds = convert_datetime_to_std_time(np.array([times[0] - datetime.timedelta(days=0.5),
                                                                       times[0] + datetime.timedelta(days=0.5)]))
        coord_list[coord_map['time']] = (DimCoord(time_nums, standard_name='time',
                                                  units='days since 1600-01-01 00:00:00', var_name='time',
                                                  bounds=time_bounds),
                                         coord_map['time'])
        data_size *= time_dim_length

    if hybrid_ht_len:
        coord_list[coord_map['hybrid_ht']] = (DimCoord(np.arange(hybrid_ht_len, dtype='i8') + 10,
                                                       "model_level_number", units="1"), coord_map['hybrid_ht'])
        data_size *= hybrid_ht_len

    if hybrid_pr_len:
        coord_list[coord_map['hybrid_pr']] = (DimCoord(np.arange(hybrid_pr_len, dtype='i8'),
                                                       "atmosphere_hybrid_sigma_pressure_coordinate", units="1"),
                                              coord_map['hybrid_pr'])
        data_size *= hybrid_pr_len

    data = np.reshape(np.arange(data_size) + data_offset + 1., tuple(len(i[0].points) for i in coord_list))

    if mask:
        data = np.ma.asarray(data)
        data.mask = mask

    return_cube = Cube(data, dim_coords_and_dims=coord_list, var_name='rain', standard_name='rainfall_rate',
                       long_name="TOTAL RAINFALL RATE: LS+CONV KG/M2/S", units="kg m-2 s-1")

    if hybrid_ht_len:
        return_cube.add_aux_coord(iris.coords.AuxCoord(np.arange(hybrid_ht_len, dtype='i8') + 40,
                                                       long_name="level_height",
                                                       units="m", var_name='hybrid_ht'), coord_map['hybrid_ht'])
        return_cube.add_aux_coord(iris.coords.AuxCoord(np.arange(hybrid_ht_len, dtype='i8') + 50,
                                                       long_name="sigma", units="1", var_name='sigma'),
                                  coord_map['hybrid_ht'])
        return_cube.add_aux_coord(iris.coords.AuxCoord(
            np.arange(lat_dim_length * lon_dim_length, dtype='i8').reshape(lat_dim_length, lon_dim_length) + 100,
            long_name="surface_altitude",
            units="m"), [coord_map['lat'], coord_map['lon']])

        return_cube.add_aux_factory(HybridHeightFactory(
            delta=return_cube.coord("level_height"),
            sigma=return_cube.coord("sigma"),
            orography=return_cube.coord("surface_altitude")))
    elif hybrid_pr_len:
        return_cube.add_aux_coord(iris.coords.AuxCoord(np.arange(hybrid_pr_len, dtype='i8') + 40,
                                                       long_name="hybrid A coefficient at layer midpoints",
                                                       units="Pa", var_name='a'), coord_map['hybrid_pr'])
        return_cube.add_aux_coord(iris.coords.AuxCoord(np.arange(hybrid_pr_len, dtype='f8') + 50,
                                                       long_name="hybrid B coefficient at layer midpoints", units="1",
                                                       var_name='b'),
                                  coord_map['hybrid_pr'])
        return_cube.add_aux_coord(
            iris.coords.AuxCoord(np.arange(lat_dim_length * lon_dim_length * time_dim_length, dtype='i8')
                                 .reshape(lat_dim_length, lon_dim_length, time_dim_length) * 100000 + surf_pres_offset,
                                 "surface_air_pressure", units="Pa"),
            [coord_map['lat'], coord_map['lon'], coord_map['time']])

        if geopotential_height:
            return_cube.add_aux_coord(iris.coords.AuxCoord(
                np.arange(lat_dim_length * lon_dim_length * time_dim_length * hybrid_pr_len, dtype='i8')
                .reshape(lat_dim_length, lon_dim_length, time_dim_length, hybrid_pr_len) + 10,
                "altitude", long_name="Geopotential height at layer midpoints", units="meter"),
                [coord_map['lat'], coord_map['lon'], coord_map['time'], coord_map['hybrid_pr']])

        return_cube.add_aux_factory(HybridPressureFactory(
            delta=return_cube.coord("hybrid A coefficient at layer midpoints"),
            sigma=return_cube.coord("hybrid B coefficient at layer midpoints"),
            surface_air_pressure=return_cube.coord("surface_air_pressure")))

    for coord in return_cube.coords(dim_coords=True):
        if coord.bounds is None:
            coord.guess_bounds()

    return return_cube
from unittest import TestCase
from nose.tools import istest, raises
from iris.exceptions import CoordinateNotFoundError
from cis.exceptions import ClassNotFoundError
from cis.collocation.col_implementations import GriddedCollocator, gridded_gridded_nn, gridded_gridded_li, nn_p
from cis.test.util.mock import make_dummy_2d_cube, make_dummy_2d_cube_with_small_offset_in_lat_and_lon, \
    make_dummy_2d_cube_with_small_offset_in_lat, make_dummy_2d_cube_with_small_offset_in_lon, \
    make_list_with_2_dummy_2d_cubes_where_verticies_are_in_cell_centres, make_mock_cube
        from cis.time_util import convert_datetime_to_std_time
        from cis.time_util import convert_datetime_to_std_time
        from cis.data_io.gridded_data import GriddedDataList
        from cis.data_io.gridded_data import GriddedDataList
import numpy
import cis.data_io.gridded_data as gridded_data
        import iris.coords
        import datetime
        import numpy as np
        import numpy as np
        import iris.coords
        import datetime
        assert numpy.array_equal(data_cube.data, out_cube.data)
        assert numpy.array_equal(sample_cube.coord('latitude').points, out_cube.coord('latitude').points)
        assert numpy.array_equal(sample_cube.coord('longitude').points, out_cube.coord('longitude').points)
        assert numpy.array_equal(data_cube.coord('time').points, out_cube.coord('time').points)

    @istest
    def test_gridded_gridded_li_with_one_grid_containing_altitude(self):
        sample_cube = gridded_data.make_from_cube(make_mock_cube())
        data_cube = gridded_data.make_from_cube(make_mock_cube(alt_dim_length=7))

        col = self.collocator

        out_cube = col.collocate(points=sample_cube, data=data_cube, constraint=None, kernel=gridded_gridded_li())[0]

        assert numpy.array_equal(data_cube.data, out_cube.data)
        assert numpy.array_equal(sample_cube.coord('latitude').points, out_cube.coord('latitude').points)
        assert numpy.array_equal(sample_cube.coord('longitude').points, out_cube.coord('longitude').points)
        assert numpy.array_equal(data_cube.coord('altitude').points, out_cube.coord('altitude').points)

    @istest
    def test_gridded_gridded_nn_with_one_grid_containing_time_and_slightly_offset(self):
        sample_cube = gridded_data.make_from_cube(make_mock_cube(data_offset=1.0))
        data_cube = gridded_data.make_from_cube(make_mock_cube(time_dim_length=7, horizontal_offset=0.1))

        col = self.collocator

        out_cube = col.collocate(points=sample_cube, data=data_cube, constraint=None, kernel=gridded_gridded_nn())[0]

        assert numpy.array_equal(data_cube.data, out_cube.data)
        assert numpy.array_equal(sample_cube.coord('latitude').points, out_cube.coord('latitude').points)
        assert numpy.array_equal(sample_cube.coord('longitude').points, out_cube.coord('longitude').points)
        assert numpy.array_equal(data_cube.coord('time').points, out_cube.coord('time').points)

    @istest
    def test_gridded_gridded_nn_with_one_grid_containing_time_and_moderate_offset(self):
        sample_cube = gridded_data.make_from_cube(make_mock_cube())
        data_cube = gridded_data.make_from_cube(make_mock_cube(time_dim_length=7, horizontal_offset=2.6))

        col = self.collocator

        out_cube = col.collocate(points=sample_cube, data=data_cube, constraint=None, kernel=gridded_gridded_nn())[0]

        expected_result = numpy.array([[[1., 2., 3., 4., 5., 6., 7.],
                                        [1., 2., 3., 4., 5., 6., 7.],
                                        [8., 9., 10., 11., 12., 13., 14.]],

                                       [[1., 2., 3., 4., 5., 6., 7.],
                                        [1., 2., 3., 4., 5., 6., 7.],
                                        [8., 9., 10., 11., 12., 13., 14.]],

                                       [[22., 23., 24., 25., 26., 27., 28.],
                                        [22., 23., 24., 25., 26., 27., 28.],
                                        [29., 30., 31., 32., 33., 34., 35.]],

                                       [[43., 44., 45., 46., 47., 48., 49.],
                                        [43., 44., 45., 46., 47., 48., 49.],
                                        [50., 51., 52., 53., 54., 55., 56.]],

                                       [[64., 65., 66., 67., 68., 69., 70.],
                                        [64., 65., 66., 67., 68., 69., 70.],
                                        [71., 72., 73., 74., 75., 76., 77.]]])

        assert numpy.array_equal(out_cube.data, expected_result)
        assert numpy.array_equal(sample_cube.coord('latitude').points, out_cube.coord('latitude').points)
        assert numpy.array_equal(sample_cube.coord('longitude').points, out_cube.coord('longitude').points)
        assert numpy.array_equal(data_cube.coord('time').points, out_cube.coord('time').points)

    @istest
    def test_gridded_gridded_li_with_one_grid_containing_time_and_slightly_offset(self):
        sample_cube = gridded_data.make_from_cube(make_mock_cube(data_offset=1.0))
        data_cube = gridded_data.make_from_cube(make_mock_cube(time_dim_length=7, horizontal_offset=0.1))

        col = self.collocator

        out_cube = col.collocate(points=sample_cube, data=data_cube, constraint=None, kernel=gridded_gridded_li())[0]

        result = numpy.array([[[0.44, 1.44, 2.44, 3.44, 4.44, 5.44, 6.44],
                               [7.44, 8.44, 9.44, 10.44, 11.44, 12.44, 13.44],
                               [14.44, 15.44, 16.44, 17.44, 18.44, 19.44, 20.44]],

                              [[21.44, 22.44, 23.44, 24.44, 25.44, 26.44, 27.44],
                               [28.44, 29.44, 30.44, 31.44, 32.44, 33.44, 34.44],
                               [35.44, 36.44, 37.44, 38.44, 39.44, 40.44, 41.44]],

                              [[42.44, 43.44, 44.44, 45.44, 46.44, 47.44, 48.44],
                               [49.44, 50.44, 51.44, 52.44, 53.44, 54.44, 55.44],
                               [56.44, 57.44, 58.44, 59.44, 60.44, 61.44, 62.44]],

                              [[63.44, 64.44, 65.44, 66.44, 67.44, 68.44, 69.44],
                               [70.44, 71.44, 72.44, 73.44, 74.44, 75.44, 76.44],
                               [77.44, 78.44, 79.44, 80.44, 81.44, 82.44, 83.44]],

                              [[84.44, 85.44, 86.44, 87.44, 88.44, 89.44, 90.44],
                               [91.44, 92.44, 93.44, 94.44, 95.44, 96.44, 97.44],
                               [98.44, 99.44, 100.44, 101.44, 102.44, 103.44, 104.44]]])

        assert numpy.allclose(out_cube.data, result)
        assert numpy.array_equal(sample_cube.coord('latitude').points, out_cube.coord('latitude').points)
        assert numpy.array_equal(sample_cube.coord('longitude').points, out_cube.coord('longitude').points)
        assert numpy.array_equal(data_cube.coord('time').points, out_cube.coord('time').points)

    @istest
    def test_gridded_gridded_nn_with_sample_containing_time_and_pressure_and_small_offset(self):
        sample_cube = gridded_data.make_from_cube(make_mock_cube(time_dim_length=7,
                                                                 pres_dim_length=10, data_offset=1.0))
        data_cube = gridded_data.make_from_cube(make_mock_cube(horizontal_offset=0.1, time_offset=0.1))

        col = self.collocator

        out_cube = col.collocate(points=sample_cube, data=data_cube, constraint=None, kernel=gridded_gridded_nn())[0]

        assert numpy.array_equal(data_cube.data, out_cube.data)
        assert numpy.array_equal(sample_cube.coord('latitude').points, out_cube.coord('latitude').points)
        assert numpy.array_equal(sample_cube.coord('longitude').points, out_cube.coord('longitude').points)
        assert not does_coord_exist_in_cube(out_cube, 'time')
        assert not does_coord_exist_in_cube(out_cube, 'pressure')

    @istest
    def test_gridded_gridded_li_with_sample_grid_4d_and_slightly_offset(self):
        sample_cube = gridded_data.make_from_cube(make_mock_cube(alt_dim_length=7, time_dim_length=7, data_offset=1.0))

        data_cube = gridded_data.make_from_cube(make_mock_cube(horizontal_offset=0.1))