Completed
Pull Request — master (#468)
by
unknown
01:19
created

broadcast_indices()   A

Complexity

Conditions 3

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
c 1
b 0
f 0
dl 0
loc 15
rs 9.4285
1
# Copyright (c) 2008-2017 MetPy Developers.
2
# Distributed under the terms of the BSD 3-Clause License.
3
# SPDX-License-Identifier: BSD-3-Clause
4
"""Contains a collection of generally useful calculation tools."""
5
6
import functools
7
import warnings
8
9
import numpy as np
10
import numpy.ma as ma
11
from scipy.spatial import cKDTree
12
13
from ..package_tools import Exporter
14
from ..units import units
15
16
exporter = Exporter(globals())
17
18
19
@exporter.export
20
def resample_nn_1d(a, centers):
21
    """Return one-dimensional nearest-neighbor indexes based on user-specified centers.
22
23
    Parameters
24
    ----------
25
    a : array-like
26
        1-dimensional array of numeric values from which to
27
        extract indexes of nearest-neighbors
28
    centers : array-like
29
        1-dimensional array of numeric values representing a subset of values to approximate
30
31
    Returns
32
    -------
33
        An array of indexes representing values closest to given array values
34
35
    """
36
    ix = []
37
    for center in centers:
38
        index = (np.abs(a - center)).argmin()
39
        if index not in ix:
40
            ix.append(index)
41
    return ix
42
43
44
@exporter.export
45
def nearest_intersection_idx(a, b):
46
    """Determine the index of the point just before two lines with common x values.
47
48
    Parameters
49
    ----------
50
    a : array-like
51
        1-dimensional array of y-values for line 1
52
    b : array-like
53
        1-dimensional array of y-values for line 2
54
55
    Returns
56
    -------
57
        An array of indexes representing the index of the values
58
        just before the intersection(s) of the two lines.
59
60
    """
61
    # Difference in the two y-value sets
62
    difference = a - b
63
64
    # Determine the point just before the intersection of the lines
65
    # Will return multiple points for multiple intersections
66
    sign_change_idx, = np.nonzero(np.diff(np.sign(difference)))
67
68
    return sign_change_idx
69
70
71
@exporter.export
72
def find_intersections(x, a, b, direction='all'):
73
    """Calculate the best estimate of intersection.
74
75
    Calculates the best estimates of the intersection of two y-value
76
    data sets that share a common x-value set.
77
78
    Parameters
79
    ----------
80
    x : array-like
81
        1-dimensional array of numeric x-values
82
    a : array-like
83
        1-dimensional array of y-values for line 1
84
    b : array-like
85
        1-dimensional array of y-values for line 2
86
    direction : string
87
        specifies direction of crossing. 'all', 'increasing' (a becoming greater than b),
88
        or 'decreasing' (b becoming greater than a).
89
90
    Returns
91
    -------
92
        A tuple (x, y) of array-like with the x and y coordinates of the
93
        intersections of the lines.
94
95
    """
96
    # Find the index of the points just before the intersection(s)
97
    nearest_idx = nearest_intersection_idx(a, b)
98
    next_idx = nearest_idx + 1
99
100
    # Determine the sign of the change
101
    sign_change = np.sign(a[next_idx] - b[next_idx])
102
103
    # x-values around each intersection
104
    _, x0 = _next_non_masked_element(x, nearest_idx)
105
    _, x1 = _next_non_masked_element(x, next_idx)
106
107
    # y-values around each intersection for the first line
108
    _, a0 = _next_non_masked_element(a, nearest_idx)
109
    _, a1 = _next_non_masked_element(a, next_idx)
110
111
    # y-values around each intersection for the second line
112
    _, b0 = _next_non_masked_element(b, nearest_idx)
113
    _, b1 = _next_non_masked_element(b, next_idx)
114
115
    # Calculate the x-intersection. This comes from finding the equations of the two lines,
116
    # one through (x0, a0) and (x1, a1) and the other through (x0, b0) and (x1, b1),
117
    # finding their intersection, and reducing with a bunch of algebra.
118
    delta_y0 = a0 - b0
119
    delta_y1 = a1 - b1
120
    intersect_x = (delta_y1 * x0 - delta_y0 * x1) / (delta_y1 - delta_y0)
121
122
    # Calculate the y-intersection of the lines. Just plug the x above into the equation
123
    # for the line through the a points. One could solve for y like x above, but this
124
    # causes weirder unit behavior and seems a little less good numerically.
125
    intersect_y = ((intersect_x - x0) / (x1 - x0)) * (a1 - a0) + a0
126
127
    # Make a mask based on the direction of sign change desired
128
    if direction == 'increasing':
129
        mask = sign_change > 0
130
    elif direction == 'decreasing':
131
        mask = sign_change < 0
132
    elif direction == 'all':
133
        return intersect_x, intersect_y
134
    else:
135
        raise ValueError('Unknown option for direction: {0}'.format(str(direction)))
136
    return intersect_x[mask], intersect_y[mask]
137
138
139
@exporter.export
140
def interpolate_nans(x, y, kind='linear'):
141
    """Interpolate NaN values in y.
142
143
    Interpolate NaN values in the y dimension. Works with unsorted x values.
144
145
    Parameters
146
    ----------
147
    x : array-like
148
        1-dimensional array of numeric x-values
149
    y : array-like
150
        1-dimensional array of numeric y-values
151
    kind : string
152
        specifies the kind of interpolation x coordinate - 'linear' or 'log'
153
154
    Returns
155
    -------
156
        An array of the y coordinate data with NaN values interpolated.
157
158
    """
159
    x_sort_args = np.argsort(x)
160
    x = x[x_sort_args]
161
    y = y[x_sort_args]
162
    nans = np.isnan(y)
163
    if kind == 'linear':
164
        y[nans] = np.interp(x[nans], x[~nans], y[~nans])
165
    elif kind == 'log':
166
        y[nans] = np.interp(np.log(x[nans]), np.log(x[~nans]), y[~nans])
167
    else:
168
        raise ValueError('Unknown option for kind: {0}'.format(str(kind)))
169
    return y[x_sort_args]
170
171
172
def _next_non_masked_element(a, idx):
173
    """Return the next non masked element of a masked array.
174
175
    If an array is masked, return the next non-masked element (if the given index is masked).
176
    If no other unmasked points are after the given masked point, returns none.
177
178
    Parameters
179
    ----------
180
    a : array-like
181
        1-dimensional array of numeric values
182
    idx : integer
183
        index of requested element
184
185
    Returns
186
    -------
187
        Index of next non-masked element and next non-masked element
188
189
    """
190
    try:
191
        next_idx = idx + a[idx:].mask.argmin()
192
        if ma.is_masked(a[next_idx]):
193
            return None, None
194
        else:
195
            return next_idx, a[next_idx]
196
    except (AttributeError, TypeError, IndexError):
197
        return idx, a[idx]
198
199
200
def delete_masked_points(*arrs):
201
    """Delete masked points from arrays.
202
203
    Takes arrays and removes masked points to help with calculations and plotting.
204
205
    Parameters
206
    ----------
207
    arrs : one or more array-like
208
        source arrays
209
210
    Returns
211
    -------
212
    arrs : one or more array-like
213
        arrays with masked elements removed
214
215
    """
216
    if any(hasattr(a, 'mask') for a in arrs):
217
        keep = ~functools.reduce(np.logical_or, (np.ma.getmaskarray(a) for a in arrs))
218
        return tuple(ma.asarray(a[keep]) for a in arrs)
219
    else:
220
        return arrs
221
222
223
@exporter.export
224
def reduce_point_density(points, radius, priority=None):
225
    r"""Return a mask to reduce the density of points in irregularly-spaced data.
226
227
    This function is used to down-sample a collection of scattered points (e.g. surface
228
    data), returning a mask that can be used to select the points from one or more arrays
229
    (e.g. arrays of temperature and dew point). The points selected can be controlled by
230
    providing an array of ``priority`` values (e.g. rainfall totals to ensure that
231
    stations with higher precipitation remain in the mask).
232
233
    Parameters
234
    ----------
235
    points : (N, K) array-like
236
        N locations of the points in K dimensional space
237
    radius : float
238
        minimum radius allowed between points
239
    priority : (N, K) array-like, optional
240
        If given, this should have the same shape as ``points``; these values will
241
        be used to control selection priority for points.
242
243
    Returns
244
    -------
245
        (N,) array-like of boolean values indicating whether points should be kept. This
246
        can be used directly to index numpy arrays to return only the desired points.
247
248
    Examples
249
    --------
250
    >>> metpy.calc.reduce_point_density(np.array([1, 2, 3]), 1.)
251
    array([ True, False,  True], dtype=bool)
252
    >>> metpy.calc.reduce_point_density(np.array([1, 2, 3]), 1.,
253
    ... priority=np.array([0.1, 0.9, 0.3]))
254
    array([False,  True, False], dtype=bool)
255
256
    """
257
    # Handle 1D input
258
    if points.ndim < 2:
259
        points = points.reshape(-1, 1)
260
261
    # Make a kd-tree to speed searching of data.
262
    tree = cKDTree(points)
263
264
    # Need to use sorted indices rather than sorting the position
265
    # so that the keep mask matches *original* order.
266
    if priority is not None:
267
        # Need to sort the locations in decreasing priority.
268
        sorted_indices = np.argsort(priority)[::-1]
269
    else:
270
        # Take advantage of iterator nature of range here to avoid making big lists
271
        sorted_indices = range(len(points))
272
273
    # Keep all points initially
274
    keep = np.ones(len(points), dtype=np.bool)
275
276
    # Loop over all the potential points
277
    for ind in sorted_indices:
278
        # Only proceed if we haven't already excluded this point
279
        if keep[ind]:
280
            # Find the neighbors and eliminate them
281
            neighbors = tree.query_ball_point(points[ind], radius)
282
            keep[neighbors] = False
283
284
            # We just removed ourselves, so undo that
285
            keep[ind] = True
286
287
    return keep
288
289
290
@exporter.export
291
@units.wraps(None, ('=A', '=A', None))
292
def log_interp(x, xp, *args, **kwargs):
293
    r"""Interpolates data with logarithmic x-scale over a specified axis.
294
295
    Interpolation on a logarithmic x-scale for interpolation values in pressure coordintates.
296
297
    Parameters
298
    ----------
299
    x : array-like
300
        1-D array of desired interpolated values.
301
302
    xp : array-like
303
        The x-coordinates of the data points.
304
305
    args : array-like
306
        The y-coordinates of the data points, same shape as xp.
307
308
    axis : int, optional
309
        The axis to interpolate over. Defaults to 0.
310
311
    fill_value: float, optional
312
        Specify handling of interpolation points out of data bounds. If None, will return
313
        ValueError if points are out of bounds. Defaults to nan.
314
315
    Returns
316
    -------
317
    array-like
318
        Interpolated values for each point with coordinates sorted in ascending order.
319
320
    Examples
321
     --------
322
     >>> x_log = np.array([1e3, 1e4, 1e5, 1e6])
323
     >>> y_log = np.log(x_log) * 2 + 3
324
     >>> x_interp = np.array([5e3, 5e4, 5e5])
325
     >>> metpy.calc.log_interp(x_interp, x_log, y_log)
326
     array([ 20.03438638,  24.63955657,  29.24472675])
327
328
    Notes
329
    -----
330
    xp and args must be the same shape and have the same units.
331
332
    """
333
    # Pull out keyword args
334
    fill_value = kwargs.pop('fill_value', np.nan)
335
    axis = kwargs.pop('axis', 0)
336
337
    # Save number of dimensions in xp
338
    ndim = xp.ndim
339
340
    # Sort input data
341
    sort_args = np.argsort(xp, axis=axis)
342
    sort_x = np.argsort(x)
343
344
    # indices for sorting
345
    sorter = broadcast_indices(xp, sort_args, ndim, axis)
346
347
    # Ensure pressure in increasing order
348
    variables = [arr[sorter] for arr in args]
349
350
    # Make x broadcast with xp
351
    x_array = x[sort_x]
352
    expand = [np.newaxis] * ndim
353
    expand[axis] = slice(None)
354
    x_array = x_array[expand]
355
356
    # Log x and xp
357
    log_x_array = np.log(x_array)
358
    log_xp = np.log(xp[sorter])
359
360
    # Calculate value above interpolated value
361
    minv = np.apply_along_axis(np.searchsorted, axis, xp[sorter], x[sort_x])
362
    minv2 = np.copy(minv)
363
364
    # If extrap is none and data is out of bounds, raise value error
365
    if ((np.max(minv) == xp.shape[axis]) or (np.min(minv) == 0)) and fill_value is None:
366
        raise ValueError('Interpolation point out of data bounds encountered')
367
368
    # Warn if interpolated values are outside data bounds, will make these the values
369
    # at end of data range.
370
    if np.max(minv) == xp.shape[axis]:
371
        warnings.warn('Interpolation point out of data bounds encountered')
372
        minv2[minv == xp.shape[axis]] = xp.shape[axis] - 1
373
    if np.max(minv) == 0:
374
        warnings.warn('Interpolation point out of data bounds encountered')
375
        minv2[minv == 0] = 1
376
377
    # Get indicies for broadcasting arrays
378
    above = broadcast_indices(xp, minv2, ndim, axis)
379
    below = broadcast_indices(xp, minv2 - 1, ndim, axis)
380
381
    # Create empty output list
382
    ret = []
383
384
    # Calculate interpolation for each variable
385
    for var in variables:
386
        var_interp = var[below] + ((log_x_array - log_xp[below]) /
387
                                   (log_xp[above] - log_xp[below])) * (var[above] -
388
                                                                       var[below])
389
390
        # set to nan if fill_value = nan
391
        var_interp[minv == xp.shape[axis]] = fill_value
392
        var_interp[minv == 0] = fill_value
393
394
        # Output to list
395
        ret.append(var_interp)
396
    if len(ret) == 1:
397
        return ret[0]
398
    else:
399
        return ret
400
401
402
def broadcast_indices(x, minv, ndim, axis):
403
    """Calculate index values to properly broadcast index array within data array.
404
405
    See usage in log_interp.
406
    """
407
    ret = []
408
    for dim in range(ndim):
409
        if dim == axis:
410
            ret.append(minv)
411
        else:
412
            broadcast_slice = [np.newaxis] * ndim
413
            broadcast_slice[dim] = slice(None)
414
            dim_inds = np.arange(x.shape[dim])
415
            ret.append(dim_inds[broadcast_slice])
416
    return ret
417