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

sbcape_cin()   B

Complexity

Conditions 2

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
c 1
b 0
f 0
dl 0
loc 38
rs 8.8571
1
# Copyright (c) 2008-2015 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 thermodynamic calculations."""
5
6
from __future__ import division
7
8
import numpy as np
9
import scipy.integrate as si
10
import scipy.optimize as so
11
12
from .tools import _greater_or_close, _less_or_close, broadcast_indices, find_intersections, \
13
    get_layer, interp
14
from ..constants import Cp_d, epsilon, kappa, Lv, P0, Rd
15
from ..package_tools import Exporter
16
from ..units import atleast_1d, check_units, concatenate, units
17
18
exporter = Exporter(globals())
19
20
sat_pressure_0c = 6.112 * units.millibar
21
22
23
@exporter.export
24
@check_units('[pressure]', '[temperature]')
25
def potential_temperature(pressure, temperature):
26
    r"""Calculate the potential temperature.
27
28
    Uses the Poisson equation to calculation the potential temperature
29
    given `pressure` and `temperature`.
30
31
    Parameters
32
    ----------
33
    pressure : `pint.Quantity`
34
        The total atmospheric pressure
35
    temperature : `pint.Quantity`
36
        The temperature
37
38
    Returns
39
    -------
40
    `pint.Quantity`
41
        The potential temperature corresponding to the the temperature and
42
        pressure.
43
44
    See Also
45
    --------
46
    dry_lapse
47
48
    Notes
49
    -----
50
    Formula:
51
52
    .. math:: \Theta = T (P_0 / P)^\kappa
53
54
    Examples
55
    --------
56
    >>> from metpy.units import units
57
    >>> metpy.calc.potential_temperature(800. * units.mbar, 273. * units.kelvin)
58
    <Quantity(290.96653180346203, 'kelvin')>
59
60
    """
61
    return temperature * (P0 / pressure).to('dimensionless')**kappa
62
63
64
@exporter.export
65
@check_units('[pressure]', '[temperature]')
66
def dry_lapse(pressure, temperature):
67
    r"""Calculate the temperature at a level assuming only dry processes.
68
69
    This function lifts a parcel starting at `temperature`, conserving
70
    potential temperature. The starting pressure should be the first item in
71
    the `pressure` array.
72
73
    Parameters
74
    ----------
75
    pressure : `pint.Quantity`
76
        The atmospheric pressure level(s) of interest
77
    temperature : `pint.Quantity`
78
        The starting temperature
79
80
    Returns
81
    -------
82
    `pint.Quantity`
83
       The resulting parcel temperature at levels given by `pressure`
84
85
    See Also
86
    --------
87
    moist_lapse : Calculate parcel temperature assuming liquid saturation
88
                  processes
89
    parcel_profile : Calculate complete parcel profile
90
    potential_temperature
91
92
    """
93
    return temperature * (pressure / pressure[0])**kappa
94
95
96
@exporter.export
97
@check_units('[pressure]', '[temperature]')
98
def moist_lapse(pressure, temperature):
99
    r"""Calculate the temperature at a level assuming liquid saturation processes.
100
101
    This function lifts a parcel starting at `temperature`. The starting
102
    pressure should be the first item in the `pressure` array. Essentially,
103
    this function is calculating moist pseudo-adiabats.
104
105
    Parameters
106
    ----------
107
    pressure : `pint.Quantity`
108
        The atmospheric pressure level(s) of interest
109
    temperature : `pint.Quantity`
110
        The starting temperature
111
112
    Returns
113
    -------
114
    `pint.Quantity`
115
       The temperature corresponding to the the starting temperature and
116
       pressure levels.
117
118
    See Also
119
    --------
120
    dry_lapse : Calculate parcel temperature assuming dry adiabatic processes
121
    parcel_profile : Calculate complete parcel profile
122
123
    Notes
124
    -----
125
    This function is implemented by integrating the following differential
126
    equation:
127
128
    .. math:: \frac{dT}{dP} = \frac{1}{P} \frac{R_d T + L_v r_s}
129
                                {C_{pd} + \frac{L_v^2 r_s \epsilon}{R_d T^2}}
130
131
    This equation comes from [Bakhshaii2013]_.
132
133
    """
134
    def dt(t, p):
135
        t = units.Quantity(t, temperature.units)
136
        p = units.Quantity(p, pressure.units)
137
        rs = saturation_mixing_ratio(p, t)
138
        frac = ((Rd * t + Lv * rs) /
139
                (Cp_d + (Lv * Lv * rs * epsilon / (Rd * t * t)))).to('kelvin')
140
        return frac / p
141
    return units.Quantity(si.odeint(dt, atleast_1d(temperature).squeeze(),
142
                                    pressure.squeeze()).T.squeeze(), temperature.units)
143
144
145
@exporter.export
146
@check_units('[pressure]', '[temperature]', '[temperature]')
147
def lcl(pressure, temperature, dewpt, max_iters=50, eps=1e-5):
148
    r"""Calculate the lifted condensation level (LCL) using from the starting point.
149
150
    The starting state for the parcel is defined by `temperature`, `dewpt`,
151
    and `pressure`.
152
153
    Parameters
154
    ----------
155
    pressure : `pint.Quantity`
156
        The starting atmospheric pressure
157
    temperature : `pint.Quantity`
158
        The starting temperature
159
    dewpt : `pint.Quantity`
160
        The starting dew point
161
162
    Returns
163
    -------
164
    `(pint.Quantity, pint.Quantity)`
165
        The LCL pressure and temperature
166
167
    Other Parameters
168
    ----------------
169
    max_iters : int, optional
170
        The maximum number of iterations to use in calculation, defaults to 50.
171
    eps : float, optional
172
        The desired relative error in the calculated value, defaults to 1e-5.
173
174
    See Also
175
    --------
176
    parcel_profile
177
178
    Notes
179
    -----
180
    This function is implemented using an iterative approach to solve for the
181
    LCL. The basic algorithm is:
182
183
    1. Find the dew point from the LCL pressure and starting mixing ratio
184
    2. Find the LCL pressure from the starting temperature and dewpoint
185
    3. Iterate until convergence
186
187
    The function is guaranteed to finish by virtue of the `max_iters` counter.
188
189
    """
