# Copyright (c) 2018 MetPy Developers.
# Distributed under the terms of the BSD 3-Clause License.
# SPDX-License-Identifier: BSD-3-Clause
"""Interpolate data valid at one set of points to another in multiple dimensions."""
import logging
import numpy as np
from scipy.interpolate import griddata, Rbf
from scipy.spatial import cKDTree, ConvexHull, Delaunay, qhull
from . import geometry, tools
from ..package_tools import Exporter
exporter = Exporter(globals())
log = logging.getLogger(__name__)
def cressman_point(sq_dist, values, radius):
r"""Generate a Cressman interpolation value for a point.
The calculated value is based on the given distances and search radius.
Parameters
----------
sq_dist: (N, ) ndarray
Squared distance between observations and grid point
values: (N, ) ndarray
Observation values in same order as sq_dist
radius: float
Maximum distance to search for observations to use for
interpolation.
Returns
-------
value: float
Interpolation value for grid point.
"""
weights = tools.cressman_weights(sq_dist, radius)
total_weights = np.sum(weights)
return sum(v * (w / total_weights) for (w, v) in zip(weights, values))
def barnes_point(sq_dist, values, kappa, gamma=None):
r"""Generate a single pass Barnes interpolation value for a point.
The calculated value is based on the given distances, kappa and gamma values. This
is calculated as an inverse distance-weighted average of the points in the neighborhood,
with weights given as:
.. math:: w = e ^ \frac{-r^2}{\kappa}
* :math:`\kappa` is a scaling parameter
* :math:`r` is the distance to a point.
For more information see [Barnes1964]_ or [Koch1983]_.
Parameters
----------
sq_dist: (N, ) ndarray
Squared distance between observations and grid point
values: (N, ) ndarray
Observation values in same order as sq_dist
kappa: float
Response parameter for barnes interpolation.
gamma: float
Adjustable smoothing parameter for the barnes interpolation. Default 1.
Returns
-------
value: float
Interpolation value for grid point.
"""
if gamma is None:
gamma = 1
weights = tools.barnes_weights(sq_dist, kappa, gamma)
total_weights = np.sum(weights)
return sum(v * (w / total_weights) for (w, v) in zip(weights, values))
def natural_neighbor_point(xp, yp, variable, grid_loc, tri, neighbors, circumcenters):
r"""Generate a natural neighbor interpolation of the observations to the given point.
This uses the Liang and Hale approach [Liang2010]_. The interpolation will fail if
the grid point has no natural neighbors.
Parameters
----------
xp: (N, ) ndarray
x-coordinates of observations
yp: (N, ) ndarray
y-coordinates of observations
variable: (N, ) ndarray
observation values associated with (xp, yp) pairs.
IE, variable[i] is a unique observation at (xp[i], yp[i])
grid_loc: (float, float)
Coordinates of the grid point at which to calculate the
interpolation.
tri: `scipy.spatial.Delaunay`
Delaunay triangulation of the observations.
neighbors: (N, ) ndarray
Simplex codes of the grid point's natural neighbors. The codes
will correspond to codes in the triangulation.
circumcenters: list
Pre-calculated triangle circumcenters for quick look ups. Requires
indices for the list to match the simplices from the Delaunay triangulation.
Returns
-------
value: float
Interpolated value for the grid location
"""
edges = geometry.find_local_boundary(tri, neighbors)
edge_vertices = [segment[0] for segment in geometry.order_edges(edges)]
num_vertices = len(edge_vertices)
p1 = edge_vertices[0]
p2 = edge_vertices[1]
c1 = geometry.circumcenter(grid_loc, tri.points[p1], tri.points[p2])
polygon = [c1]
area_list = []
total_area = 0.0
for i in range(num_vertices):
p3 = edge_vertices[(i + 2) % num_vertices]
try:
c2 = geometry.circumcenter(grid_loc, tri.points[p3], tri.points[p2])
polygon.append(c2)
for check_tri in neighbors:
if p2 in tri.simplices[check_tri]:
polygon.append(circumcenters[check_tri])
pts = [polygon[i] for i in ConvexHull(polygon).vertices]
value = variable[(tri.points[p2][0] == xp) & (tri.points[p2][1] == yp)]
cur_area = geometry.area(pts)
total_area += cur_area
area_list.append(cur_area * value[0])
except (ZeroDivisionError, qhull.QhullError) as e:
message = ('Error during processing of a grid. '
'Interpolation will continue but be mindful '
'of errors in output. ') + str(e)
log.warning(message)
return np.nan
polygon = [c2]
p2 = p3
return sum(x / total_area for x in area_list)
[docs]@exporter.export
def natural_neighbor_to_points(points, values, xi):
r"""Generate a natural neighbor interpolation to the given points.
This assigns values to the given interpolation points using the Liang and Hale
[Liang2010]_. approach.
Parameters
----------
points: array_like, shape (n, 2)
Coordinates of the data points.
values: array_like, shape (n,)
Values of the data points.
xi: array_like, shape (M, 2)
Points to interpolate the data onto.
Returns
-------
img: (M,) ndarray
Array representing the interpolated values for each input point in `xi`
See Also
--------
natural_neighbor_to_grid
"""
tri = Delaunay(points)
members, circumcenters = geometry.find_natural_neighbors(tri, xi)
img = np.empty(shape=(xi.shape[0]), dtype=values.dtype)
img.fill(np.nan)
for ind, (grid, neighbors) in enumerate(members.items()):
if len(neighbors) > 0:
points_transposed = np.array(points).transpose()
img[ind] = natural_neighbor_point(points_transposed[0], points_transposed[1],
values, xi[grid], tri, neighbors, circumcenters)
return img
[docs]@exporter.export
def inverse_distance_to_points(points, values, xi, r, gamma=None, kappa=None, min_neighbors=3,
kind='cressman'):
r"""Generate an inverse distance weighting interpolation to the given points.
Values are assigned to the given interpolation points based on either [Cressman1959]_ or
[Barnes1964]_. The Barnes implementation used here based on [Koch1983]_.
Parameters
----------
points: array_like, shape (n, 2)
Coordinates of the data points.
values: array_like, shape (n,)
Values of the data points.
xi: array_like, shape (M, 2)
Points to interpolate the data onto.
r: float
Radius from grid center, within which observations
are considered and weighted.
gamma: float
Adjustable smoothing parameter for the barnes interpolation. Default None.
kappa: float
Response parameter for barnes interpolation. Default None.
min_neighbors: int
Minimum number of neighbors needed to perform barnes or cressman interpolation
for a point. Default is 3.
kind: str
Specify what inverse distance weighting interpolation to use.
Options: 'cressman' or 'barnes'. Default 'cressman'
Returns
-------
img: (M,) ndarray
Array representing the interpolated values for each input point in `xi`
See Also
--------
inverse_distance_to_grid
"""
obs_tree = cKDTree(points)
indices = obs_tree.query_ball_point(xi, r=r)
img = np.empty(shape=(xi.shape[0]), dtype=values.dtype)
img.fill(np.nan)
for idx, (matches, grid) in enumerate(zip(indices, xi)):
if len(matches) >= min_neighbors:
x1, y1 = obs_tree.data[matches].T
values_subset = values[matches]
dists = geometry.dist_2(grid[0], grid[1], x1, y1)
if kind == 'cressman':
img[idx] = cressman_point(dists, values_subset, r)
elif kind == 'barnes':
img[idx] = barnes_point(dists, values_subset, kappa, gamma)
else:
raise ValueError(str(kind) + ' interpolation not supported.')
return img
[docs]@exporter.export
def interpolate_to_points(points, values, xi, interp_type='linear', minimum_neighbors=3,
gamma=0.25, kappa_star=5.052, search_radius=None, rbf_func='linear',
rbf_smooth=0):
r"""Interpolate unstructured point data to the given points.
This function interpolates the given `values` valid at `points` to the points `xi`. This is
modeled after `scipy.interpolate.griddata`, but acts as a generalization of it by including
the following types of interpolation:
- Linear
- Nearest Neighbor
- Cubic
- Radial Basis Function
- Natural Neighbor (2D Only)
- Barnes (2D Only)
- Cressman (2D Only)
Parameters
----------
points: array_like, shape (n, D)
Coordinates of the data points.
values: array_like, shape (n,)
Values of the data points.
xi: array_like, shape (M, D)
Points to interpolate the data onto.
interp_type: str
What type of interpolation to use. Available options include:
1) "linear", "nearest", "cubic", or "rbf" from `scipy.interpolate`.
2) "natural_neighbor", "barnes", or "cressman" from `metpy.interpolate`.
Default "linear".
minimum_neighbors: int
Minimum number of neighbors needed to perform barnes or cressman interpolation for a
point. Default is 3.
gamma: float
Adjustable smoothing parameter for the barnes interpolation. Default 0.25.
kappa_star: float
Response parameter for barnes interpolation, specified nondimensionally
in terms of the Nyquist. Default 5.052
search_radius: float
A search radius to use for the barnes and cressman interpolation schemes.
If search_radius is not specified, it will default to the average spacing of
observations.
rbf_func: str
Specifies which function to use for Rbf interpolation.
Options include: 'multiquadric', 'inverse', 'gaussian', 'linear', 'cubic',
'quintic', and 'thin_plate'. Defualt 'linear'. See `scipy.interpolate.Rbf` for more
information.
rbf_smooth: float
Smoothing value applied to rbf interpolation. Higher values result in more smoothing.
Returns
-------
values_interpolated: (M,) ndarray
Array representing the interpolated values for each input point in `xi`.
Notes
-----
This function primarily acts as a wrapper for the individual interpolation routines. The
individual functions are also available for direct use.
See Also
--------
interpolate_to_grid
"""
# If this is a type that `griddata` handles, hand it along to `griddata`
if interp_type in ['linear', 'nearest', 'cubic']:
return griddata(points, values, xi, method=interp_type)
# If this is natural neighbor, hand it along to `natural_neighbor`
elif interp_type == 'natural_neighbor':
return natural_neighbor_to_points(points, values, xi)
# If this is Barnes/Cressman, determine search_radios and hand it along to
# `inverse_distance`
elif interp_type in ['cressman', 'barnes']:
ave_spacing = tools.average_spacing(points)
if search_radius is None:
search_radius = 5 * ave_spacing
if interp_type == 'cressman':
return inverse_distance_to_points(points, values, xi, search_radius,
min_neighbors=minimum_neighbors,
kind=interp_type)
else:
kappa = tools.calc_kappa(ave_spacing, kappa_star)
return inverse_distance_to_points(points, values, xi, search_radius, gamma, kappa,
min_neighbors=minimum_neighbors,
kind=interp_type)
# If this is radial basis function, make the interpolator and apply it
elif interp_type == 'rbf':
points_transposed = np.array(points).transpose()
xi_transposed = np.array(xi).transpose()
rbfi = Rbf(points_transposed[0], points_transposed[1], values, function=rbf_func,
smooth=rbf_smooth)
return rbfi(xi_transposed[0], xi_transposed[1])
else:
raise ValueError('Interpolation option not available. '
'Try: linear, nearest, cubic, natural_neighbor, '
'barnes, cressman, rbf')