Example #1
0
    def check_mode(self, mode=None):
        kwargs = {}
        if mode is not None:
            kwargs['extrapolation_mode'] = mode
        linear = Linear(**kwargs)
        if mode is None:
            mode = 'linear'
        self.assertEqual(linear.extrapolation_mode, mode)

        # To avoid duplicating tests, just check that creating an
        # interpolator defers to the LinearInterpolator with the
        # correct arguments (and honouring the return value!)
        with mock.patch('iris.analysis.LinearInterpolator',
                        return_value=mock.sentinel.interpolator) as li:
            interpolator = linear.interpolator(mock.sentinel.cube,
                                               mock.sentinel.coords)
        li.assert_called_once_with(mock.sentinel.cube, mock.sentinel.coords,
                                   extrapolation_mode=mode)
        self.assertIs(interpolator, mock.sentinel.interpolator)

        # As above, check method defers to LinearRegridder.
        with mock.patch('iris.analysis.LinearRegridder',
                        return_value=mock.sentinel.regridder) as lr:
            regridder = linear.regridder(mock.sentinel.src,
                                         mock.sentinel.target)
        lr.assert_called_once_with(mock.sentinel.src, mock.sentinel.target,
                                   extrapolation_mode=mode)
        self.assertIs(regridder, mock.sentinel.regridder)
Example #2
0
    def test_interpolator(self, linear_interp_patch):
        mock_interpolator = mock.Mock(name='mocked linear interpolator')
        linear_interp_patch.return_value = mock_interpolator

        linear = Linear(self.extrap)
        cube = mock.Mock(name='cube')
        coords = mock.Mock(name='coords')

        interpolator = linear.interpolator(cube, coords)

        self.assertIs(interpolator, mock_interpolator)
        linear_interp_patch.assert_called_once_with(
            cube, coords, extrapolation_mode=self.extrap)
Example #3
0
def _regrid_to_target(src_cube, target_coords, target_cube):
    # Interpolate onto the target grid.
    sample_points = [(coord, coord.points) for coord in target_coords]
    result_cube = src_cube.interpolate(sample_points, Linear())

    # Any scalar coords on the target_cube will have become vector
    # coords on the resample src_cube (i.e. result_cube).
    # These unwanted vector coords need to be pushed back to scalars.
    index = [slice(None, None)] * result_cube.ndim
    for target_coord in target_coords:
        if not target_cube.coord_dims(target_coord):
            result_dim = result_cube.coord_dims(target_coord)[0]
            index[result_dim] = 0
    if not all(key == slice(None, None) for key in index):
        result_cube = result_cube[tuple(index)]
    return result_cube
def main():
    filename = 'rp_prognostics_after'
    path = datadir + 'stochastic/'

    # Parameters to compare between forecasts
    name = 'Geopotential Height [m]'
    pressure = 500
    reference = 'fp'
    lead_time = 7 * 24

    # Load reference forecast
    cs = iris.Constraint(name=name,
                         pressure=pressure,
                         precision=23,
                         forecast_period=lead_time)
    fp = iris.load_cube(path + 'rp_convection.nc', cs)

    # Regrid other reference forecasts
    if reference == 't39':
        ref = iris.load_cube(datadir + 'output/exp_007/yyyymmddhh_p.nc', cs)
        ref = ref.regrid(fp, Linear())
        fp.data = ref.data

    elif reference == 'sppt':
        ref = iris.load_cube(datadir + 'output/exp_004/yyyymmddhh_p.nc', cs)
        fp.data = ref.data

    # Compare full precision with reduced precision forecasts
    cs = iris.Constraint(name=name,
                         pressure=pressure,
                         forecast_period=lead_time)
    for filename in os.listdir(path):
        rp = iris.load_cube(path + filename, cs)
        plot_errors(fp, rp, marker='x', label=filename.split('.')[0])

    # Plot ensemble spread as upper limit
    ensemble = iris.load_cube(datadir + 'ensembles/yyyymmddhh_p52b.nc', cs)
    std_dev = ensemble_std_dev(ensemble)
    plt.axhline(std_dev.data, color='k', linestyle='--')

    plt.ylabel('RMS Error')
    plt.title(name)
    plt.legend()
    plt.show()

    return
