Completed
Pull Request — master (#444)
by
unknown
01:26
created

mixed_layer()   B

Complexity

Conditions 1

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
c 2
b 0
f 0
dl 0
loc 30
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
8
import numpy as np
9
import numpy.ma as ma
10
from scipy.spatial import cKDTree
11
12
from ..calc import pressure_at_height_above_pressure
13
from ..package_tools import Exporter
14
from ..units import check_units, 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 is 'linear':
164
        y[nans] = np.interp(x[nans], x[~nans], y[~nans])
165
    elif kind is '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
@check_units('[pressure]')
292
def get_layer(p, datavar, bottom=None, depth=100 * units.hPa, interpolate=True):
293
    r"""Return an atmospheric layer from upper air data with the requested bottom and depth.
294
295
    This function will subset an upper air dataset to contain only the specified layer. The
296
    bottom of the layer can be specified with a pressure or height above the surface
297
    pressure. The bottom defaults to the surface pressure. The depth of the layer can be
298
    specified in terms of pressure or height above the bottom of the layer. If the top and
299
    bottom of the layer are not in the data, they are interpolated by default.
300
301
    Parameters
302
    ----------
303
    p : array-like
304
        Atmospheric pressure profile
305
    datavar : array-like
306
        Atmospheric variable measured at the given pressures
307
    bottom : `pint.Quantity`
308
        The bottom of the layer as a pressure or height above the surface pressure
309
    depth : `pint.Quantity`
310
        The thickness of the layer as a pressure or height above the bottom of the layer
311
    interpolate : bool
312
        Interpolate the top and bottom points if they are not in the given data
313
314
    Returns
315
    -------
316
    `pint.Quantity, pint.Quantity`
317
        The pressure and data variable of the layer
318
319
    """
320
    # Make sure pressure and datavar are the same length
321
    if len(p) != len(datavar):
322
        raise ValueError('Pressure and data variable must have the same length.')
323
324
    # If no bottom is specified, use the first pressure value
325
    if not bottom:
326
        bottom_pressure = p[0]
327
328
    # If bottom is specified as a height (AGL), convert to pressure
329
    if bottom:
330
        if bottom.dimensionality == {'[length]': 1.0}:
331
            bottom_pressure = pressure_at_height_above_pressure(p[0], bottom)
332
        if bottom.dimensionality == {'[length]': -1.0, '[mass]': 1.0, '[time]': -2.0}:
333
            bottom_pressure = bottom
334
335
    # Calculate the pressure at the top of the layer
336
    if depth.dimensionality == {'[length]': 1.0}:
337
        top_pressure = pressure_at_height_above_pressure(bottom_pressure, depth)
338
    else:
339
        top_pressure = bottom_pressure - depth
340
341
    # Handle top or bottom values that are invalid
342
    if bottom_pressure > p[0]:
343
        raise ValueError(
344
            'Bottom of layer pressure is greater than the largest pressure in data.')
345
    if top_pressure < p[-1]:
346
        raise ValueError('Top of layer pressure is less than the lowest pressure in data.')
347
    if bottom_pressure < top_pressure:
348
        raise ValueError(
349
            'Pressure at the top of the layer is greater than that at the bottom.')
350
351
        # Mask the pressure values we have data for in the layer
352
    inds = (p <= bottom_pressure) & (p >= top_pressure)
353
    p_interp = p[inds]
354
355
    if interpolate:
356
        # If we don't have the bottom or top requested, append them
357
        if top_pressure not in p_interp:
358
            p_interp = np.sort(np.append(p_interp, top_pressure)) * units.hPa
359
        if bottom_pressure not in p_interp:
360
            p_interp = np.sort(np.append(p_interp, bottom_pressure)) * units.hPa
361
362
        # Interpolate for the possibly missing bottom/top pressure values
363
        sort_args = np.argsort(p)
364
        datavar_interp = np.interp(p_interp, p[sort_args], datavar[sort_args]) * units.degC
365
        p = p_interp
366
        datavar = datavar_interp
367
    else:
368
        datavar = datavar[inds]
369
        p = p_interp
370
371
    return p, datavar
372
373
374
@exporter.export
375
@check_units('[pressure]')
376
def mixed_layer(p, datavar, bottom=None, depth=100 * units.hPa, interpolate=True):
377
    r"""Mix a variable over a layer using pressure as a mass-average.
378
379
    This function will integrate a data variable with respect to pressure and using the
380
    mean value theorem determine the average value.
381
382
    Parameters
383
    ----------
384
    p : array-like
385
        Atmospheric pressure profile
386
    datavar : array-like
387
        Atmospheric variable measured at the given pressures
388
    bottom : `pint.Quantity`
389
        The bottom of the layer as a pressure or height above the surface pressure
390
    depth : `pint.Quantity`
391
        The thickness of the layer as a pressure or height above the bottom of the layer
392
    interpolate : bool
393
        Interpolate the top and bottom points if they are not in the given data
394
395
    Returns
396
    -------
397
    `pint.Quantity`
398
        The mixed value of the data variable.
399
400
    """
401
    p_layer, datavar_layer = get_layer(p, datavar, bottom, depth, interpolate)
402
    actual_depth = abs(p_layer[0] - p_layer[-1])
403
    return (1. / actual_depth.m) * np.trapz(datavar_layer, p_layer) * datavar.units
404
405
406
@exporter.export
407
@check_units('[pressure]', '[temperature]', '[temperature]')
408
def mixed_parcel(p, T, Td, parcel_start_pressure=p[0], bottom=None, depth=100 * units.hPa,
409
                 interpolate=True):
410
    r"""Calculate the properties of a parcel mixed from a layer.
411
412
    Determines the properties of an air parcel that is the result of complete mixing of a
413
    given atmospheric layer.
414
415
    Parameters
416
    ----------
417
    p : array-like
418
        Atmospheric pressure profile
419
    T : array-like
420
        Atmospheric temperature profile
421
    Td : array-like
422
        Atmospheric dewpoint profile
423
    parcel_start_pressure : `pint.Quantity`
424
        Pressure at which the mixed parcel should begin
425
    bottom : `pint.Quantity`
426
        The bottom of the layer as a pressure or height above the surface pressure
427
    depth : `pint.Quantity`
428
        The thickness of the layer as a pressure or height above the bottom of the layer
429
    interpolate : bool
430
        Interpolate the top and bottom points if they are not in the given data
431
432
    Returns
433
    -------
434
    `pint.Quantity, pint.Quantity, pint.Quantity`
435
        The pressure, temperature, and dewpoint of the mixed parcel.
436
437
    """
438
    # Calculate the potential temperature and mixing ratio over the layer
439
    theta = mpcalc.potential_temperature(p, T)
440
    mixing_ratio = mpcalc.saturation_mixing_ratio(p, Td)
441
442
    # Mix the variables over the layer
443
    mean_theta = mixed_layer(p, theta, bottom, depth, interpolate)
444
    mean_mixing_ratio = mixed_layer(p, mixing_ratio, bottom, depth, interpolate)
445
446
    # Convert back to temperature
447
    mean_temperature = mean_theta / mpcalc.potential_temperature(parcel_start_pressure,
448
                                                                 1 * units.degK)
449
450
    # Convert back to dewpoint
451
    mean_vapor_pressure = mpcalc.vapor_pressure(parcel_start_pressure, mean_mixing_ratio)
452
    mean_dewpoint = mpcalc.dewpoint(mean_vapor_pressure)
453
454
    return parcel_start_pressure, mean_temperature.to(T.units), mean_dewpoint.to(Td.units)