190
    def _lcl_iter(p, p0, w, t):
191
        td = dewpoint(vapor_pressure(units.Quantity(p, pressure.units), w))
192
        return (p0 * (td / t) ** (1. / kappa)).m
193
194
    w = mixing_ratio(saturation_vapor_pressure(dewpt), pressure)
195
    fp = so.fixed_point(_lcl_iter, pressure.m, args=(pressure.m, w, temperature),
196
                        xtol=eps, maxiter=max_iters)
197
    lcl_p = units.Quantity(fp, pressure.units)
198
    return lcl_p, dewpoint(vapor_pressure(lcl_p, w))
199
200
201
@exporter.export
202
@check_units('[pressure]', '[temperature]', '[temperature]')
203
def lfc(pressure, temperature, dewpt):
204
    r"""Calculate the level of free convection (LFC).
205
206
    This works by finding the first intersection of the ideal parcel path and
207
    the measured parcel temperature.
208
209
    Parameters
210
    ----------
211
    pressure : `pint.Quantity`
212
        The atmospheric pressure
213
    temperature : `pint.Quantity`
214
        The temperature at the levels given by `pressure`
215
    dewpt : `pint.Quantity`
216
        The dew point at the levels given by `pressure`
217
218
    Returns
219
    -------
220
    `pint.Quantity`
221
        The LFC pressure and temperature
222
223
    See Also
224
    --------
225
    parcel_profile
226
227
    """
228
    ideal_profile = parcel_profile(pressure, temperature[0], dewpt[0]).to('degC')
229
230
    # The parcel profile and data have the same first data point, so we ignore
231
    # that point to get the real first intersection for the LFC calculation.
232
    x, y = find_intersections(pressure[1:], ideal_profile[1:], temperature[1:],
233
                              direction='increasing')
234
    # Two possible cases here: LFC = LCL, or LFC doesn't exist
235
    if len(x) == 0:
236
        if np.all(_less_or_close(ideal_profile, temperature)):
237
            # LFC doesn't exist
238
            return np.nan * pressure.units, np.nan * temperature.units
239
        else:  # LFC = LCL
240
            x, y = lcl(pressure[0], temperature[0], dewpt[0])
241
            return x, y
242
    else:
243
        return x[0], y[0]
244
245
246
@exporter.export
247
@check_units('[pressure]', '[temperature]', '[temperature]')
248
def el(pressure, temperature, dewpt):
249
    r"""Calculate the equilibrium level.
250
251
    This works by finding the last intersection of the ideal parcel path and
252
    the measured environmental temperature. If there is one or fewer intersections, there is
253
    no equilibrium level.
254
255
    Parameters
256
    ----------
257
    pressure : `pint.Quantity`
258
        The atmospheric pressure
259
    temperature : `pint.Quantity`
260
        The temperature at the levels given by `pressure`
261
    dewpt : `pint.Quantity`
262
        The dew point at the levels given by `pressure`
263
264
    Returns
265
    -------
266
    `pint.Quantity, pint.Quantity`
267
        The EL pressure and temperature
268
269
    See Also
270
    --------
271
    parcel_profile
272
273
    """
274
    ideal_profile = parcel_profile(pressure, temperature[0], dewpt[0]).to('degC')
275
    x, y = find_intersections(pressure[1:], ideal_profile[1:], temperature[1:])
276
    # Find LFC to make sure that the function isn't passing off the LFC as the EL
277
    lfc_pressure, _ = lfc(pressure, temperature, dewpt)
278
279
    # If there is only one intersection, there are two possibilities:
280
    # the dataset does not contain the EL, or the LFC = LCL.
281
    if len(x) <= 1:
282
        if (ideal_profile[-1] < temperature[-1]) and (len(x) == 1):
283
            # Profile top colder than environment with one
284
            # intersection, EL exists and LFC = LCL
285
            return x[-1], y[-1]
286
        else:
287
            # The EL does not exist, either due to incomplete data
288
            # or no intersection occurring.
289
            return np.nan * pressure.units, np.nan * temperature.units
290
    else:
291
        # Checking to make sure the LFC is not returned as the EL
292
        # when data is truncated below the EL
293
        if np.isclose(x[-1], lfc_pressure, atol=1e-6):
294
            return np.nan * pressure.units, np.nan * temperature.units
295
        else:
296
            return x[-1], y[-1]
297
298
299
@exporter.export
300
@check_units('[pressure]', '[temperature]', '[temperature]')
301
def parcel_profile(pressure, temperature, dewpt):
302
    r"""Calculate the profile a parcel takes through the atmosphere.
303
304
    The parcel starts at `temperature`, and `dewpt`, lifted up
305
    dry adiabatically to the LCL, and then moist adiabatically from there.
306
    `pressure` specifies the pressure levels for the profile.
307
308
    Parameters
309
    ----------
310
    pressure : `pint.Quantity`
311
        The atmospheric pressure level(s) of interest. The first entry should be the starting
312
        point pressure.
313
    temperature : `pint.Quantity`
314
        The starting temperature
315
    dewpt : `pint.Quantity`
316
        The starting dew point
317
318
    Returns
319
    -------
320
    `pint.Quantity`
321
        The parcel temperatures at the specified pressure levels.
322
323
    See Also
324
    --------
325
    lcl, moist_lapse, dry_lapse
326
327
    """
