Completed
Pull Request — master (#468)
by
unknown
59s
created

_next_non_masked_element()   B

Complexity

Conditions 3

Size

Total Lines 26

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