Example #5
0
    def _regrid_to_target(self, cube: Cube, target_grid: Cube,
                          regridded_title: Optional[str]) -> Cube:
        """
        Regrid cube to target_grid, inherit grid attributes and update title

        Args:
            cube:
                Cube to be regridded
            target_grid:
                Data on the target grid. If regridding with mask, this cube
                should contain land-sea mask data to be used in adjusting land
                and sea points after regridding.
            regridded_title:
                New value for the "title" attribute to be used after
                regridding. If not set, a default value is used.

        Returns:
            Regridded cube with updated attributes
        """
        regridder = Linear(extrapolation_mode=self.extrapolation_mode)
        if "nearest" in self.regrid_mode:
            regridder = Nearest(extrapolation_mode=self.extrapolation_mode)
        cube = cube.regrid(target_grid, regridder)

        if self.REGRID_REQUIRES_LANDMASK[self.regrid_mode]:
            cube = self._adjust_landsea(cube, target_grid)

        # identify grid-describing attributes on source cube that need updating
        required_grid_attributes = [
            attr for attr in cube.attributes if attr in MOSG_GRID_ATTRIBUTES
        ]
        # update attributes if available on target grid, otherwise remove
        for key in required_grid_attributes:
            if key in target_grid.attributes:
                cube.attributes[key] = target_grid.attributes[key]
            else:
                cube.attributes.pop(key)

        cube.attributes["title"] = (MANDATORY_ATTRIBUTE_DEFAULTS["title"]
                                    if regridded_title is None else
                                    regridded_title)

        return cube
Example #6
0
def redo_cubes(cubes, basis_cube, stash_maps=[], slices=None, time=None):
    # Coordinates to copy to analyses
    z = grid.extract_dim_coord(basis_cube, 'z')

    # Define attributes of custom variables by stash mapping
    for stash_map in stash_maps:
        stash_map.remap_cubelist(cubes)

    newcubelist = CubeList()
    for cube in cubes:
        print(cube)
        newcube = cube.copy()
        # Remove unneccessary time coordinates
        for coord in ['forecast_period', 'forecast_reference_time']:
            try:
                newcube.remove_coord(coord)
            except iris.exceptions.CoordinateNotFoundError:
                pass

        if newcube.ndim == 3:
            # Use the hybrid height coordinate as the main vertical coordinate
            try:
                newcube.remove_coord('model_level_number')
            except iris.exceptions.CoordinateNotFoundError:
                pass
            newcube.coord('level_height').rename(z.name())
            iris.util.promote_aux_coord_to_dim_coord(newcube, z.name())

            # Remap the cubes to theta points
            newcube = interpolate.remap_3d(newcube, basis_cube, z.name())

        else:
            # Regrid in the horizontal
            newcube = newcube.regrid(basis_cube, Linear())

        # Convert the main time coordinate
        if time is not None:
            newcube.coord('time').convert_units(time)

        newcubelist.append(newcube)

    return newcubelist
Example #7
0
    def __init__(self,
                 call_func,
                 observation,
                 model,
                 scenarios,
                 reference_period=None,
                 correction_period=None,
                 time_unit='day',
                 work_dir=gettempdir(),
                 interpolator=Linear(),
                 save_regridded=False):
        """
        Args:

        * call_func (callable):
            | *call signature*: (obs_cube, ref_cube, sce_cubes,
                                 \*args, \**kwargs)

        * observation (:class:`.io.Dataset`):
            the observation dataset

        * model (:class:`.io.Dataset`):
            the model dataset

        * scenarios (:class:`.io.Dataset` or list of those)
            the scenarios that shall be bias corrected

        Kwargs:

        * reference_period (tuple of :class:`datetime.datetime`):
            the reference period that observations and model share;
            if not given, take it from the observation and model
            reference dataset, respectively

        * correction_period (tuple of :class:`datetime.datetime`):
            the period for which the correction shall be done;
            if not given, take it from the scenario dataset

        * time_unit (str):
            correction will be performed on daily (day) or
            monthly (month) basis

        * interpolator:
            an available interpolation scheme for regridding to the
            observational dataset. Currently available schemes are:

            * :class:`iris.analysis.Linear` (default)

            * :class:`iris.analysis.Nearest`

            * :class:`iris.analysis.AreaWeighted`

        * work_dir (path):
            directory where intermediate files will be written

        * save_regridded (boolean):
            wheter regridded data shall be stored to disk (default: False)
        """
        self.call_func = call_func
        self.obs = observation
        obs_phenomenon = {
            'units': self.obs._orig_units,
            'standard_name': self.obs._orig_standard_name,
            'long_name': self.obs._orig_long_name,
            'var_name': self.obs._orig_var_name
        }
        self.mod = model
        self.sce = scenarios

        # set the reference period
        if reference_period:
            self.obs.period = reference_period
            self.mod.period = reference_period

        # set the spatial extent of the observation to the model
        self.mod.extent = self.obs.extent
        self.mod.adjustments = obs_phenomenon

        # make the scenarios list if they are not already
        if hasattr(scenarios, '__iter__'):
            self.sce = scenarios
        else:
            self.sce = [scenarios]

        for sce in self.sce:
            sce.extent = self.obs.extent
            sce.adjustments = obs_phenomenon
            if correction_period:
                sce.period = correction_period

        self.time_unit = time_unit
        self.interpolator = interpolator
        self.work_dir = work_dir
        self.save_regridded = save_regridded
Example #8
0
 def test_invalid(self):
     with self.assertRaisesRegexp(ValueError, 'Extrapolation mode'):
         Linear('bogus')