328
    # Find the LCL
329
    l = lcl(pressure[0], temperature, dewpt)[0].to(pressure.units)
330
331
    # Find the dry adiabatic profile, *including* the LCL. We need >= the LCL in case the
332
    # LCL is included in the levels. It's slightly redundant in that case, but simplifies
333
    # the logic for removing it later.
334
    press_lower = concatenate((pressure[pressure >= l], l))
335
    t1 = dry_lapse(press_lower, temperature)
336
337
    # Find moist pseudo-adiabatic profile starting at the LCL
338
    press_upper = concatenate((l, pressure[pressure < l]))
339
    t2 = moist_lapse(press_upper, t1[-1]).to(t1.units)
340
341
    # Return LCL *without* the LCL point
342
    return concatenate((t1[:-1], t2[1:]))
343
344
345
@exporter.export
346
@check_units('[pressure]', '[dimensionless]')
347
def vapor_pressure(pressure, mixing):
348
    r"""Calculate water vapor (partial) pressure.
349
350
    Given total `pressure` and water vapor `mixing` ratio, calculates the
351
    partial pressure of water vapor.
352
353
    Parameters
354
    ----------
355
    pressure : `pint.Quantity`
356
        total atmospheric pressure
357
    mixing : `pint.Quantity`
358
        dimensionless mass mixing ratio
359
360
    Returns
361
    -------
362
    `pint.Quantity`
363
        The ambient water vapor (partial) pressure in the same units as
364
        `pressure`.
365
366
    Notes
367
    -----
368
    This function is a straightforward implementation of the equation given in many places,
369
    such as [Hobbs1977]_ pg.71:
370
371
    .. math:: e = p \frac{r}{r + \epsilon}
372
373
    See Also
374
    --------
375
    saturation_vapor_pressure, dewpoint
376
377
    """
378
    return pressure * mixing / (epsilon + mixing)
379
380
381
@exporter.export
382
@check_units('[temperature]')
383
def saturation_vapor_pressure(temperature):
384
    r"""Calculate the saturation water vapor (partial) pressure.
385
386
    Parameters
387
    ----------
388
    temperature : `pint.Quantity`
389
        The temperature
390
391
    Returns
392
    -------
393
    `pint.Quantity`
394
        The saturation water vapor (partial) pressure
395
396
    See Also
397
    --------
398
    vapor_pressure, dewpoint
399
400
    Notes
401
    -----
402
    Instead of temperature, dewpoint may be used in order to calculate
403
    the actual (ambient) water vapor (partial) pressure.
404
405
    The formula used is that from [Bolton1980]_ for T in degrees Celsius:
406
407
    .. math:: 6.112 e^\frac{17.67T}{T + 243.5}
408
409
    """
410
    # Converted from original in terms of C to use kelvin. Using raw absolute values of C in
411
    # a formula plays havoc with units support.
412
    return sat_pressure_0c * np.exp(17.67 * (temperature - 273.15 * units.kelvin) /
413
                                    (temperature - 29.65 * units.kelvin))
414
415
416
@exporter.export
417
@check_units('[temperature]', '[dimensionless]')
418
def dewpoint_rh(temperature, rh):
419
    r"""Calculate the ambient dewpoint given air temperature and relative humidity.
420
421
    Parameters
422
    ----------
423
    temperature : `pint.Quantity`
424
        Air temperature
425
    rh : `pint.Quantity`
426
        Relative humidity expressed as a ratio in the range [0, 1]
427
428
    Returns
429
    -------
430
    `pint.Quantity`
431
        The dew point temperature
432
433
    See Also
434
    --------
435
    dewpoint, saturation_vapor_pressure
436
437
    """
438
    return dewpoint(rh * saturation_vapor_pressure(temperature))
439
440
441
@exporter.export
442
@check_units('[pressure]')
443
def dewpoint(e):
444
    r"""Calculate the ambient dewpoint given the vapor pressure.
445
446
    Parameters
447
    ----------
448
    e : `pint.Quantity`
449
        Water vapor partial pressure
450
451
    Returns
452
    -------
453
    `pint.Quantity`
454
        Dew point temperature
455
456
    See Also
457
    --------
458
    dewpoint_rh, saturation_vapor_pressure, vapor_pressure
459
460
    Notes
461
    -----
462
    This function inverts the [Bolton1980]_ formula for saturation vapor
463
    pressure to instead calculate the temperature. This yield the following
464
    formula for dewpoint in degrees Celsius:
465
466
    .. math:: T = \frac{243.5 log(e / 6.112)}{17.67 - log(e / 6.112)}
467
468
    """
469
    val = np.log(e / sat_pressure_0c)
470
    return 0. * units.degC + 243.5 * units.delta_degC * val / (17.67 - val)
471
472
473
@exporter.export
474
@check_units('[pressure]', '[pressure]', '[dimensionless]')
475
def mixing_ratio(part_press, tot_press, molecular_weight_ratio=epsilon):
476
    r"""Calculate the mixing ratio of a gas.
477
478
    This calculates mixing ratio given its partial pressure and the total pressure of
479
    the air. There are no required units for the input arrays, other than that
480
    they have the same units.
481
482
    Parameters
483
    ----------
484
    part_press : `pint.Quantity`
485
        Partial pressure of the constituent gas
486
    tot_press : `pint.Quantity`
487
        Total air pressure
488
    molecular_weight_ratio : `pint.Quantity` or float, optional
489
        The ratio of the molecular weight of the constituent gas to that assumed
490
        for air. Defaults to the ratio for water vapor to dry air
491
        (:math:`\epsilon\approx0.622`).
492
493
    Returns
494
    -------
495
    `pint.Quantity`
496
        The (mass) mixing ratio, dimensionless (e.g. Kg/Kg or g/g)
497
498
    Notes
499
    -----
500
    This function is a straightforward implementation of the equation given in many places,
501
    such as [Hobbs1977]_ pg.73:
502
503
    .. math:: r = \epsilon \frac{e}{p - e}
504
505
    See Also
506
    --------
507
    saturation_mixing_ratio, vapor_pressure
508
509
    """
