source: trunk/anuga_core/anuga/fit_interpolate/interpolate2d.py @ 9737

Last change on this file since 9737 was 9737, checked in by steve, 9 years ago

Commit svn after a lot of git updates

File size: 12.1 KB
Line 
1"""Bilinear and piecewise constant interpolation.
2
3* Interpolate from a regular numpy array to arbitrary points.
4* NaN's are taken care of sensibly.
5* Algorithm works both for cartesian (x, y) and geographic (lon, lat)
6  coordinates.
7* Guarantees interpolated values within extrema of grid points, i.e.
8  no overshoot.
9
10See docstring for details on usage and the mathematical derivation.
11
12To test, run the accompanying script test_interpolate2d.py
13
14Author: Ole Nielsen 2011
15
16"""
17__author__ = 'Ole Nielsen <ole.moller.nielsen@gmail.com>'
18__revision__ = '$Format:%H$'
19__date__ = '01/11/2011'
20__license__ = 'GPL v3'
21__copyright__ = 'Copyright 2012, Australia Indonesia Facility for '
22__copyright__ += 'Disaster Reduction'
23
24import numpy
25from anuga.anuga_exceptions import ANUGAError
26
27
28def interpolate2d(x, y, Z, points, mode='linear', bounds_error=False):
29    """Fundamental 2D interpolation routine
30
31    Input
32        x: 1D array of x-coordinates of the mesh on which to interpolate
33        y: 1D array of y-coordinates of the mesh on which to interpolate
34        Z: 2D array of values for each x, y pair
35        points: Nx2 array of coordinates where interpolated values are sought
36        mode: Determines the interpolation order. Options are
37            * 'constant' - piecewise constant nearest neighbour interpolation
38            * 'linear' - bilinear interpolation using the four
39                         nearest neighbours (default)
40        bounds_error: Boolean flag. If True (default) a BoundsError exception
41            will be raised when interpolated values are requested
42            outside the domain of the input data. If False, nan
43            is returned for those values
44
45    Output
46        1D array of length N (same length as points) with interpolated values
47
48    Notes
49        Input coordinates x and y are assumed to be monotonically increasing,
50        but need not be equidistantly spaced.
51
52        Z is assumed to have dimension M x N, where M = len(x) and N = len(y).
53        In other words it is assumed that the x values follow the first
54        (vertical) axis downwards and y values the second (horizontal) axis
55        from left to right.
56
57        If this routine is to be used for interpolation of raster grids where
58        data is typically organised with longitudes (x) going from left to
59        right and latitudes (y) from left to right then user
60        interpolate_raster in this module
61
62    Derivation
63
64        Bilinear interpolation is based on the standard 1D linear interpolation
65        formula:
66
67        Given points (x0, y0) and (x1, y1) and a value of x where
68        x0 <= x <= x1, the linearly interpolated value y at x is given as
69
70        alpha*(y1-y0) + y0
71
72        or
73
74        alpha*y1 + (1-alpha)*y0                (1)
75
76        where alpha = (x-x0)/(x1-x0)           (1a)
77
78
79        2D bilinear interpolation aims at obtaining an interpolated value z at
80        a point (x,y) which lies inside a square formed by points (x0, y0),
81        (x1, y0), (x0, y1) and (x1, y1) for which values z00, z10, z01 and
82        z11 are known.
83
84        This is obtained by applying equation (1) twice  in the
85        x-direction to obtain interpolated points q0 and q1 for (x, y0) and
86        (x, y1), respectively.
87
88        q0 = alpha*z10 + (1-alpha)*z00         (2)
89
90        and
91
92        q1 = alpha*z11 + (1-alpha)*z01         (3)
93
94
95        Then using equation (1) in the y-direction on the results from (2)
96        and (3)
97
98        z = beta*q1 + (1-beta)*q0              (4)
99
100        where beta = (y-y0)/(y1-y0)            (4a)
101
102
103        Substituting (2) and (3) into (4) yields
104
105        z = alpha*beta*z11 + beta*z01 - alpha*beta*z01 +
106            alpha*z10 + z00 - alpha*z00 - alpha*beta*z10 - beta*z00 +
107            alpha*beta*z00
108          = alpha*beta*(z11 - z01 - z10 + z00) +
109            alpha*(z10 - z00) + beta*(z01 - z00) + z00
110
111        which can be further simplified to
112
113        z = alpha*beta*(z11 - dx - dy - z00) + alpha*dx + beta*dy + z00  (5)
114
115        where
116        dx = z10 - z00
117        dy = z01 - z00
118
119        Equation (5) is what is implemented in the function interpolate2d.
120
121
122        Piecewise constant interpolation can be implemented using the same
123        coefficients (1a) and (4a) that are used for bilinear interpolation
124        as they are a measure of the relative distance to the left and lower
125        neigbours. A value of 0 will pick the left or lower bound whereas a
126        value of 1 will pick the right or higher bound. Hence z can be
127        assigned to its nearest neigbour as follows
128
129            | z00   alpha < 0.5 and beta < 0.5    # lower left corner
130            |
131            | z10   alpha >= 0.5 and beta < 0.5   # lower right corner
132        z = |
133            | z01   alpha < 0.5 and beta >= 0.5   # upper left corner
134            |
135            | z11   alpha >= 0.5 and beta >= 0.5  # upper right corner
136
137    """
138
139    # Check inputs and provide xi, eta as x and y coordinates from
140    # points vector
141    x, y, Z, xi, eta = check_inputs(x, y, Z, points, mode, bounds_error)
142
143    # Identify elements that are outside interpolation domain or NaN
144    oldset = numpy.seterr(invalid='ignore')  # Suppress comparison with nan warning
145    outside = (xi < x[0]) + (eta < y[0]) + (xi > x[-1]) + (eta > y[-1])
146    outside += numpy.isnan(xi) + numpy.isnan(eta)
147    numpy.seterr(**oldset)  # Restore warnings
148   
149    # Restrict interpolation points to those that are inside the grid
150    inside = numpy.logical_not(outside)  # Invert boolean array to find elements inside
151    xi = xi[inside]
152    eta = eta[inside]
153
154    # Find upper neighbours for each interpolation point
155    # ('left' means first occurrence)
156    idx = numpy.searchsorted(x, xi, side='left')
157    idy = numpy.searchsorted(y, eta, side='left')
158
159    # Get the four neighbours for each interpolation point
160    x0 = x[idx - 1]  # Left
161    x1 = x[idx]      # Right
162    y0 = y[idy - 1]  # Lower
163    y1 = y[idy]      # Upper
164
165    # And the corresponding four grid values
166    z00 = Z[idx - 1, idy - 1]
167    z01 = Z[idx - 1, idy]
168    z10 = Z[idx, idy - 1]
169    z11 = Z[idx, idy]
170
171    # Coefficients for weighting between lower and upper bounds
172    oldset = numpy.seterr(invalid='ignore')  # Suppress zero division warning
173    alpha = (xi - x0) / (x1 - x0)
174    beta = (eta - y0) / (y1 - y0)
175    numpy.seterr(**oldset)  # Restore warnings
176
177    if mode == 'linear':
178        # Bilinear interpolation formula as per equation (5) above
179        dx = z10 - z00
180        dy = z01 - z00
181        z = z00 + alpha * dx + beta * dy + alpha * beta * (z11 - dx - dy - z00)
182    else:
183        # Piecewise constant (as verified in input_check)
184
185        # Set up masks for the quadrants
186        left = alpha < 0.5
187        right = numpy.logical_not(left)
188        lower = beta < 0.5
189        upper = numpy.logical_not(lower)
190
191        lower_left = lower * left
192        lower_right = lower * right
193        upper_left = upper * left
194
195        # Initialise result array with all elements set to upper right
196        z = z11
197
198        # Then set the other quadrants
199        z[lower_left] = z00[lower_left]
200        z[lower_right] = z10[lower_right]
201        z[upper_left] = z01[upper_left]
202
203    # Populate result with interpolated values for points inside domain
204    # and NaN for values outside
205    r = numpy.zeros(len(points))
206    r[inside] = z
207    r[outside] = numpy.nan
208
209    # Return interpolated points
210    return r
211
212def interpolate_raster(x, y, z, points, mode='linear', bounds_error=False):
213    """2D interpolation of raster data
214    It is assumed that data is organised in matrix z as latitudes from
215    bottom up along the first dimension and longitudes from west to east
216    along the second dimension.
217    Further it is assumed that x is the vector of longitudes and y the
218    vector of latitudes.
219    See interpolate2d for details of the interpolation routine
220    :param x: 1D array of x-coordinates of the mesh on which to interpolate
221    :type x: numpy.ndarray
222    :param y: 1D array of y-coordinates of the mesh on which to interpolate
223    :type y: numpy.ndarray
224    :param z: 2D array of values for each x, y pair
225    :type z: numpy.ndarry
226    :param points: Nx2 array of coordinates where interpolated values are
227        sought
228    :type points: numpy.narray
229    :param mode: Determines the interpolation order.
230        Options are:
231            * 'constant' - piecewise constant nearest neighbour interpolation
232            * 'linear' - bilinear interpolation using the four
233              nearest neighbours (default)
234    :type mode: str
235    :param bounds_error: If True (default) a BoundsError exception
236          will be raised when interpolated values are requested
237          outside the domain of the input data. If False, nan
238          is returned for those values
239    :type bounds_error: bool
240    :returns: 1D array with same length as points with interpolated values
241    :raises: Exception, BoundsError (see note about bounds_error)
242    """
243
244    # Flip matrix z up-down to interpret latitudes ordered from south to north
245    z = numpy.flipud(z)
246
247    # Transpose z to have y coordinates along the first axis and x coordinates
248    # along the second axis
249    # noinspection PyUnresolvedReferences
250    z = z.transpose()
251
252    # Call underlying interpolation routine and return
253    res = interpolate2d(x, y, z, points, mode=mode, bounds_error=bounds_error)
254    return res
255
256#------------------------
257# Auxiliary functionality
258#------------------------
259def check_inputs(x, y, Z, points, mode, bounds_error):
260    """Check inputs for interpolate2d function
261    """
262
263    msg = ('Only mode "linear" and "constant" are implemented. '
264           'I got "%s"' % mode)
265    if mode not in ['linear', 'constant']:
266        raise ANUGAError(msg)
267
268    x = numpy.array(x)
269
270    try:
271        y = numpy.array(y)
272    except Exception, e:
273        msg = ('Input vector y could not be converted to numpy array: '
274               '%s' % str(e))
275        raise Exception(msg)
276
277    msg = ('Input vector x must be monotonically increasing. I got '
278           'min(x) == %.15f, but x[0] == %.15f' % (min(x), x[0]))
279    if not min(x) == x[0]:
280        raise ANUGAError(msg)
281
282    msg = ('Input vector y must be monotoneously increasing. '
283           'I got min(y) == %.15f, but y[0] == %.15f' % (min(y), y[0]))
284    if not min(y) == y[0]:
285        raise ANUGAError(msg)
286
287    msg = ('Input vector x must be monotoneously increasing. I got '
288           'max(x) == %.15f, but x[-1] == %.15f' % (max(x), x[-1]))
289    if not max(x) == x[-1]:
290        raise ANUGAError(msg)
291
292    msg = ('Input vector y must be monotoneously increasing. I got '
293           'max(y) == %.15f, but y[-1] == %.15f' % (max(y), y[-1]))
294    if not max(y) == y[-1]:
295        raise ANUGAError(msg)
296
297    try:
298        Z = numpy.array(Z)
299        m, n = Z.shape
300    except Exception, e:
301        msg = 'Z must be a 2D numpy array: %s' % str(e)
302        raise Exception(msg)
303
304    Nx = len(x)
305    Ny = len(y)
306
307    msg =  ('Input array Z must have dimensions %i x %i corresponding to the '
308            'lengths of the input coordinates x and y. However, '
309            'Z has dimensions %i x %i.' % (Nx, Ny, m, n))
310    if not (Nx == m and Ny == n):
311        raise ANUGAError(msg)
312
313    # Get interpolation points
314    points = numpy.array(points)
315    xi = points[:, 0]
316    eta = points[:, 1]
317
318    if bounds_error:
319        xi0 = numpy.nanmin(xi)
320        xi1 = numpy.nanmax(xi)
321        eta0 = numpy.nanmin(eta)
322        eta1 = numpy.nanmax(eta)
323
324        msg = ('Interpolation point xi=%f was less than the smallest '
325               'value in domain (x=%f) and bounds_error was requested.'
326               % (xi0, x[0]))
327        if xi0 < x[0]:
328            raise BoundsError(msg)
329
330        msg = ('Interpolation point xi=%f was greater than the largest '
331               'value in domain (x=%f) and bounds_error was requested.'
332               % (xi1, x[-1]))
333        if xi1 > x[-1]:
334            raise BoundsError(msg)
335
336        msg = ('Interpolation point eta=%f was less than the smallest '
337               'value in domain (y=%f) and bounds_error was requested.'
338               % (eta0, y[0]))
339        if eta0 < y[0]:
340            raise BoundsError(msg)
341
342        msg = ('Interpolation point eta=%f was greater than the largest '
343               'value in domain (y=%f) and bounds_error was requested.'
344               % (eta1, y[-1]))
345        if eta1 > y[-1]:
346            raise BoundsError(msg)
347
348    return x, y, Z, xi, eta
Note: See TracBrowser for help on using the repository browser.