Example #9
0
 def test_default(self):
     linear = Linear()
     self.assertEqual(linear.extrapolation_mode, 'linear')
Example #10
0
def create_scheme(mode=None):
    kwargs = {}
    if mode is not None:
        kwargs['extrapolation_mode'] = mode
    return Linear(**kwargs)
Example #11
0
 def test___init__(self):
     linear = Linear(extrapolation_mode=self.extrap)
     self.assertEqual(getattr(linear, 'extrapolation_mode', None),
                      self.extrap)
Example #12
0
def linear(cube, sample_points, extrapolation_mode='linear'):
    """
    Return a cube of the linearly interpolated points given the desired
    sample points.

    Given a list of tuple pairs mapping coordinates (or coordinate names)
    to their desired values, return a cube with linearly interpolated values.
    If more than one coordinate is specified, the linear interpolation will be
    carried out in sequence, thus providing n-linear interpolation
    (bi-linear, tri-linear, etc.).

    If the input cube's data is masked, the result cube will have a data
    mask interpolated to the new sample points

    .. testsetup::

        import numpy as np

    For example:

        >>> cube = iris.load_cube(iris.sample_data_path('air_temp.pp'))
        >>> sample_points = [('latitude', np.linspace(-90, 90, 10)),
        ...                  ('longitude', np.linspace(-180, 180, 20))]
        >>> iris.analysis.interpolate.linear(cube, sample_points)
        <iris 'Cube' of air_temperature / (K) (latitude: 10; longitude: 20)>

    .. note::

        By definition, linear interpolation requires all coordinates to
        be 1-dimensional.

    .. note::

        If a specified coordinate is single valued its value will be
        extrapolated to the desired sample points by assuming a gradient of
        zero.

    Args:

    * cube
        The cube to be interpolated.

    * sample_points
        List of one or more tuple pairs mapping coordinate to desired
        points to interpolate. Points may be a scalar or a numpy array
        of values.  Multi-dimensional coordinates are not supported.

    Kwargs:

    * extrapolation_mode - string - one of 'linear', 'nan' or 'error'

        * If 'linear' the point will be calculated by extending the
          gradient of closest two points.
        * If 'nan' the extrapolation point will be put as a NaN.
        * If 'error' a value error will be raised notifying of the
          attempted extrapolation.

    .. note::

        If the source cube's data, or any of its resampled coordinates,
        have an integer data type they will be promoted to a floating
        point data type in the result.

    .. deprecated:: 1.10

        The module :mod:`iris.analysis.interpolate` is deprecated.
        Please replace usage of
        :func:`iris.analysis.interpolate.linear`
        with :meth:`iris.cube.Cube.interpolate` using the scheme
        :class:`iris.analysis.Linear`.

    """
    if isinstance(sample_points, dict):
        sample_points = list(sample_points.items())

    # catch the case where a user passes a single (coord/name, value) pair rather than a list of pairs
    if sample_points and not (isinstance(sample_points[0], collections.Container) and not isinstance(sample_points[0], six.string_types)):
        raise TypeError('Expecting the sample points to be a list of tuple pairs representing (coord, points), got a list of %s.' % type(sample_points[0]))

    scheme = Linear(extrapolation_mode)
    return cube.interpolate(sample_points, scheme)
Example #13
0
 def test_invalid(self):
     with self.assertRaisesRegex(ValueError, "Extrapolation mode"):
         Linear("bogus")