510
    return molecular_weight_ratio * part_press / (tot_press - part_press)
511
512
513
@exporter.export
514
@check_units('[pressure]', '[temperature]')
515
def saturation_mixing_ratio(tot_press, temperature):
516
    r"""Calculate the saturation mixing ratio of water vapor.
517
518
    This calculation is given total pressure and the temperature. The implementation
519
    uses the formula outlined in [Hobbs1977]_ pg.73.
520
521
    Parameters
522
    ----------
523
    tot_press: `pint.Quantity`
524
        Total atmospheric pressure
525
    temperature: `pint.Quantity`
526
        The temperature
527
528
    Returns
529
    -------
530
    `pint.Quantity`
531
        The saturation mixing ratio, dimensionless
532
533
    """
534
    return mixing_ratio(saturation_vapor_pressure(temperature), tot_press)
535
536
537
@exporter.export
538
@check_units('[pressure]', '[temperature]')
539
def equivalent_potential_temperature(pressure, temperature):
540
    r"""Calculate equivalent potential temperature.
541
542
    This calculation must be given an air parcel's pressure and temperature.
543
    The implementation uses the formula outlined in [Hobbs1977]_ pg.78-79.
544
545
    Parameters
546
    ----------
547
    pressure: `pint.Quantity`
548
        Total atmospheric pressure
549
    temperature: `pint.Quantity`
550
        The temperature
551
552
    Returns
553
    -------
554
    `pint.Quantity`
555
        The corresponding equivalent potential temperature of the parcel
556
557
    Notes
558
    -----
559
    .. math:: \Theta_e = \Theta e^\frac{L_v r_s}{C_{pd} T}
560
561
    """
562
    pottemp = potential_temperature(pressure, temperature)
563
    smixr = saturation_mixing_ratio(pressure, temperature)
564
    return pottemp * np.exp(Lv * smixr / (Cp_d * temperature))
565
566
567
@exporter.export
568
@check_units('[temperature]', '[dimensionless]', '[dimensionless]')
569
def virtual_temperature(temperature, mixing, molecular_weight_ratio=epsilon):
570
    r"""Calculate virtual temperature.
571
572
    This calculation must be given an air parcel's temperature and mixing ratio.
573
    The implementation uses the formula outlined in [Hobbs2006]_ pg.80.
574
575
    Parameters
576
    ----------
577
    temperature: `pint.Quantity`
578
        The temperature
579
    mixing : `pint.Quantity`
580
        dimensionless mass mixing ratio
581
    molecular_weight_ratio : `pint.Quantity` or float, optional
582
        The ratio of the molecular weight of the constituent gas to that assumed
583
        for air. Defaults to the ratio for water vapor to dry air
584
        (:math:`\epsilon\approx0.622`).
585
586
    Returns
587
    -------
588
    `pint.Quantity`
589
        The corresponding virtual temperature of the parcel
590
591
    Notes
592
    -----
593
    .. math:: T_v = T \frac{\text{w} + \epsilon}{\epsilon\,(1 + \text{w})}
594
595
    """
596
    return temperature * ((mixing + molecular_weight_ratio) /
597
                          (molecular_weight_ratio * (1 + mixing)))
598
599
600
@exporter.export
601
@check_units('[pressure]', '[temperature]', '[dimensionless]', '[dimensionless]')
602
def virtual_potential_temperature(pressure, temperature, mixing,
603
                                  molecular_weight_ratio=epsilon):
604
    r"""Calculate virtual potential temperature.
605
606
    This calculation must be given an air parcel's pressure, temperature, and mixing ratio.
607
    The implementation uses the formula outlined in [Markowski2010]_ pg.13.
608
609
    Parameters
610
    ----------
611
    pressure: `pint.Quantity`
612
        Total atmospheric pressure
613
    temperature: `pint.Quantity`
614
        The temperature
615
    mixing : `pint.Quantity`
616
        dimensionless mass mixing ratio
617
    molecular_weight_ratio : `pint.Quantity` or float, optional
618
        The ratio of the molecular weight of the constituent gas to that assumed
619
        for air. Defaults to the ratio for water vapor to dry air
620
        (:math:`\epsilon\approx0.622`).
621
622
    Returns
623
    -------
624
    `pint.Quantity`
625
        The corresponding virtual potential temperature of the parcel
626
627
    Notes
628
    -----
629
    .. math:: \Theta_v = \Theta \frac{\text{w} + \epsilon}{\epsilon\,(1 + \text{w})}
630
631
    """
632
    pottemp = potential_temperature(pressure, temperature)
633
    return virtual_temperature(pottemp, mixing, molecular_weight_ratio)
634
635
636
@exporter.export
637
@check_units('[pressure]', '[temperature]', '[dimensionless]', '[dimensionless]')
638
def density(pressure, temperature, mixing, molecular_weight_ratio=epsilon):
639
    r"""Calculate density.
640
641
    This calculation must be given an air parcel's pressure, temperature, and mixing ratio.
642
    The implementation uses the formula outlined in [Hobbs2006]_ pg.67.
643
644
    Parameters
645
    ----------
646
    temperature: `pint.Quantity`
647
        The temperature
648
    pressure: `pint.Quantity`
649
        Total atmospheric pressure
650
    mixing : `pint.Quantity`
651
        dimensionless mass mixing ratio
652
    molecular_weight_ratio : `pint.Quantity` or float, optional
653
        The ratio of the molecular weight of the constituent gas to that assumed
654
        for air. Defaults to the ratio for water vapor to dry air
655
        (:math:`\epsilon\approx0.622`).
656
657
    Returns
658
    -------
659
    `pint.Quantity`
660
        The corresponding density of the parcel
661
662
    Notes
663
    -----
664
    .. math:: \rho = \frac{p}{R_dT_v}
665
666
    """
