Completed
Pull Request — master (#518)
by
unknown
01:07
created

extract_cross_section()   B

Complexity

Conditions 6

Size

Total Lines 54

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 6
c 2
b 0
f 0
dl 0
loc 54
rs 7.8519

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 . import height_to_pressure_std, pressure_to_height_std
14
from ..package_tools import Exporter
15
from ..units import check_units, units
16
17
exporter = Exporter(globals())
18
19
20
@exporter.export
21
def resample_nn_1d(a, centers):
22
    """Return one-dimensional nearest-neighbor indexes based on user-specified centers.
23
24
    Parameters
25
    ----------
26
    a : array-like
27
        1-dimensional array of numeric values from which to
28
        extract indexes of nearest-neighbors
29
    centers : array-like
30
        1-dimensional array of numeric values representing a subset of values to approximate
31
32
    Returns
33
    -------
34
        An array of indexes representing values closest to given array values
35
36
    """
37
    ix = []
38
    for center in centers:
39
        index = (np.abs(a - center)).argmin()
40
        if index not in ix:
41
            ix.append(index)
42
    return ix
43
44
45
@exporter.export
46
def nearest_intersection_idx(a, b):
47
    """Determine the index of the point just before two lines with common x values.
48
49
    Parameters
50
    ----------
51
    a : array-like
52
        1-dimensional array of y-values for line 1
53
    b : array-like
54
        1-dimensional array of y-values for line 2
55
56
    Returns
57
    -------
58
        An array of indexes representing the index of the values
59
        just before the intersection(s) of the two lines.
60
61
    """
62
    # Difference in the two y-value sets
63
    difference = a - b
64
65
    # Determine the point just before the intersection of the lines
66
    # Will return multiple points for multiple intersections
67
    sign_change_idx, = np.nonzero(np.diff(np.sign(difference)))
68
69
    return sign_change_idx
70
71
72
@exporter.export
73
@units.wraps(('=A', '=B'), ('=A', '=B', '=B'))
74
def find_intersections(x, a, b, direction='all'):
75
    """Calculate the best estimate of intersection.
76
77
    Calculates the best estimates of the intersection of two y-value
78
    data sets that share a common x-value set.
79
80
    Parameters
81
    ----------
82
    x : array-like
83
        1-dimensional array of numeric x-values
84
    a : array-like
85
        1-dimensional array of y-values for line 1
86
    b : array-like
87
        1-dimensional array of y-values for line 2
88
    direction : string, optional
89
        specifies direction of crossing. 'all', 'increasing' (a becoming greater than b),
90
        or 'decreasing' (b becoming greater than a). Defaults to 'all'.
91
92
    Returns
93
    -------
94
        A tuple (x, y) of array-like with the x and y coordinates of the
95
        intersections of the lines.
96
97
    """
98
    # Find the index of the points just before the intersection(s)
99
    nearest_idx = nearest_intersection_idx(a, b)
100
    next_idx = nearest_idx + 1
101
102
    # Determine the sign of the change
103
    sign_change = np.sign(a[next_idx] - b[next_idx])
104
105
    # x-values around each intersection
106
    _, x0 = _next_non_masked_element(x, nearest_idx)
107
    _, x1 = _next_non_masked_element(x, next_idx)
108
109
    # y-values around each intersection for the first line
110
    _, a0 = _next_non_masked_element(a, nearest_idx)
111
    _, a1 = _next_non_masked_element(a, next_idx)
112
113
    # y-values around each intersection for the second line
114
    _, b0 = _next_non_masked_element(b, nearest_idx)
115
    _, b1 = _next_non_masked_element(b, next_idx)
116
117
    # Calculate the x-intersection. This comes from finding the equations of the two lines,
118
    # one through (x0, a0) and (x1, a1) and the other through (x0, b0) and (x1, b1),
119
    # finding their intersection, and reducing with a bunch of algebra.
120
    delta_y0 = a0 - b0
121
    delta_y1 = a1 - b1
122
    intersect_x = (delta_y1 * x0 - delta_y0 * x1) / (delta_y1 - delta_y0)
123
124
    # Calculate the y-intersection of the lines. Just plug the x above into the equation
125
    # for the line through the a points. One could solve for y like x above, but this
126
    # causes weirder unit behavior and seems a little less good numerically.
127
    intersect_y = ((intersect_x - x0) / (x1 - x0)) * (a1 - a0) + a0
128
129
    # Make a mask based on the direction of sign change desired
130
    if direction == 'increasing':
131
        mask = sign_change > 0
132
    elif direction == 'decreasing':
133
        mask = sign_change < 0
134
    elif direction == 'all':
135
        return intersect_x, intersect_y
136
    else:
137
        raise ValueError('Unknown option for direction: {0}'.format(str(direction)))
138
    return intersect_x[mask], intersect_y[mask]
139
140
141
@exporter.export
142
def interpolate_nans(x, y, kind='linear'):
143
    """Interpolate NaN values in y.
144
145
    Interpolate NaN values in the y dimension. Works with unsorted x values.
146
147
    Parameters
148
    ----------
149
    x : array-like
150
        1-dimensional array of numeric x-values
151
    y : array-like
152
        1-dimensional array of numeric y-values
153
    kind : string
154
        specifies the kind of interpolation x coordinate - 'linear' or 'log', optional.
155
        Defaults to 'linear'.
156
157
    Returns
158
    -------
159
        An array of the y coordinate data with NaN values interpolated.
160
161
    """
162
    x_sort_args = np.argsort(x)
163
    x = x[x_sort_args]
164
    y = y[x_sort_args]
165
    nans = np.isnan(y)
166
    if kind == 'linear':
167
        y[nans] = np.interp(x[nans], x[~nans], y[~nans])
168
    elif kind == 'log':
169
        y[nans] = np.interp(np.log(x[nans]), np.log(x[~nans]), y[~nans])
170
    else:
171
        raise ValueError('Unknown option for kind: {0}'.format(str(kind)))
172
    return y[x_sort_args]
173
174
175
def _next_non_masked_element(a, idx):
176
    """Return the next non masked element of a masked array.
177
178
    If an array is masked, return the next non-masked element (if the given index is masked).
179
    If no other unmasked points are after the given masked point, returns none.
180
181
    Parameters
182
    ----------
183
    a : array-like
184
        1-dimensional array of numeric values
185
    idx : integer
186
        index of requested element
187
188
    Returns
189
    -------
190
        Index of next non-masked element and next non-masked element
191
192
    """
193
    try:
194
        next_idx = idx + a[idx:].mask.argmin()
195
        if ma.is_masked(a[next_idx]):
196
            return None, None
197
        else:
198
            return next_idx, a[next_idx]
199
    except (AttributeError, TypeError, IndexError):
200
        return idx, a[idx]
201
202
203
def delete_masked_points(*arrs):
204
    """Delete masked points from arrays.
205
206
    Takes arrays and removes masked points to help with calculations and plotting.
207
208
    Parameters
209
    ----------
210
    arrs : one or more array-like
211
        source arrays
212
213
    Returns
214
    -------
215
    arrs : one or more array-like
216
        arrays with masked elements removed
217
218
    """
219
    if any(hasattr(a, 'mask') for a in arrs):
220
        keep = ~functools.reduce(np.logical_or, (np.ma.getmaskarray(a) for a in arrs))
221
        return tuple(ma.asarray(a[keep]) for a in arrs)
222
    else:
223
        return arrs
224
225
226
@exporter.export
227
def reduce_point_density(points, radius, priority=None):
228
    r"""Return a mask to reduce the density of points in irregularly-spaced data.
229
230
    This function is used to down-sample a collection of scattered points (e.g. surface
231
    data), returning a mask that can be used to select the points from one or more arrays
232
    (e.g. arrays of temperature and dew point). The points selected can be controlled by
233
    providing an array of ``priority`` values (e.g. rainfall totals to ensure that
234
    stations with higher precipitation remain in the mask).
235
236
    Parameters
237
    ----------
238
    points : (N, K) array-like
239
        N locations of the points in K dimensional space
240
    radius : float
241
        minimum radius allowed between points
242
    priority : (N, K) array-like, optional
243
        If given, this should have the same shape as ``points``; these values will
244
        be used to control selection priority for points.
245
246
    Returns
247
    -------
248
        (N,) array-like of boolean values indicating whether points should be kept. This
249
        can be used directly to index numpy arrays to return only the desired points.
250
251
    Examples
252
    --------
253
    >>> metpy.calc.reduce_point_density(np.array([1, 2, 3]), 1.)
254
    array([ True, False,  True], dtype=bool)
255
    >>> metpy.calc.reduce_point_density(np.array([1, 2, 3]), 1.,
256
    ... priority=np.array([0.1, 0.9, 0.3]))
257
    array([False,  True, False], dtype=bool)
258
259
    """
260
    # Handle 1D input
261
    if points.ndim < 2:
262
        points = points.reshape(-1, 1)
263
264
    # Make a kd-tree to speed searching of data.
265
    tree = cKDTree(points)
266
267
    # Need to use sorted indices rather than sorting the position
268
    # so that the keep mask matches *original* order.
269
    if priority is not None:
270
        # Need to sort the locations in decreasing priority.
271
        sorted_indices = np.argsort(priority)[::-1]
272
    else:
273
        # Take advantage of iterator nature of range here to avoid making big lists
274
        sorted_indices = range(len(points))
275
276
    # Keep all points initially
277
    keep = np.ones(len(points), dtype=np.bool)
278
279
    # Loop over all the potential points
280
    for ind in sorted_indices:
281
        # Only proceed if we haven't already excluded this point
282
        if keep[ind]:
283
            # Find the neighbors and eliminate them
284
            neighbors = tree.query_ball_point(points[ind], radius)
285
            keep[neighbors] = False
286
287
            # We just removed ourselves, so undo that
288
            keep[ind] = True
289
290
    return keep
291
292
293
def _get_bound_pressure_height(pressure, bound, heights=None, interpolate=True):
294
    """Calculate the bounding pressure and height in a layer.
295
296
    Given pressure, optional heights, and a bound, return either the closest pressure/height
297
    or interpolated pressure/height. If no heights are provided, a standard atmosphere is
298
    assumed.
299
300
    Parameters
301
    ----------
302
    pressure : `pint.Quantity`
303
        Atmospheric pressures
304
    bound : `pint.Quantity`
305
        Bound to retrieve (in pressure or height)
306
    heights : `pint.Quantity`, optional
307
        Atmospheric heights associated with the pressure levels. Defaults to using
308
        heights calculated from ``pressure`` assuming a standard atmosphere.
309
    interpolate : boolean, optional
310
        Interpolate the bound or return the nearest. Defaults to True.
311
312
    Returns
313
    -------
314
    `pint.Quantity`
315
        The bound pressure and height.
316
317
    """
318
    # Bound is given in pressure
319
    if bound.dimensionality == {'[length]': -1.0, '[mass]': 1.0, '[time]': -2.0}:
320
        # If the bound is in the pressure data, we know the pressure bound exactly
321
        if bound in pressure:
322
            bound_pressure = bound
323
            # If we have heights, we know the exact height value, otherwise return standard
324
            # atmosphere height for the pressure
325
            if heights is not None:
326
                bound_height = heights[pressure == bound_pressure]
327
            else:
328
                bound_height = pressure_to_height_std(bound_pressure)
329
        # If bound is not in the data, return the nearest or interpolated values
330
        else:
331
            if interpolate:
332
                bound_pressure = bound  # Use the user specified bound
333
                if heights is not None:  # Interpolate heights from the height data
334
                    bound_height = log_interp(bound_pressure, pressure, heights)
335
                else:  # If not heights given, use the standard atmosphere
336
                    bound_height = pressure_to_height_std(bound_pressure)
337
            else:  # No interpolation, find the closest values
338
                idx = (np.abs(pressure - bound)).argmin()
339
                bound_pressure = pressure[idx]
340
                if heights is not None:
341
                    bound_height = heights[idx]
342
                else:
343
                    bound_height = pressure_to_height_std(bound_pressure)
344
345
    # Bound is given in height
346
    elif bound.dimensionality == {'[length]': 1.0}:
347
        # If there is height data, see if we have the bound or need to interpolate/find nearest
348
        if heights is not None:
349
            if bound in heights:  # Bound is in the height data
350
                bound_height = bound
351
                bound_pressure = pressure[heights == bound]
352
            else:  # Bound is not in the data
353
                if interpolate:
354
                    bound_height = bound
355
356
                    # Need to cast back to the input type since interp (up to at least numpy
357
                    # 1.13 always returns float64. This can cause upstream users problems,
358
                    # resulting in something like np.append() to upcast.
359
                    bound_pressure = np.interp(np.atleast_1d(bound), heights,
360
                                               pressure).astype(bound.dtype) * pressure.units
361
                else:
362
                    idx = (np.abs(heights - bound)).argmin()
363
                    bound_pressure = pressure[idx]
364
                    bound_height = heights[idx]
365
        else:  # Don't have heights, so assume a standard atmosphere
366
            bound_height = bound
367
            bound_pressure = height_to_pressure_std(bound)
368
            # If interpolation is on, this is all we need, if not, we need to go back and
369
            # find the pressure closest to this and refigure the bounds
370
            if not interpolate:
371
                idx = (np.abs(pressure - bound_pressure)).argmin()
372
                bound_pressure = pressure[idx]
373
                bound_height = pressure_to_height_std(bound_pressure)
374
375
    # Bound has invalid units
376
    else:
377
        raise ValueError('Bound must be specified in units of length or pressure.')
378
379
    # If the bound is out of the range of the data, we shouldn't extrapolate
380
    if (bound_pressure < np.min(pressure)) or (bound_pressure > np.max(pressure)):
381
        raise ValueError('Specified bound is outside pressure range.')
382
    if heights is not None:
383
        if (bound_height > np.max(heights)) or (bound_height < np.min(heights)):
384
            raise ValueError('Specified bound is outside height range.')
385
386
    return bound_pressure, bound_height
387
388
389
@exporter.export
390
@check_units('[pressure]')
391
def get_layer(pressure, *args, **kwargs):
392
    r"""Return an atmospheric layer from upper air data with the requested bottom and depth.
393
394
    This function will subset an upper air dataset to contain only the specified layer. The
395
    bottom of the layer can be specified with a pressure or height above the surface
396
    pressure. The bottom defaults to the surface pressure. The depth of the layer can be
397
    specified in terms of pressure or height above the bottom of the layer. If the top and
398
    bottom of the layer are not in the data, they are interpolated by default.
399
400
    Parameters
401
    ----------
402
    pressure : array-like
403
        Atmospheric pressure profile
404
    *args : array-like
405
        Atmospheric variable(s) measured at the given pressures
406
    heights: array-like, optional
407
        Atmospheric heights corresponding to the given pressures. Defaults to using
408
        heights calculated from ``p`` assuming a standard atmosphere.
409
    bottom : `pint.Quantity`, optional
410
        The bottom of the layer as a pressure or height above the surface pressure. Defaults
411
        to the lowest pressure or height given.
412
    depth : `pint.Quantity`, optional
413
        The thickness of the layer as a pressure or height above the bottom of the layer.
414
        Defaults to 100 hPa.
415
    interpolate : bool, optional
416
        Interpolate the top and bottom points if they are not in the given data. Defaults
417
        to True.
418
419
    Returns
420
    -------
421
    `pint.Quantity, pint.Quantity`
422
        The pressure and data variables of the layer
423
424
    """
425
    # Pop off keyword arguments
426
    heights = kwargs.pop('heights', None)
427
    bottom = kwargs.pop('bottom', None)
428
    depth = kwargs.pop('depth', 100 * units.hPa)
429
    interpolate = kwargs.pop('interpolate', True)
430
431
    # Make sure pressure and datavars are the same length
432
    for datavar in args:
433
        if len(pressure) != len(datavar):
434
            raise ValueError('Pressure and data variables must have the same length.')
435
436
    # If the bottom is not specified, make it the surface pressure
437
    if bottom is None:
438
        bottom = pressure[0]
439
440
    bottom_pressure, bottom_height = _get_bound_pressure_height(pressure, bottom,
441
                                                                heights=heights,
442
                                                                interpolate=interpolate)
443
444
    # Calculate the top if whatever units depth is in
445
    if depth.dimensionality == {'[length]': -1.0, '[mass]': 1.0, '[time]': -2.0}:
446
        top = bottom_pressure - depth
447
    elif depth.dimensionality == {'[length]': 1}:
448
        top = bottom_height + depth
449
    else:
450
        raise ValueError('Depth must be specified in units of length or pressure')
451
452
    top_pressure, _ = _get_bound_pressure_height(pressure, top, heights=heights,
453
                                                 interpolate=interpolate)
454
455
    ret = []  # returned data variables in layer
456
457
    # Ensure pressures are sorted in ascending order
458
    sort_inds = np.argsort(pressure)
459
    pressure = pressure[sort_inds]
460
461
    # Mask based on top and bottom pressure
462
    inds = (pressure <= bottom_pressure) & (pressure >= top_pressure)
463
    p_interp = pressure[inds]
464
465
    # Interpolate pressures at bounds if necessary and sort
466
    if interpolate:
467
        # If we don't have the bottom or top requested, append them
468
        if top_pressure not in p_interp:
469
            p_interp = np.sort(np.append(p_interp, top_pressure)) * pressure.units
470
        if bottom_pressure not in p_interp:
471
            p_interp = np.sort(np.append(p_interp, bottom_pressure)) * pressure.units
472
473
    ret.append(p_interp[::-1])
474
475
    for datavar in args:
476
        # Ensure that things are sorted in ascending order
477
        datavar = datavar[sort_inds]
478
479
        if interpolate:
480
            # Interpolate for the possibly missing bottom/top values
481
            datavar_interp = log_interp(p_interp, pressure, datavar)
482
            datavar = datavar_interp
483
        else:
484
            datavar = datavar[inds]
485
486
        ret.append(datavar[::-1])
487
    return ret
488
489
490
@exporter.export
491
@units.wraps(None, ('=A', '=A'))
492
def interp(x, xp, *args, **kwargs):
493
    r"""Interpolates data with any shape over a specified axis.
494
495
    Interpolation over a specified axis for arrays of any shape.
496
497
    Parameters
498
    ----------
499
    x : array-like
500
        1-D array of desired interpolated values.
501
502
    xp : array-like
503
        The x-coordinates of the data points.
504
505
    args : array-like
506
        The data to be interpolated. Can be multiple arguments, all must be the same shape as
507
        xp.
508
509
    axis : int, optional
510
        The axis to interpolate over. Defaults to 0.
511
512
    fill_value: float, optional
513
        Specify handling of interpolation points out of data bounds. If None, will return
514
        ValueError if points are out of bounds. Defaults to nan.
515
516
    Returns
517
    -------
518
    array-like
519
        Interpolated values for each point with coordinates sorted in ascending order.
520
521
    Examples
522
    --------
523
     >>> x = np.array([1., 2., 3., 4.])
524
     >>> y = np.array([1., 2., 3., 4.])
525
     >>> x_interp = np.array([2.5, 3.5])
526
     >>> metpy.calc.interp(x_interp, x, y)
527
     array([ 2.5,  3.5])
528
529
    Notes
530
    -----
531
    xp and args must be the same shape.
532
533
    """
534
    # Pull out keyword args
535
    fill_value = kwargs.pop('fill_value', np.nan)
536
    axis = kwargs.pop('axis', 0)
537
538
    # Make x an array
539
    x = np.asanyarray(x).reshape(-1)
540
541
    # Save number of dimensions in xp
542
    ndim = xp.ndim
543
544
    # Sort input data
545
    sort_args = np.argsort(xp, axis=axis)
546
    sort_x = np.argsort(x)
547
548
    # indices for sorting
549
    sorter = broadcast_indices(xp, sort_args, ndim, axis)
550
551
    # sort xp
552
    xp = xp[sorter]
553
    # Ensure pressure in increasing order
554
    variables = [arr[sorter] for arr in args]
555
556
    # Make x broadcast with xp
557
    x_array = x[sort_x]
558
    expand = [np.newaxis] * ndim
559
    expand[axis] = slice(None)
560
    x_array = x_array[expand]
561
562
    # Calculate value above interpolated value
563
    minv = np.apply_along_axis(np.searchsorted, axis, xp, x[sort_x])
564
    minv2 = np.copy(minv)
565
566
    # If fill_value is none and data is out of bounds, raise value error
567
    if ((np.max(minv) == xp.shape[axis]) or (np.min(minv) == 0)) and fill_value is None:
568
        raise ValueError('Interpolation point out of data bounds encountered')
569
570
    # Warn if interpolated values are outside data bounds, will make these the values
571
    # at end of data range.
572
    if np.max(minv) == xp.shape[axis]:
573
        warnings.warn('Interpolation point out of data bounds encountered')
574
        minv2[minv == xp.shape[axis]] = xp.shape[axis] - 1
575
    if np.min(minv) == 0:
576
        minv2[minv == 0] = 1
577
578
    # Get indices for broadcasting arrays
579
    above = broadcast_indices(xp, minv2, ndim, axis)
580
    below = broadcast_indices(xp, minv2 - 1, ndim, axis)
581
582
    if np.any(x_array < xp[below]):
583
        warnings.warn('Interpolation point out of data bounds encountered')
584
585
    # Create empty output list
586
    ret = []
587
588
    # Calculate interpolation for each variable
589
    for var in variables:
590
        var_interp = var[below] + ((x_array - xp[below]) /
591
                                   (xp[above] - xp[below])) * (var[above] -
592
                                                               var[below])
593
594
        # Set points out of bounds to fill value.
595
        var_interp[minv == xp.shape[axis]] = fill_value
596
        var_interp[x_array < xp[below]] = fill_value
597
598
        # Check for input points in decreasing order and return output to match.
599
        if x[0] > x[-1]:
600
            var_interp = np.swapaxes(np.swapaxes(var_interp, 0, axis)[::-1], 0, axis)
601
        # Output to list
602
        ret.append(var_interp)
603
    if len(ret) == 1:
604
        return ret[0]
605
    else:
606
        return ret
607
608
609
def broadcast_indices(x, minv, ndim, axis):
610
    """Calculate index values to properly broadcast index array within data array.
611
612
    See usage in interp.
613
    """
614
    ret = []
615
    for dim in range(ndim):
616
        if dim == axis:
617
            ret.append(minv)
618
        else:
619
            broadcast_slice = [np.newaxis] * ndim
620
            broadcast_slice[dim] = slice(None)
621
            dim_inds = np.arange(x.shape[dim])
622
            ret.append(dim_inds[broadcast_slice])
623
    return ret
624
625
626
@exporter.export
627
@units.wraps(None, ('=A', '=A'))
628
def log_interp(x, xp, *args, **kwargs):
629
    r"""Interpolates data with logarithmic x-scale over a specified axis.
630
631
    Interpolation on a logarithmic x-scale for interpolation values in pressure coordintates.
632
633
    Parameters
634
    ----------
635
    x : array-like
636
        1-D array of desired interpolated values.
637
638
    xp : array-like
639
        The x-coordinates of the data points.
640
641
    args : array-like
642
        The data to be interpolated. Can be multiple arguments, all must be the same shape as
643
        xp.
644
645
    axis : int, optional
646
        The axis to interpolate over. Defaults to 0.
647
648
    fill_value: float, optional
649
        Specify handling of interpolation points out of data bounds. If None, will return
650
        ValueError if points are out of bounds. Defaults to nan.
651
652
    Returns
653
    -------
654
    array-like
655
        Interpolated values for each point with coordinates sorted in ascending order.
656
657
    Examples
658
     --------
659
     >>> x_log = np.array([1e3, 1e4, 1e5, 1e6])
660
     >>> y_log = np.log(x_log) * 2 + 3
661
     >>> x_interp = np.array([5e3, 5e4, 5e5])
662
     >>> metpy.calc.log_interp(x_interp, x_log, y_log)
663
     array([ 20.03438638,  24.63955657,  29.24472675])
664
665
    Notes
666
    -----
667
    xp and args must be the same shape.
668
669
    """
670
    # Pull out kwargs
671
    fill_value = kwargs.pop('fill_value', np.nan)
672
    axis = kwargs.pop('axis', 0)
673
674
    # Log x and xp
675
    log_x = np.log(x)
676
    log_xp = np.log(xp)
677
    return interp(log_x, log_xp, *args, axis=axis, fill_value=fill_value)
678
679
680
def _greater_or_close(a, value, **kwargs):
681
    r"""Compare values for greater or close to boolean masks.
682
683
    Returns a boolean mask for values greater than or equal to a target within a specified
684
    absolute or relative tolerance (as in :func:`numpy.isclose`).
685
686
    Parameters
687
    ----------
688
    a : array-like
689
        Array of values to be compared
690
    value : float
691
        Comparison value
692
693
    Returns
694
    -------
695
    array-like
696
        Boolean array where values are greater than or nearly equal to value.
697
698
    """
699
    return np.greater(a, value) | np.isclose(a, value, **kwargs)
700
701
702
def _less_or_close(a, value, **kwargs):
703
    r"""Compare values for less or close to boolean masks.
704
705
    Returns a boolean mask for values less than or equal to a target within a specified
706
    absolute or relative tolerance (as in :func:`numpy.isclose`).
707
708
    Parameters
709
    ----------
710
    a : array-like
711
        Array of values to be compared
712
    value : float
713
        Comparison value
714
715
    Returns
716
    -------
717
    array-like
718
        Boolean array where values are less than or nearly equal to value.
719
720
    """
721
    return np.less(a, value) | np.isclose(a, value, **kwargs)
722
723
724
@exporter.export
725
def extract_cross_section(left_index, right_index, lat, lon, *args, **kwargs):
726
    r"""Extract the data in a vertical cross-section along a line from two given endpoints.
727
728
    Parameters
729
    ----------
730
    left_index : array
731
        Array of index values corresponding to latitude and longitude of the first
732
        (north/west) point.
733
    right_index : array
734
        Array of index values corresponding to latitude and longitude of the second
735
        (south/east) point.
736
    lat : array
737
        Array of latitude points
738
    lon : array
739
        Array of longitude points
740
    args : array
741
        Variables to subset along the cross-section
742
    num : int, optional
743
        Number of sampling points
744
745
    Returns
746
    -------
747
    List with latitude and longitude arrays subset along cross-section line followed by
748
    additional variables.
749
750
    Notes
751
    -----
752
    Nearest neighbor sampling is used to subset along line. Expects data in 3-dimensions.
753
754
    """
755
    # get number of sampling points
756
    num = kwargs.pop('num', 80)
757
758
    # Check if lat/lon arrays are one or two dimensions, raise error if neither
759
    if (lat.ndim > 2) or (lat.ndim != lon.ndim):
760
        raise ValueError('X and Y must be 1-D or 2-D')
761
    if lat.ndim == 1:
762
        lon, lat = np.meshgrid(lon, lat)
763
764
    # Pull out col and row endpoints
765
    col = [left_index[1], right_index[1]]
766
    row = [left_index[0], right_index[0]]
767
768
    # Nearest neighbor sampling points
769
    row, col = [np.linspace(item[0], item[1], num) for item in [row, col]]
770
771
    # Pull cross-section points from each arg
772
    ret = []
773
    ret.append(lat[np.round(row).astype(int), np.round(col).astype(int)])
774
    ret.append(lon[np.round(row).astype(int), np.round(col).astype(int)])
775
    for arr in args:
776
        ret.append(arr[:, np.round(row).astype(int), np.round(col).astype(int)])
777
    return ret
778