def regrid(source_cube, grid_cube, mode='bilinear', **kwargs):
    """
    Returns a new cube with values derived from the source_cube on the horizontal grid specified
    by the grid_cube.

    Fundamental input requirements:
        1) Both cubes must have a CoordSystem.
        2) The source 'x' and 'y' coordinates must not share data dimensions with any other coordinates.

    In addition, the algorithm currently used requires:
        3) Both CS instances must be compatible:
            i.e. of the same type, with the same attribute values, and with compatible coordinates.
        4) No new data dimensions can be created.
        5) Source cube coordinates to map to a single dimension.

    Args:

    * source_cube:
        An instance of :class:`iris.cube.Cube` which supplies the source data and metadata.
    * grid_cube:
        An instance of :class:`iris.cube.Cube` which supplies the horizontal grid definition.

    Kwargs:

    * mode (string):
        Regridding interpolation algorithm to be applied, which may be one of the following:

            * 'bilinear' for bi-linear interpolation (default), see :func:`iris.analysis.interpolate.linear`.
            * 'nearest' for nearest neighbour interpolation.

    Returns:
        A new :class:`iris.cube.Cube` instance.

    .. note::

        The masked status of values are currently ignored.  See :func:\
`~iris.experimental.regrid.regrid_bilinear_rectilinear_src_and_grid`
        for regrid support with mask awareness.

    .. deprecated:: 1.10

        Please use :meth:`iris.cube.Cube.regrid` instead, with an appropriate
        regridding scheme:

        *   For mode='bilinear', simply use the :class:`~iris.analysis.Linear`
            scheme.

        *   For mode='nearest', use the :class:`~iris.analysis.Nearest` scheme,
            with extrapolation_mode='extrapolate', but be aware of the
            following possible differences:

            *   Any missing result points, i.e. those which match source points
                which are masked or NaN, are returned as as NaN values by this
                routine.  The 'Nearest' scheme, however, represents missing
                results as masked points in a masked array.
                *Which* points are missing is unchanged.

            *   Longitude wrapping for this routine is controlled by the
                'circular' property of the x coordinate.
                The 'Nearest' scheme, however, *always* wraps any coords with
                modular units, such as (correctly formed) longitudes.
                Thus, behaviour can be different if "x_coord.circular" is
                False :  In that case, if the original non-longitude-wrapped
                operation is required, it can be replicated by converting all
                X and Y coordinates' units to '1' and removing their coordinate
                systems.

    """
    if mode == 'bilinear':
        scheme = Linear(**kwargs)
        return source_cube.regrid(grid_cube, scheme)

    # Condition 1
    source_cs = source_cube.coord_system(iris.coord_systems.CoordSystem)
    grid_cs = grid_cube.coord_system(iris.coord_systems.CoordSystem)
    if (source_cs is None) != (grid_cs is None):
        raise ValueError(
            "The source and grid cubes must both have a CoordSystem or both have None."
        )

    # Condition 2: We can only have one x coordinate and one y coordinate with the source CoordSystem, and those coordinates
    # must be the only ones occupying their respective dimension
    source_x = source_cube.coord(axis='x', coord_system=source_cs)
    source_y = source_cube.coord(axis='y', coord_system=source_cs)

    source_x_dims = source_cube.coord_dims(source_x)
    source_y_dims = source_cube.coord_dims(source_y)

    source_x_dim = None
    if source_x_dims:
        if len(source_x_dims) > 1:
            raise ValueError(
                'The source x coordinate may not describe more than one data dimension.'
            )
        source_x_dim = source_x_dims[0]
        dim_sharers = ', '.join([
            coord.name()
            for coord in source_cube.coords(contains_dimension=source_x_dim)
            if coord is not source_x
        ])
        if dim_sharers:
            raise ValueError(
                'No coordinates may share a dimension (dimension %s) with the x '
                'coordinate, but (%s) do.' % (source_x_dim, dim_sharers))

    source_y_dim = None
    if source_y_dims:
        if len(source_y_dims) > 1:
            raise ValueError(
                'The source y coordinate may not describe more than one data dimension.'
            )
        source_y_dim = source_y_dims[0]
        dim_sharers = ', '.join([
            coord.name()
            for coord in source_cube.coords(contains_dimension=source_y_dim)
            if coord is not source_y
        ])
        if dim_sharers:
            raise ValueError(
                'No coordinates may share a dimension (dimension %s) with the y '
                'coordinate, but (%s) do.' % (source_y_dim, dim_sharers))

    if source_x_dim is not None and source_y_dim == source_x_dim:
        raise ValueError(
            'The source x and y coords may not describe the same data dimension.'
        )

    # Condition 3
    # Check for compatible horizontal CSs. Currently that means they're exactly the same except for the coordinate
    # values.
    # The same kind of CS ...
    compatible = (source_cs == grid_cs)
    if compatible:
        grid_x = grid_cube.coord(axis='x', coord_system=grid_cs)
        grid_y = grid_cube.coord(axis='y', coord_system=grid_cs)
        compatible = source_x.is_compatible(grid_x) and \
            source_y.is_compatible(grid_y)
    if not compatible:
        raise ValueError(
            "The new grid must be defined on the same coordinate system, and have the same coordinate "
            "metadata, as the source.")

    # Condition 4
    if grid_cube.coord_dims(grid_x) and not source_x_dims or \
            grid_cube.coord_dims(grid_y) and not source_y_dims:
        raise ValueError(
            "The new grid must not require additional data dimensions.")

    x_coord = grid_x.copy()
    y_coord = grid_y.copy()

    #
    # Adjust the data array to match the new grid.
    #

    # get the new shape of the data
    new_shape = list(source_cube.shape)
    if source_x_dims:
        new_shape[source_x_dims[0]] = grid_x.shape[0]
    if source_y_dims:
        new_shape[source_y_dims[0]] = grid_y.shape[0]

    new_data = np.empty(new_shape, dtype=source_cube.data.dtype)

    # Prepare the index pattern which will be used to insert a single "column" of data.
    # NB. A "column" is a slice constrained to a single XY point, which therefore extends over *all* the other axes.
    # For an XYZ cube this means a column only extends over Z and corresponds to the normal definition of "column".
    indices = [slice(None, None)] * new_data.ndim

    if mode == 'bilinear':
        # Perform bilinear interpolation, passing through any keywords.
        points_dict = [(source_x, list(x_coord.points)),
                       (source_y, list(y_coord.points))]
        new_data = linear(source_cube, points_dict, **kwargs).data
    else:
        # Perform nearest neighbour interpolation on each column in turn.
        for iy, y in enumerate(y_coord.points):
            for ix, x in enumerate(x_coord.points):
                column_pos = [(source_x, x), (source_y, y)]
                column_data = extract_nearest_neighbour(
                    source_cube, column_pos).data
                if source_y_dim is not None:
                    indices[source_y_dim] = iy
                if source_x_dim is not None:
                    indices[source_x_dim] = ix
                new_data[tuple(indices)] = column_data

    # Special case to make 0-dimensional results take the same form as NumPy
    if new_data.shape == ():
        new_data = new_data.flat[0]

    # Start with just the metadata and the re-sampled data...
    new_cube = iris.cube.Cube(new_data)
    new_cube.metadata = source_cube.metadata

    # ... and then copy across all the unaffected coordinates.

    # Record a mapping from old coordinate IDs to new coordinates,
    # for subsequent use in creating updated aux_factories.
    coord_mapping = {}

    def copy_coords(source_coords, add_method):
        for coord in source_coords:
            if coord is source_x or coord is source_y:
                continue
            dims = source_cube.coord_dims(coord)
            new_coord = coord.copy()
            add_method(new_coord, dims)
            coord_mapping[id(coord)] = new_coord

    copy_coords(source_cube.dim_coords, new_cube.add_dim_coord)
    copy_coords(source_cube.aux_coords, new_cube.add_aux_coord)

    for factory in source_cube.aux_factories:
        new_cube.add_aux_factory(factory.updated(coord_mapping))

    # Add the new coords
    if source_x in source_cube.dim_coords:
        new_cube.add_dim_coord(x_coord, source_x_dim)
    else:
        new_cube.add_aux_coord(x_coord, source_x_dims)

    if source_y in source_cube.dim_coords:
        new_cube.add_dim_coord(y_coord, source_y_dim)
    else:
        new_cube.add_aux_coord(y_coord, source_y_dims)

    return new_cube