667
    virttemp = virtual_temperature(temperature, mixing, molecular_weight_ratio)
668
    return (pressure / (Rd * virttemp)).to(units.kilogram / units.meter ** 3)
669
670
671
@exporter.export
672
@check_units('[temperature]', '[temperature]', '[pressure]')
673
def relative_humidity_wet_psychrometric(dry_bulb_temperature, web_bulb_temperature,
674
                                        pressure, **kwargs):
675
    r"""Calculate the relative humidity with wet bulb and dry bulb temperatures.
676
677
    This uses a psychrometric relationship as outlined in [WMO8-2014]_, with
678
    coefficients from [Fan1987]_.
679
680
    Parameters
681
    ----------
682
    dry_bulb_temperature: `pint.Quantity`
683
        Dry bulb temperature
684
    web_bulb_temperature: `pint.Quantity`
685
        Wet bulb temperature
686
    pressure: `pint.Quantity`
687
        Total atmospheric pressure
688
689
    Returns
690
    -------
691
    `pint.Quantity`
692
        Relative humidity
693
694
    Notes
695
    -----
696
    .. math:: RH = 100 \frac{e}{e_s}
697
698
    * :math:`RH` is relative humidity
699
    * :math:`e` is vapor pressure from the wet psychrometric calculation
700
    * :math:`e_s` is the saturation vapor pressure
701
702
    See Also
703
    --------
704
    psychrometric_vapor_pressure_wet, saturation_vapor_pressure
705
706
    """
707
    return (100 * units.percent * psychrometric_vapor_pressure_wet(dry_bulb_temperature,
708
            web_bulb_temperature, pressure, **kwargs) /
709
            saturation_vapor_pressure(dry_bulb_temperature))
710
711
712
@exporter.export
713
@check_units('[temperature]', '[temperature]', '[pressure]')
714
def psychrometric_vapor_pressure_wet(dry_bulb_temperature, wet_bulb_temperature, pressure,
715
                                     psychrometer_coefficient=6.21e-4 / units.kelvin):
716
    r"""Calculate the vapor pressure with wet bulb and dry bulb temperatures.
717
718
    This uses a psychrometric relationship as outlined in [WMO8-2014]_, with
719
    coefficients from [Fan1987]_.
720
721
    Parameters
722
    ----------
723
    dry_bulb_temperature: `pint.Quantity`
724
        Dry bulb temperature
725
    wet_bulb_temperature: `pint.Quantity`
726
        Wet bulb temperature
727
    pressure: `pint.Quantity`
728
        Total atmospheric pressure
729
    psychrometer_coefficient: `pint.Quantity`
730
        Psychrometer coefficient
731
732
    Returns
733
    -------
734
    `pint.Quantity`
735
        Vapor pressure
736
737
    Notes
738
    -----
739
    .. math:: e' = e'_w(T_w) - A p (T - T_w)
740
741
    * :math:`e'` is vapor pressure
742
    * :math:`e'_w(T_w)` is the saturation vapor pressure with respect to water at temperature
743
      :math:`T_w`
744
    * :math:`p` is the pressure of the wet bulb
745
    * :math:`T` is the temperature of the dry bulb
746
    * :math:`T_w` is the temperature of the wet bulb
747
    * :math:`A` is the psychrometer coefficient
748
749
    Psychrometer coefficient depends on the specific instrument being used and the ventilation
750
    of the instrument.
751
752
    See Also
753
    --------
754
    saturation_vapor_pressure
755
756
    """
757
    return (saturation_vapor_pressure(wet_bulb_temperature) - psychrometer_coefficient *
758
            pressure * (dry_bulb_temperature - wet_bulb_temperature).to('kelvin'))
759
760
761
@exporter.export
762
@check_units('[dimensionless]', '[temperature]', '[pressure]')
763
def relative_humidity_from_mixing_ratio(mixing_ratio, temperature, pressure):
764
    r"""Calculate the relative humidity from mixing ratio, temperature, and pressure.
765
766
    Parameters
767
    ----------
768
    mixing_ratio: `pint.Quantity`
769
        Dimensionless mass mixing ratio
770
    temperature: `pint.Quantity`
771
        Air temperature
772
    pressure: `pint.Quantity`
773
        Total atmospheric pressure
774
775
    Returns
776
    -------
777
    `pint.Quantity`
778
        Relative humidity
779
780
    Notes
781
    -----
782
    Formula from [Hobbs1977]_ pg. 74.
783
784
    .. math:: RH = 100 \frac{w}{w_s}
785
786
    * :math:`RH` is relative humidity
787
    * :math:`w` is mxing ratio
788
    * :math:`w_s` is the saturation mixing ratio
789
790
    See Also
791
    --------
792
    saturation_mixing_ratio
793
794
    """
795
    return (100 * units.percent *
796
            mixing_ratio / saturation_mixing_ratio(pressure, temperature))
797
798
799
@exporter.export
800
@check_units('[dimensionless]')
801
def mixing_ratio_from_specific_humidity(specific_humidity):
802
    r"""Calculate the mixing ratio from specific humidity.
803
804
    Parameters
805
    ----------
806
    specific_humidity: `pint.Quantity`
807
        Specific humidity of air
808
809
    Returns
810
    -------
811
    `pint.Quantity`
812
        Mixing ratio
813
814
    Notes
815
    -----
816
    Formula from [Salby1996]_ pg. 118.
817
818
    .. math:: w = \frac{q}{1-q}
819
820
    * :math:`w` is mxing ratio
821
    * :math:`q` is the specific humidity
822
823
    See Also
824
    --------
825
    mixing_ratio
826
827
    """
