Completed
Pull Request — master (#453)
by
unknown
01:32
created

log_interp()   B

Complexity

Conditions 4

Size

Total Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
c 2
b 0
f 0
dl 0
loc 45
rs 8.5806
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
8
import numpy as np
9
import numpy.ma as ma
10
from scipy.spatial import cKDTree
11
12
from ..package_tools import Exporter
13
14
exporter = Exporter(globals())
15
16
17
@exporter.export
18
def resample_nn_1d(a, centers):
19
    """Return one-dimensional nearest-neighbor indexes based on user-specified centers.
20
21
    Parameters
22
    ----------
23
    a : array-like
24
        1-dimensional array of numeric values from which to
25
        extract indexes of nearest-neighbors
26
    centers : array-like
27
        1-dimensional array of numeric values representing a subset of values to approximate
28
29
    Returns
30
    -------
31
        An array of indexes representing values closest to given array values
32
33
    """
34
    ix = []
35
    for center in centers:
36
        index = (np.abs(a - center)).argmin()
37
        if index not in ix:
38
            ix.append(index)
39
    return ix
40
41
42
@exporter.export
43
def nearest_intersection_idx(a, b):
44
    """Determine the index of the point just before two lines with common x values.
45
46
    Parameters
47
    ----------
48
    a : array-like
49
        1-dimensional array of y-values for line 1
50
    b : array-like
51
        1-dimensional array of y-values for line 2
52
53
    Returns
54
    -------
55
        An array of indexes representing the index of the values
56
        just before the intersection(s) of the two lines.
57
58
    """
59
    # Difference in the two y-value sets
60
    difference = a - b
61
62
    # Determine the point just before the intersection of the lines
63
    # Will return multiple points for multiple intersections
64
    sign_change_idx, = np.nonzero(np.diff(np.sign(difference)))
65
66
    return sign_change_idx
67
68
69
@exporter.export
70
def find_intersections(x, a, b, direction='all'):
71
    """Calculate the best estimate of intersection.
72
73
    Calculates the best estimates of the intersection of two y-value
74
    data sets that share a common x-value set.
75
76
    Parameters
77
    ----------
78
    x : array-like
79
        1-dimensional array of numeric x-values
80
    a : array-like
81
        1-dimensional array of y-values for line 1
82
    b : array-like
83
        1-dimensional array of y-values for line 2
84
    direction : string
85
        specifies direction of crossing. 'all', 'increasing' (a becoming greater than b),
86
        or 'decreasing' (b becoming greater than a).
87
88
    Returns
89
    -------
90
        A tuple (x, y) of array-like with the x and y coordinates of the
91
        intersections of the lines.
92
93
    """
94
    # Find the index of the points just before the intersection(s)
95
    nearest_idx = nearest_intersection_idx(a, b)
96
    next_idx = nearest_idx + 1
97
98
    # Determine the sign of the change
99
    sign_change = np.sign(a[next_idx] - b[next_idx])
100
101
    # x-values around each intersection
102
    _, x0 = _next_non_masked_element(x, nearest_idx)
103
    _, x1 = _next_non_masked_element(x, next_idx)
104
105
    # y-values around each intersection for the first line
106
    _, a0 = _next_non_masked_element(a, nearest_idx)
107
    _, a1 = _next_non_masked_element(a, next_idx)
108
109
    # y-values around each intersection for the second line
110
    _, b0 = _next_non_masked_element(b, nearest_idx)
111
    _, b1 = _next_non_masked_element(b, next_idx)
112
113
    # Calculate the x-intersection. This comes from finding the equations of the two lines,
114
    # one through (x0, a0) and (x1, a1) and the other through (x0, b0) and (x1, b1),
115
    # finding their intersection, and reducing with a bunch of algebra.
116
    delta_y0 = a0 - b0
117
    delta_y1 = a1 - b1
118
    intersect_x = (delta_y1 * x0 - delta_y0 * x1) / (delta_y1 - delta_y0)
119
120
    # Calculate the y-intersection of the lines. Just plug the x above into the equation
121
    # for the line through the a points. One could solve for y like x above, but this
122
    # causes weirder unit behavior and seems a little less good numerically.
123
    intersect_y = ((intersect_x - x0) / (x1 - x0)) * (a1 - a0) + a0
124
125
    # Make a mask based on the direction of sign change desired
126
    if direction == 'increasing':
127
        mask = sign_change > 0
128
    elif direction == 'decreasing':
129
        mask = sign_change < 0
130
    elif direction == 'all':
131
        return intersect_x, intersect_y
132
    else:
133
        raise ValueError('Unknown option for direction: {0}'.format(str(direction)))
134
    return intersect_x[mask], intersect_y[mask]
135
136
137
@exporter.export
138
def interpolate_nans(x, y, kind='linear'):
139
    """Interpolate NaN values in y.
140
141
    Interpolate NaN values in the y dimension. Works with unsorted x values.
142
143
    Parameters
144
    ----------
145
    x : array-like
146
        1-dimensional array of numeric x-values
147
    y : array-like
148
        1-dimensional array of numeric y-values
149
    kind : string
150
        specifies the kind of interpolation x coordinate - 'linear' or 'log'
151
152
    Returns
153
    -------
154
        An array of the y coordinate data with NaN values interpolated.
155
156
    """
157
    x_sort_args = np.argsort(x)
158
    x = x[x_sort_args]
159
    y = y[x_sort_args]
160
    nans = np.isnan(y)
161
    if kind is 'linear':
162
        y[nans] = np.interp(x[nans], x[~nans], y[~nans])
163
    elif kind is 'log':
164
        y[nans] = np.interp(np.log(x[nans]), np.log(x[~nans]), y[~nans])
165
    else:
166
        raise ValueError('Unknown option for kind: {0}'.format(str(kind)))
167
    return y[x_sort_args]
168
169
170
def _next_non_masked_element(a, idx):
171
    """Return the next non masked element of a masked array.
172
173
    If an array is masked, return the next non-masked element (if the given index is masked).
174
    If no other unmasked points are after the given masked point, returns none.
175
176
    Parameters
177
    ----------
178
    a : array-like
179
        1-dimensional array of numeric values
180
    idx : integer
181
        index of requested element
182
183
    Returns
184
    -------
185
        Index of next non-masked element and next non-masked element
186
187
    """
188
    try:
189
        next_idx = idx + a[idx:].mask.argmin()
190
        if ma.is_masked(a[next_idx]):
191
            return None, None
192
        else:
193
            return next_idx, a[next_idx]
194
    except (AttributeError, TypeError, IndexError):
195
        return idx, a[idx]
196
197
198
def delete_masked_points(*arrs):
199
    """Delete masked points from arrays.
200
201
    Takes arrays and removes masked points to help with calculations and plotting.
202
203
    Parameters
204
    ----------
205
    arrs : one or more array-like
206
        source arrays
207
208
    Returns
209
    -------
210
    arrs : one or more array-like
211
        arrays with masked elements removed
212
213
    """
214
    if any(hasattr(a, 'mask') for a in arrs):
215
        keep = ~functools.reduce(np.logical_or, (np.ma.getmaskarray(a) for a in arrs))
216
        return tuple(ma.asarray(a[keep]) for a in arrs)
217
    else:
218
        return arrs
219
220
221
@exporter.export
222
def reduce_point_density(points, radius, priority=None):
223
    r"""Return a mask to reduce the density of points in irregularly-spaced data.
224
225
    This function is used to down-sample a collection of scattered points (e.g. surface
226
    data), returning a mask that can be used to select the points from one or more arrays
227
    (e.g. arrays of temperature and dew point). The points selected can be controlled by
228
    providing an array of ``priority`` values (e.g. rainfall totals to ensure that
229
    stations with higher precipitation remain in the mask).
230
231
    Parameters
232
    ----------
233
    points : (N, K) array-like
234
        N locations of the points in K dimensional space
235
    radius : float
236
        minimum radius allowed between points
237
    priority : (N, K) array-like, optional
238
        If given, this should have the same shape as ``points``; these values will
239
        be used to control selection priority for points.
240
241
    Returns
242
    -------
243
        (N,) array-like of boolean values indicating whether points should be kept. This
244
        can be used directly to index numpy arrays to return only the desired points.
245
246
    Examples
247
    --------
248
    >>> metpy.calc.reduce_point_density(np.array([1, 2, 3]), 1.)
249
    array([ True, False,  True], dtype=bool)
250
    >>> metpy.calc.reduce_point_density(np.array([1, 2, 3]), 1.,
251
    ... priority=np.array([0.1, 0.9, 0.3]))
252
    array([False,  True, False], dtype=bool)
253
254
    """
255
    # Handle 1D input
256
    if points.ndim < 2:
257
        points = points.reshape(-1, 1)
258
259
    # Make a kd-tree to speed searching of data.
260
    tree = cKDTree(points)
261
262
    # Need to use sorted indices rather than sorting the position
263
    # so that the keep mask matches *original* order.
264
    if priority is not None:
265
        # Need to sort the locations in decreasing priority.
266
        sorted_indices = np.argsort(priority)[::-1]
267
    else:
268
        # Take advantage of iterator nature of range here to avoid making big lists
269
        sorted_indices = range(len(points))
270
271
    # Keep all points initially
272
    keep = np.ones(len(points), dtype=np.bool)
273
274
    # Loop over all the potential points
275
    for ind in sorted_indices:
276
        # Only proceed if we haven't already excluded this point
277
        if keep[ind]:
278
            # Find the neighbors and eliminate them
279
            neighbors = tree.query_ball_point(points[ind], radius)
280
            keep[neighbors] = False
281
282
            # We just removed ourselves, so undo that
283
            keep[ind] = True
284
285
    return keep
286
287
288
@exporter.export
289
def log_interp(x, xp, fp, **kwargs):
290
    r"""Interpolates data with logarithmic x-scale.
291
292
    Interpolation on a logarithmic x-scale for interpolation values in pressure coordintates.
293
294
    Parameters
295
    ----------
296
    x : array-like
297
        The x-coordinates of the interpolated values.
298
299
    xp : array-like
300
        The x-coordinates of the data points.
301
302
    fp : array-like
303
        The y-coordinates of the data points, same length as xp.
304
305
    Returns
306
    -------
307
    array-like
308
        The interpolated values, same shape as x.
309
310
    Examples
311
    --------
312
    >>> x_log = np.array([1e3, 1e4, 1e5, 1e6])
313
    >>> y_log = np.log(x_log) * 2 + 3
314
    >>> x_interp = np.array([5e3, 5e4, 5e5])
315
    >>> metpy.calc.log_interp(x_interp, x_log, y_log)
316
    array([ 20.03438638,  24.63955657,  29.24472675])
317
318
    """
319
    sort_args = np.argsort(xp)
320
321
    if hasattr(x, 'units'):
322
        x = x.m
323
324
    if hasattr(xp, 'units'):
325
        xp = xp.m
326
327
    interpolated_vals = np.interp(np.log(x), np.log(xp[sort_args]), fp[sort_args], **kwargs)
328
329
    if hasattr(fp, 'units'):
330
        interpolated_vals = interpolated_vals * fp.units
331
332
    return interpolated_vals
333