Example #15
0
    def _regrid_to_target(
        self,
        cube: Cube,
        target_grid: Cube,
        regridded_title: Optional[str],
        regrid_mode: str,
    ) -> Cube:
        """
        Regrid cube to target_grid, inherit grid attributes and update title

        Args:
            cube:
                Cube to be regridded
            target_grid:
                Data on the target grid. If regridding with mask, this cube
                should contain land-sea mask data to be used in adjusting land
                and sea points after regridding.
            regridded_title:
                New value for the "title" attribute to be used after
                regridding. If not set, a default value is used.
            regrid_mode:
                "bilinear","nearest","nearest-with-mask",
                "nearest-2","nearest-with-mask-2","bilinear-2","bilinear-with-mask-2"

        Returns:
            Regridded cube with updated attributes.
        """
        if regrid_mode in (
                "nearest-with-mask",
                "nearest-with-mask-2",
                "bilinear-with-mask-2",
        ):
            if self.landmask_name not in self.landmask_source_grid.name():
                msg = "Expected {} in input_landmask cube but found {}".format(
                    self.landmask_name, repr(self.landmask_source_grid))
                warnings.warn(msg)

            if self.landmask_name not in target_grid.name():
                msg = "Expected {} in target_grid cube but found {}".format(
                    self.landmask_name, repr(target_grid))
                warnings.warn(msg)

        # basic categories (1) Iris-based (2) new nearest based  (3) new bilinear-based
        if regrid_mode in ("bilinear", "nearest", "nearest-with-mask"):
            if "nearest" in regrid_mode:
                regridder = Nearest(extrapolation_mode=self.extrapolation_mode)
            else:
                regridder = Linear(extrapolation_mode=self.extrapolation_mode)
            cube = cube.regrid(target_grid, regridder)

            # Iris regridding is used, and then adjust if land_sea mask is considered
            if self.REGRID_REQUIRES_LANDMASK[regrid_mode]:
                cube = AdjustLandSeaPoints(
                    vicinity_radius=self.landmask_vicinity,
                    extrapolation_mode=self.extrapolation_mode,
                )(cube, self.landmask_source_grid, target_grid)

        # new version of nearest/bilinear option with/without land-sea mask
        elif regrid_mode in (
                "nearest-2",
                "nearest-with-mask-2",
                "bilinear-2",
                "bilinear-with-mask-2",
        ):
            cube = RegridWithLandSeaMask(
                regrid_mode=regrid_mode,
                vicinity_radius=self.landmask_vicinity)(
                    cube, self.landmask_source_grid, target_grid)

        # identify grid-describing attributes on source cube that need updating
        required_grid_attributes = [
            attr for attr in cube.attributes if attr in MOSG_GRID_ATTRIBUTES
        ]

        # update attributes if available on target grid, otherwise remove
        for key in required_grid_attributes:
            if key in target_grid.attributes:
                cube.attributes[key] = target_grid.attributes[key]
            else:
                cube.attributes.pop(key)

        cube.attributes["title"] = (MANDATORY_ATTRIBUTE_DEFAULTS["title"]
                                    if regridded_title is None else
                                    regridded_title)

        return cube