828
    return specific_humidity / (1 - specific_humidity)
829
830
831
@exporter.export
832
@check_units('[dimensionless]', '[temperature]', '[pressure]')
833
def relative_humidity_from_specific_humidity(specific_humidity, temperature, pressure):
834
    r"""Calculate the relative humidity from specific humidity, temperature, and pressure.
835
836
    Parameters
837
    ----------
838
    specific_humidity: `pint.Quantity`
839
        Specific humidity of air
840
    temperature: `pint.Quantity`
841
        Air temperature
842
    pressure: `pint.Quantity`
843
        Total atmospheric pressure
844
845
    Returns
846
    -------
847
    `pint.Quantity`
848
        Relative humidity
849
850
    Notes
851
    -----
852
    Formula from [Hobbs1977]_ pg. 74. and [Salby1996]_ pg. 118.
853
854
    .. math:: RH = 100 \frac{q}{(1-q)w_s}
855
856
    * :math:`RH` is relative humidity
857
    * :math:`q` is specific humidity
858
    * :math:`w_s` is the saturation mixing ratio
859
860
    See Also
861
    --------
862
    relative_humidity_from_mixing_ratio
863
864
    """
865
    return (100 * units.percent *
866
            mixing_ratio_from_specific_humidity(specific_humidity) /
867
            saturation_mixing_ratio(pressure, temperature))
868
869
870
@exporter.export
871
@check_units('[pressure]', '[temperature]', '[temperature]', '[temperature]')
872
def cape_cin(pressure, temperature, dewpt, parcel_profile):
873
    r"""Calculate CAPE and CIN.
874
875
    Calculate the convective available potential energy (CAPE) and convective inhibition (CIN)
876
    of a given upper air profile and parcel path. CIN is integrated between the surface and
877
    LFC, CAPE is integrated between the LFC and EL (or top of sounding). Intersection points of
878
    the measured temperature profile and parcel profile are linearly interpolated.
879
880
    Parameters
881
    ----------
882
    pressure : `pint.Quantity`
883
        The atmospheric pressure level(s) of interest. The first entry should be the starting
884
        point pressure.
885
    temperature : `pint.Quantity`
886
        The starting temperature
887
    dewpt : `pint.Quantity`
888
        The starting dew point
889
    parcel_profile : `pint.Quantity`
890
        The temperature profile of the parcel
891
892
    Returns
893
    -------
894
    `pint.Quantity`
895
        Convective available potential energy (CAPE).
896
    `pint.Quantity`
897
        Convective inhibition (CIN).
898
899
    Notes
900
    -----
901
    Formula adopted from [Hobbs1977]_.
902
903
    .. math:: \text{CAPE} = -R_d \int_{LFC}^{EL} (T_{parcel} - T_{env}) d\text{ln}(p)
904
905
    .. math:: \text{CIN} = -R_d \int_{SFC}^{LFC} (T_{parcel} - T_{env}) d\text{ln}(p)
906
907
908
    * :math:`CAPE` Convective available potential energy
909
    * :math:`CIN` Convective inhibition
910
    * :math:`LFC` Pressure of the level of free convection
911
    * :math:`EL` Pressure of the equilibrium level
912
    * :math:`SFC` Level of the surface or beginning of parcel path
913
    * :math:`R_d` Gas constant
914
    * :math:`g` Gravitational acceleration
915
    * :math:`T_{parcel}` Parcel temperature
916
    * :math:`T_{env}` Environment temperature
917
    * :math:`p` Atmospheric pressure
918
919
    See Also
920
    --------
921
    lfc, el
922
923
    """
924
    # Calculate LFC limit of integration
925
    lfc_pressure = lfc(pressure, temperature, dewpt)[0]
926
927
    # If there is no LFC, no need to proceed.
928
    if np.isnan(lfc_pressure):
929
        return 0 * units('J/kg'), 0 * units('J/kg')
930
    else:
931
        lfc_pressure = lfc_pressure.magnitude
932
933
    # Calculate the EL limit of integration
934
    el_pressure = el(pressure, temperature, dewpt)[0]
935
936
    # No EL and we use the top reading of the sounding.
937
    if np.isnan(el_pressure):
938
        el_pressure = pressure[-1].magnitude
939
    else:
940
        el_pressure = el_pressure.magnitude
941
942
    # Difference between the parcel path and measured temperature profiles
943
    y = (parcel_profile - temperature).to(units.degK)
944
945
    # Estimate zero crossings
946
    x, y = _find_append_zero_crossings(np.copy(pressure), y)
947
948
    # CAPE (temperature parcel < temperature environment)
949
    # Only use data between the LFC and EL for calculation
950
    p_mask = _less_or_close(x, lfc_pressure) & _greater_or_close(x, el_pressure)
951
    x_clipped = x[p_mask]
952
    y_clipped = y[p_mask]
953
954
    y_clipped[_less_or_close(y_clipped, 0 * units.degK)] = 0 * units.degK
955
    cape = (Rd * (np.trapz(y_clipped, np.log(x_clipped)) * units.degK)).to(units('J/kg'))
956
957
    # CIN (temperature parcel < temperature environment)
958
    # Only use data between the surface and LFC for calculation
959
    p_mask = _greater_or_close(x, lfc_pressure)
960
    x_clipped = x[p_mask]
961
    y_clipped = y[p_mask]
962
963
    y_clipped[_greater_or_close(y_clipped, 0 * units.degK)] = 0 * units.degK
964
    cin = (Rd * (np.trapz(y_clipped, np.log(x_clipped)) * units.degK)).to(units('J/kg'))
965
966
    return cape, cin