Example #16
0
def interpolate(cube, sample_points, method=None):
    """
    Extract a sub-cube at the given n-dimensional points.

    Args:

    * cube
        The source Cube.

    * sample_points
        A sequence of coordinate (name) - values pairs.

    Kwargs:

    * method
        Request "linear" interpolation (default) or "nearest" neighbour.
        Only nearest neighbour is available when specifying multi-dimensional
        coordinates.


    For example::

        sample_points = [('latitude', [45, 45, 45]),
        ('longitude', [-60, -50, -40])]
        interpolated_cube = interpolate(cube, sample_points)

    """
    if method not in [None, "linear", "nearest"]:
        raise ValueError("Unhandled interpolation specified : %s" % method)

    # Convert any coordinate names to coords
    points = []
    for coord, values in sample_points:
        if isinstance(coord, str):
            coord = cube.coord(coord)
        points.append((coord, values))
    sample_points = points

    # Do all value sequences have the same number of values?
    coord, values = sample_points[0]
    trajectory_size = len(values)
    for coord, values in sample_points[1:]:
        if len(values) != trajectory_size:
            raise ValueError("Lengths of coordinate values are inconsistent.")

    # Which dimensions are we squishing into the last dimension?
    squish_my_dims = set()
    for coord, values in sample_points:
        dims = cube.coord_dims(coord)
        for dim in dims:
            squish_my_dims.add(dim)

    # Derive the new cube's shape by filtering out all the dimensions we're
    # about to sample,
    # and then adding a new dimension to accommodate all the sample points.
    remaining = [
        (dim, size)
        for dim, size in enumerate(cube.shape)
        if dim not in squish_my_dims
    ]
    new_data_shape = [size for dim, size in remaining]
    new_data_shape.append(trajectory_size)

    # Start with empty data and then fill in the "column" of values for each
    # trajectory point.
    new_cube = iris.cube.Cube(np.empty(new_data_shape))
    new_cube.metadata = cube.metadata

    # Derive the mapping from the non-trajectory source dimensions to their
    # corresponding destination dimensions.
    remaining_dims = [dim for dim, size in remaining]
    dimension_remap = {dim: i for i, dim in enumerate(remaining_dims)}

    # Record a mapping from old coordinate IDs to new coordinates,
    # for subsequent use in creating updated aux_factories.
    coord_mapping = {}

    # Create all the non-squished coords
    for coord in cube.dim_coords:
        src_dims = cube.coord_dims(coord)
        if squish_my_dims.isdisjoint(src_dims):
            dest_dims = [dimension_remap[dim] for dim in src_dims]
            new_coord = coord.copy()
            new_cube.add_dim_coord(new_coord, dest_dims)
            coord_mapping[id(coord)] = new_coord
    for coord in cube.aux_coords:
        src_dims = cube.coord_dims(coord)
        if squish_my_dims.isdisjoint(src_dims):
            dest_dims = [dimension_remap[dim] for dim in src_dims]
            new_coord = coord.copy()
            new_cube.add_aux_coord(new_coord, dest_dims)
            coord_mapping[id(coord)] = new_coord

    # Create all the squished (non derived) coords, not filled in yet.
    trajectory_dim = len(remaining_dims)
    for coord in cube.dim_coords + cube.aux_coords:
        src_dims = cube.coord_dims(coord)
        if not squish_my_dims.isdisjoint(src_dims):
            points = np.array([coord.points.flatten()[0]] * trajectory_size)
            new_coord = iris.coords.AuxCoord(
                points,
                standard_name=coord.standard_name,
                long_name=coord.long_name,
                units=coord.units,
                bounds=None,
                attributes=coord.attributes,
                coord_system=coord.coord_system,
            )
            new_cube.add_aux_coord(new_coord, trajectory_dim)
            coord_mapping[id(coord)] = new_coord

    for factory in cube.aux_factories:
        new_cube.add_aux_factory(factory.updated(coord_mapping))

    # Are the given coords all 1-dimensional? (can we do linear interp?)
    for coord, values in sample_points:
        if coord.ndim > 1:
            if method == "linear":
                msg = (
                    "Cannot currently perform linear interpolation for "
                    "multi-dimensional coordinates."
                )
                raise iris.exceptions.CoordinateMultiDimError(msg)
            method = "nearest"
            break

    if method in ["linear", None]:
        for i in range(trajectory_size):
            point = [(coord, values[i]) for coord, values in sample_points]
            column = cube.interpolate(point, Linear())
            new_cube.data[..., i] = column.data
            # Fill in the empty squashed (non derived) coords.
            for column_coord in column.dim_coords + column.aux_coords:
                src_dims = cube.coord_dims(column_coord)
                if not squish_my_dims.isdisjoint(src_dims):
                    if len(column_coord.points) != 1:
                        msg = "Expected to find exactly one point. Found {}."
                        raise Exception(msg.format(column_coord.points))
                    new_cube.coord(column_coord.name()).points[
                        i
                    ] = column_coord.points[0]

    elif method == "nearest":
        # Use a cache with _nearest_neighbour_indices_ndcoords()
        cache = {}
        column_indexes = _nearest_neighbour_indices_ndcoords(
            cube, sample_points, cache=cache
        )

        # Construct "fancy" indexes, so we can create the result data array in
        # a single numpy indexing operation.
        # ALSO: capture the index range in each dimension, so that we can fetch
        # only a required (square) sub-region of the source data.
        fancy_source_indices = []
        region_slices = []
        n_index_length = len(column_indexes[0])
        dims_reduced = [False] * n_index_length
        for i_ind in range(n_index_length):
            contents = [column_index[i_ind] for column_index in column_indexes]
            each_used = [content != slice(None) for content in contents]
            if np.all(each_used):
                # This dimension is addressed : use a list of indices.
                dims_reduced[i_ind] = True
                # Select the region by min+max indices.
                start_ind = np.min(contents)
                stop_ind = 1 + np.max(contents)
                region_slice = slice(start_ind, stop_ind)
                # Record point indices with start subtracted from all of them.
                fancy_index = list(np.array(contents) - start_ind)
            elif not np.any(each_used):
                # This dimension is not addressed by the operation.
                # Use a ":" as the index.
                fancy_index = slice(None)
                # No sub-region selection for this dimension.
                region_slice = slice(None)
            else:
                # Should really never happen, if _ndcoords is right.
                msg = (
                    "Internal error in trajectory interpolation : point "
                    "selection indices should all have the same form."
                )
                raise ValueError(msg)

            fancy_source_indices.append(fancy_index)
            region_slices.append(region_slice)

        # Fetch the required (square-section) region of the source data.
        # NOTE: This is not quite as good as only fetching the individual
        # points used, but it avoids creating a sub-cube for each point,
        # which is very slow, especially when points are re-used a lot ...
        source_area_indices = tuple(region_slices)
        source_data = cube[source_area_indices].data

        # Transpose source data before indexing it to get the final result.
        # Because.. the fancy indexing will replace the indexed (horizontal)
        # dimensions with a new single dimension over trajectory points.
        # Move those dimensions to the end *first* : this ensures that the new
        # dimension also appears at the end, which is where we want it.
        # Make a list of dims with the reduced ones last.
        dims_reduced = np.array(dims_reduced)
        dims_order = np.arange(n_index_length)
        dims_order = np.concatenate(
            (dims_order[~dims_reduced], dims_order[dims_reduced])
        )
        # Rearrange the data dimensions and the fancy indices into that order.
        source_data = source_data.transpose(dims_order)
        fancy_source_indices = [
            fancy_source_indices[i_dim] for i_dim in dims_order
        ]

        # Apply the fancy indexing to get all the result data points.
        source_data = source_data[tuple(fancy_source_indices)]

        # "Fix" problems with missing datapoints producing odd values
        # when copied from a masked into an unmasked array.
        # TODO: proper masked data handling.
        if np.ma.isMaskedArray(source_data):
            # This is **not** proper mask handling, because we cannot produce a
            # masked result, but it ensures we use a "filled" version of the
            # input in this case.
            source_data = source_data.filled()
        new_cube.data[:] = source_data
        # NOTE: we assign to "new_cube.data[:]" and *not* just "new_cube.data",
        # because the existing code produces a default dtype from 'np.empty'
        # instead of preserving the input dtype.
        # TODO: maybe this should be fixed -- i.e. to preserve input dtype ??

        # Fill in the empty squashed (non derived) coords.
        column_coords = [
            coord
            for coord in cube.dim_coords + cube.aux_coords
            if not squish_my_dims.isdisjoint(cube.coord_dims(coord))
        ]
        new_cube_coords = [
            new_cube.coord(column_coord.name())
            for column_coord in column_coords
        ]
        all_point_indices = np.array(column_indexes)
        single_point_test_cube = cube[column_indexes[0]]
        for new_cube_coord, src_coord in zip(new_cube_coords, column_coords):
            # Check structure of the indexed coord (at one selected point).
            point_coord = single_point_test_cube.coord(src_coord)
            if len(point_coord.points) != 1:
                msg = (
                    "Coord {} at one x-y position has the shape {}, "
                    "instead of being a single point. "
                )
                raise ValueError(msg.format(src_coord.name(), src_coord.shape))

            # Work out which indices apply to the input coord.
            # NOTE: we know how to index the source cube to get a cube with a
            # single point for each coord, but this is very inefficient.
            # So here, we translate cube indexes into *coord* indexes.
            src_coord_dims = cube.coord_dims(src_coord)
            fancy_coord_index_arrays = [
                list(all_point_indices[:, src_dim])
                for src_dim in src_coord_dims
            ]

            # Fill the new coord with all the correct points from the old one.
            new_cube_coord.points = src_coord.points[
                tuple(fancy_coord_index_arrays)
            ]
            # NOTE: the new coords do *not* have bounds.

    return new_cube
Example #17
0
_MDI = 1e+20

# Stock cube - global grid extents (degrees).
_LAT_MIN = -90.0
_LAT_MAX = 90.0
_LAT_RANGE = _LAT_MAX - _LAT_MIN
_LON_MIN = 0.0
_LON_MAX = 360.0
_LON_RANGE = _LON_MAX - _LON_MIN

# A cached stock of standard horizontal target grids.
_CACHE = dict()

# Supported point interpolation schemes.
POINT_INTERPOLATION_SCHEMES = {
    'linear': Linear(extrapolation_mode='mask'),
    'nearest': Nearest(extrapolation_mode='mask'),
}

# Supported horizontal regridding schemes.
HORIZONTAL_SCHEMES = {
    'linear': Linear(extrapolation_mode='mask'),
    'linear_extrapolate': Linear(extrapolation_mode='extrapolate'),
    'nearest': Nearest(extrapolation_mode='mask'),
    'area_weighted': AreaWeighted(),
    'unstructured_nearest': UnstructuredNearest(),
}

# Supported vertical interpolation schemes.
VERTICAL_SCHEMES = ('linear', 'nearest',
                    'linear_horizontal_extrapolate_vertical',
Example #18
0
    def calc_true_north_offset(reference_cube: Cube) -> ndarray:
        """
        Calculate the angles between grid North and true North, as a
        matrix of values on the grid of the input reference cube.

        Args:
            reference_cube:
                2D cube on grid for which "north" is required.  Provides both
                coordinate system (reference_cube.coord_system()) and template
                spatial grid on which the angle adjustments should be provided.

        Returns:
            Angle in radians by which wind direction wrt true North at
            each point must be rotated to be relative to grid North.
        """
        reference_x_coord = reference_cube.coord(axis="x")
        reference_y_coord = reference_cube.coord(axis="y")

        # find corners of reference_cube grid in lat / lon coordinates
        latlon = [
            GLOBAL_CRS.as_cartopy_crs().transform_point(
                reference_x_coord.points[i],
                reference_y_coord.points[j],
                reference_cube.coord_system().as_cartopy_crs(),
            )
            for i in [0, -1]
            for j in [0, -1]
        ]
        latlon = np.array(latlon).T.tolist()

        # define lat / lon coordinates to cover the reference_cube grid at an
        # equivalent resolution
        lat_points = np.linspace(
            np.floor(min(latlon[1])),
            np.ceil(max(latlon[1])),
            len(reference_y_coord.points),
        )
        lon_points = np.linspace(
            np.floor(min(latlon[0])),
            np.ceil(max(latlon[0])),
            len(reference_x_coord.points),
        )

        lat_coord = DimCoord(
            lat_points, "latitude", units="degrees", coord_system=GLOBAL_CRS
        )
        lon_coord = DimCoord(
            lon_points, "longitude", units="degrees", coord_system=GLOBAL_CRS
        )

        # define a unit vector wind towards true North over the lat / lon grid
        udata = np.zeros(reference_cube.shape, dtype=np.float32)
        vdata = np.ones(reference_cube.shape, dtype=np.float32)

        ucube_truenorth = Cube(
            udata,
            "grid_eastward_wind",
            dim_coords_and_dims=[(lat_coord, 0), (lon_coord, 1)],
        )
        vcube_truenorth = Cube(
            vdata,
            "grid_northward_wind",
            dim_coords_and_dims=[(lat_coord, 0), (lon_coord, 1)],
        )

        # rotate unit vector onto reference_cube coordinate system
        ucube, vcube = rotate_winds(
            ucube_truenorth, vcube_truenorth, reference_cube.coord_system()
        )

        # unmask and regrid rotated winds onto reference_cube grid
        ucube.data = ucube.data.data
        ucube = ucube.regrid(reference_cube, Linear())
        vcube.data = vcube.data.data
        vcube = vcube.regrid(reference_cube, Linear())

        # ratio of u to v winds is the tangent of the angle which is the
        # true North to grid North rotation
        angle_adjustment = np.arctan2(ucube.data, vcube.data)

        return angle_adjustment
Example #19
0
def remap_3d(cube, target):
    cube = cube.regrid(target, Linear())
    cube = cube.interpolate(
        [('level_height', target.coord('level_height').points)], Linear())

    return cube