967
968
969
def _find_append_zero_crossings(x, y):
970
    r"""
971
    Find and interpolate zero crossings.
972
973
    Estimate the zero crossings of an x,y series and add estimated crossings to series,
974
    returning a sorted array with no duplicate values.
975
976
    Parameters
977
    ----------
978
    x : `pint.Quantity`
979
        x values of data
980
    y : `pint.Quantity`
981
        y values of data
982
983
    Returns
984
    -------
985
    x : `pint.Quantity`
986
        x values of data
987
    y : `pint.Quantity`
988
        y values of data
989
990
    """
991
    # Find and append crossings to the data
992
    crossings = find_intersections(x[1:], y[1:], np.zeros_like(y[1:]) * y.units)
993
    x = concatenate((x, crossings[0]))
994
    y = concatenate((y, crossings[1]))
995
996
    # Resort so that data are in order
997
    sort_idx = np.argsort(x)
998
    x = x[sort_idx]
999
    y = y[sort_idx]
1000
1001
    # Remove duplicate data points if there are any
1002
    keep_idx = np.ediff1d(x, to_end=[1]) > 0
1003
    x = x[keep_idx]
1004
    y = y[keep_idx]
1005
    return x, y
1006
1007
1008
@exporter.export
1009
@check_units('[pressure]', '[temperature]', '[temperature]')
1010
def most_unstable_parcel(p, temperature, dewpoint, heights=None,
1011
                         bottom=None, depth=300 * units.hPa):
1012
    """
1013
    Determine the most unstable parcel in a layer.
1014
1015
    Determines the most unstable parcel of air by calculating the equivalent
1016
    potential temperature and finding its maximum in the specified layer.
1017
1018
    Parameters
1019
    ----------
1020
    p: `pint.Quantity`
1021
        Atmospheric pressure profile
1022
    temperature: `pint.Quantity`
1023
        Atmospheric temperature profile
1024
    dewpoint: `pint.Quantity`
1025
        Atmospheric dewpoint profile
1026
    heights: `pint.Quantity`
1027
        Atmospheric height profile. Standard atmosphere assumed when None.
1028
    bottom: `pint.Quantity`
1029
        Bottom of the layer to consider for the calculation in pressure or height
1030
    depth: `pint.Quantity`
1031
        Depth of the layer to consider for the calculation in pressure or height
1032
1033
    Returns
1034
    -------
1035
    `pint.Quantity`
1036
        Pressure, temperature, and dew point of most unstable parcel in the profile.
1037
1038
    See Also
1039
    --------
1040
    get_layer
1041
1042
    """
1043
    p_layer, T_layer, Td_layer = get_layer(p, temperature, dewpoint, bottom=bottom,
1044
                                           depth=depth, heights=heights)
1045
    theta_e = equivalent_potential_temperature(p_layer, T_layer)
1046
    max_idx = np.argmax(theta_e)
1047
    return p_layer[max_idx], T_layer[max_idx], Td_layer[max_idx]
1048
1049
1050
@exporter.export
1051
@check_units('[temperature]', '[pressure]', '[temperature]')
1052
def isentropic_interpolation(theta_levels, pressure, temperature, *args, **kwargs):
1053
    r"""Interpolate data in isobaric coordinates to isentropic coordinates.
1054
1055
    Parameters
1056
    ----------
1057
    theta_levels : array
1058
        One-dimensional array of desired theta surfaces
1059
    pressure : array
1060
        One-dimensional array of pressure levels
1061
    temperature : array
1062
        Array of temperature
1063
    args : array, optional
1064
        Any additional variables will be interpolated to each isentropic level.
1065
1066
    Returns
1067
    -------
1068
    list
1069
        List with pressure at each isentropic level, followed by each additional
1070
        argument interpolated to isentropic coordinates.
1071
1072
    Other Parameters
1073
    ----------------
1074
    axis : int, optional
1075
        The axis corresponding to the vertical in the temperature array, defaults to 0.
1076
    tmpk_out : bool, optional
1077
        If true, will calculate temperature and output as the last item in the output list.
1078
        Defaults to False.
1079
    max_iters : int, optional
1080
        The maximum number of iterations to use in calculation, defaults to 50.
1081
    eps : float, optional
1082
        The desired absolute error in the calculated value, defaults to 1e-6.
1083
1084
    Notes
1085
    -----
1086
    Input variable arrays must have the same number of vertical levels as the pressure levels
1087
    array. Pressure is calculated on isentropic surfaces by assuming that temperature varies
1088
    linearly with the natural log of pressure. Linear interpolation is then used in the
1089
    vertical to find the pressure at each isentropic level. Interpolation method from
1090
    [Ziv1994]_. Any additional arguments are assumed to vary linearly with temperature and will
1091
    be linearly interpolated to the new isentropic levels.
1092
1093
    See Also
1094
    --------
1095
    potential_temperature
1096
1097
    """
1098
    # iteration function to be used later
1099
    # Calculates theta from linearly interpolated temperature and solves for pressure
1100
    def _isen_iter(iter_log_p, isentlevs_nd, ka, a, b, pok):
1101
        exner = pok * np.exp(-ka * iter_log_p)
1102
        t = a * iter_log_p + b
1103
        # Newton-Raphson iteration
1104
        f = isentlevs_nd - t * exner
1105
        fp = exner * (ka * t - a)
1106
        return iter_log_p - (f / fp)
1107
1108
    # Change when Python 2.7 no longer supported
1109
    # Pull out keyword arguments
1110
    tmpk_out = kwargs.pop('tmpk_out', False)
1111
    max_iters = kwargs.pop('max_iters', 50)
1112
    eps = kwargs.pop('eps', 1e-6)
1113
    axis = kwargs.pop('axis', 0)
1114
1115
    # Get dimensions in temperature
1116
    ndim = temperature.ndim
1117
1118
    # Convert units
1119
    pres = pressure.to('hPa')
1120
    temperature = temperature.to('kelvin')
1121
1122
    slices = [np.newaxis] * ndim
1123
    slices[axis] = slice(None)
1124
    pres = pres[slices]
1125
    pres = np.broadcast_to(pres, temperature.shape) * pres.units
1126
1127
    # Sort input data
1128
    sort_pres = np.argsort(pres.m, axis=axis)
1129
    sort_pres = np.swapaxes(np.swapaxes(sort_pres, 0, axis)[::-1], 0, axis)
1130
    sorter = broadcast_indices(pres, sort_pres, ndim, axis)
1131
    levs = pres[sorter]
1132
    theta_levels = np.asanyarray(theta_levels.to('kelvin')).reshape(-1)
1133
    sort_isentlevs = np.argsort(theta_levels)
1134
    tmpk = temperature[sorter]
1135
    isentlevels = theta_levels[sort_isentlevs]
1136
1137
    # Make the desired isentropic levels the same shape as temperature
1138
    isentlevs_nd = isentlevels
1139
    isentlevs_nd = isentlevs_nd[slices]
1140
    shape = list(temperature.shape)
1141
    shape[axis] = isentlevels.size
1142
    isentlevs_nd = np.broadcast_to(isentlevs_nd, shape)
1143
1144
    # exponent to Poisson's Equation, which is imported above
1145
    ka = kappa.to('dimensionless').m
1146
1147
    # calculate theta for each point
1148
    pres_theta = potential_temperature(levs, tmpk)
1149
1150
    # Raise error if input theta level is larger than pres_theta max
1151
    if np.max(pres_theta.m) < np.max(theta_levels):
1152
        raise ValueError('Input theta level out of data bounds')
1153
1154
    # Find log of pressure to implement assumption of linear temperature dependence on
1155
    # ln(p)
1156
    log_p = np.log(levs.m)
1157
1158
    # Calculations for interpolation routine
1159
    pok = P0 ** ka
1160
1161
    # index values for each point for the pressure level nearest to the desired theta level
1162
    minv = np.apply_along_axis(np.searchsorted, axis, pres_theta.m, theta_levels)
1163
1164
    # Create index values for broadcasting arrays
1165
    above = broadcast_indices(tmpk, minv, ndim, axis)
1166
    below = broadcast_indices(tmpk, minv - 1, ndim, axis)
1167
1168
    # calculate constants for the interpolation
1169
    a = (tmpk.m[above] - tmpk.m[below]) / (log_p[above] - log_p[below])
1170
    b = tmpk.m[above] - a * log_p[above]
1171
1172
    # calculate first guess for interpolation
1173
    first_guess = 0.5 * (log_p[above] + log_p[below])
1174
1175
    # iterative interpolation using scipy.optimize.fixed_point and _isen_iter defined above
1176
    log_p_solved = so.fixed_point(_isen_iter, first_guess, args=(isentlevs_nd, ka, a, b,
1177
                                                                 pok.m),
1178
                                  xtol=eps, maxiter=max_iters)
1179
1180
    # get back pressure and assign nan for values with pressure greater than 1000 hPa
1181
    isentprs = np.exp(log_p_solved)
1182
    isentprs[isentprs > np.max(pressure.m)] = np.nan
1183
1184
    # create list for storing output data
1185
    ret = []
1186
    ret.append(isentprs * units.hPa)
1187
1188
    # if tmpk_out = true, calculate temperature and output as last item in list
1189
    if tmpk_out:
1190
        ret.append((isentlevs_nd / ((P0.m / isentprs) ** ka)) * units.kelvin)
1191
1192
    # check to see if any additional arguments were given, if so, interpolate to
1193
    # new isentropic levels
1194
    try:
1195
        args[0]
1196
    except IndexError:
1197
        return ret
1198
    else:
1199
        # do an interpolation for each additional argument
1200
        for arr in args:
1201
            var = arr[sorter]
1202
            # interpolate to isentropic levels and add to temporary output array
1203
            arg_out = interp(isentlevels, pres_theta.m, var, axis=axis)
1204
            ret.append(arg_out)
1205
1206
    # output values as a list
1207
    return ret
1208
1209
1210
@exporter.export
1211
@check_units('[pressure]', '[temperature]', '[temperature]', '[temperature]')
1212
def sbcape_cin(pressure, temperature, dewpt, profile=None):
1213
    r"""Calculate surface-based CAPE and CIN.
1214
1215
    Calculate the convective available potential energy (CAPE) and convective inhibition (CIN)
1216
    of a given upper air profile and parcel path for a surface-based parcel. CIN is integrated
1217
    between the surface and LFC, CAPE is integrated between the LFC and EL (or top of
1218
    sounding). Intersection points of the measured temperature profile and parcel profile are
1219
    linearly interpolated.
1220
1221
    Parameters
1222
    ----------
1223
    pressure : `pint.Quantity`
1224
        The atmospheric pressure level(s) of interest. The first entry should be the starting
1225
        point pressure.
1226
    temperature : `pint.Quantity`
1227
        The starting temperature
1228
    dewpt : `pint.Quantity`
1229
        The starting dew point
1230
    profile : `pint.Quantity`
1231
        The temperature profile of the parcel
1232
1233
    Returns
1234
    -------
1235
    `pint.Quantity`
1236
        Convective available potential energy (CAPE).
1237
    `pint.Quantity`
1238
        Convective inhibition (CIN).
1239
1240
    See Also
1241
    --------
1242
    cape_cin, lfc, el
1243
1244
    """
1245
    if profile is None:
1246
        profile = parcel_profile(pressure, temperature[0], dewpt[0])
1247
    return cape_cin(pressure, temperature, dewpt, profile)
1248