Completed
Pull Request — master (#494)
by
unknown
01:06
created

mucape_cin()   B

Complexity

Conditions 1

Size

Total Lines 41

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 1
c 3
b 0
f 0
dl 0
loc 41
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 atmospheric temperature corresponding to pressure.
887
    dewpt : `pint.Quantity`
888
        The atmospheric dew point corresponding to pressure.
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 parcle 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, interpolate=False)
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], max_idx
1048
1049
1050
@exporter.export
1051
@check_units('[pressure]', '[temperature]', '[temperature]')
1052
def mucape_cin(pressure, temperature, dewpoint, **kwargs):
1053
    r"""Calculate MUCAPE and MUCIN.
1054
1055
    Calculate the convective available potential energy (CAPE) and convective inhibition (CIN)
1056
    of a given upper air profile and most unstable parcel path. CIN is integrated between the
1057
    surface and LFC, CAPE is integrated between the LFC and EL (or top of sounding).
1058
    Intersection points of the measured temperature profile and parcel profile are linearly
1059
    interpolated.
1060
1061
    Parameters
1062
    ----------
1063
    pressure : `pint.Quantity`
1064
        The atmospheric pressure level(s) of interest. The first entry should be the starting
1065
        point pressure.
1066
    temperature : `pint.Quantity`
1067
        The sounding temperature
1068
    dewpoint : `pint.Quantity`
1069
        The sounding dew point
1070
1071
    Returns
1072
    -------
1073
    `pint.Quantity`
1074
        Convective available potential energy (CAPE) for the most unstable parcel.
1075
    `pint.Quantity`
1076
        Convective inhibition (CIN) for the most unstable parcel.
1077
1078
    See Also
1079
    --------
1080
    cape_cin, lfc, el
1081
1082
    """
1083
    mu_parcel = most_unstable_parcel(pressure, temperature, dewpoint, **kwargs)
1084
    max_idx = mu_parcel[3]
1085
    mu_pressure_profile = pressure[max_idx:]
1086
    mu_temperature_profile = temperature[max_idx:]
1087
    mu_dewpoint_profile = dewpoint[max_idx:]
1088
    mu_profile = parcel_profile(mu_pressure_profile, mu_parcel[1], mu_parcel[2])
1089
    return cape_cin(mu_pressure_profile, mu_temperature_profile, mu_dewpoint_profile,
1090
                    mu_profile)
1091
1092
1093
@exporter.export
1094
@check_units('[temperature]', '[pressure]', '[temperature]')
1095
def isentropic_interpolation(theta_levels, pressure, temperature, *args, **kwargs):
1096
    r"""Interpolate data in isobaric coordinates to isentropic coordinates.
1097
1098
    Parameters
1099
    ----------
1100
    theta_levels : array
1101
        One-dimensional array of desired theta surfaces
1102
    pressure : array
1103
        One-dimensional array of pressure levels
1104
    temperature : array
1105
        Array of temperature
1106
    args : array, optional
1107
        Any additional variables will be interpolated to each isentropic level.
1108
1109
    Returns
1110
    -------
1111
    list
1112
        List with pressure at each isentropic level, followed by each additional
1113
        argument interpolated to isentropic coordinates.
1114
1115
    Other Parameters
1116
    ----------------
1117
    axis : int, optional
1118
        The axis corresponding to the vertical in the temperature array, defaults to 0.
1119
    tmpk_out : bool, optional
1120
        If true, will calculate temperature and output as the last item in the output list.
1121
        Defaults to False.
1122
    max_iters : int, optional
1123
        The maximum number of iterations to use in calculation, defaults to 50.
1124
    eps : float, optional
1125
        The desired absolute error in the calculated value, defaults to 1e-6.
1126
1127
    Notes
1128
    -----
1129
    Input variable arrays must have the same number of vertical levels as the pressure levels
1130
    array. Pressure is calculated on isentropic surfaces by assuming that temperature varies
1131
    linearly with the natural log of pressure. Linear interpolation is then used in the
1132
    vertical to find the pressure at each isentropic level. Interpolation method from
1133
    [Ziv1994]_. Any additional arguments are assumed to vary linearly with temperature and will
1134
    be linearly interpolated to the new isentropic levels.
1135
1136
    See Also
1137
    --------
1138
    potential_temperature
1139
1140
    """
1141
    # iteration function to be used later
1142
    # Calculates theta from linearly interpolated temperature and solves for pressure
1143
    def _isen_iter(iter_log_p, isentlevs_nd, ka, a, b, pok):
1144
        exner = pok * np.exp(-ka * iter_log_p)
1145
        t = a * iter_log_p + b
1146
        # Newton-Raphson iteration
1147
        f = isentlevs_nd - t * exner
1148
        fp = exner * (ka * t - a)
1149
        return iter_log_p - (f / fp)
1150
1151
    # Change when Python 2.7 no longer supported
1152
    # Pull out keyword arguments
1153
    tmpk_out = kwargs.pop('tmpk_out', False)
1154
    max_iters = kwargs.pop('max_iters', 50)
1155
    eps = kwargs.pop('eps', 1e-6)
1156
    axis = kwargs.pop('axis', 0)
1157
1158
    # Get dimensions in temperature
1159
    ndim = temperature.ndim
1160
1161
    # Convert units
1162
    pres = pressure.to('hPa')
1163
    temperature = temperature.to('kelvin')
1164
1165
    slices = [np.newaxis] * ndim
1166
    slices[axis] = slice(None)
1167
    pres = pres[slices]
1168
    pres = np.broadcast_to(pres, temperature.shape) * pres.units
1169
1170
    # Sort input data
1171
    sort_pres = np.argsort(pres.m, axis=axis)
1172
    sort_pres = np.swapaxes(np.swapaxes(sort_pres, 0, axis)[::-1], 0, axis)
1173
    sorter = broadcast_indices(pres, sort_pres, ndim, axis)
1174
    levs = pres[sorter]
1175
    theta_levels = np.asanyarray(theta_levels.to('kelvin')).reshape(-1)
1176
    sort_isentlevs = np.argsort(theta_levels)
1177
    tmpk = temperature[sorter]
1178
    isentlevels = theta_levels[sort_isentlevs]
1179
1180
    # Make the desired isentropic levels the same shape as temperature
1181
    isentlevs_nd = isentlevels
1182
    isentlevs_nd = isentlevs_nd[slices]
1183
    shape = list(temperature.shape)
1184
    shape[axis] = isentlevels.size
1185
    isentlevs_nd = np.broadcast_to(isentlevs_nd, shape)
1186
1187
    # exponent to Poisson's Equation, which is imported above
1188
    ka = kappa.to('dimensionless').m
1189
1190
    # calculate theta for each point
1191
    pres_theta = potential_temperature(levs, tmpk)
1192
1193
    # Raise error if input theta level is larger than pres_theta max
1194
    if np.max(pres_theta.m) < np.max(theta_levels):
1195
        raise ValueError('Input theta level out of data bounds')
1196
1197
    # Find log of pressure to implement assumption of linear temperature dependence on
1198
    # ln(p)
1199
    log_p = np.log(levs.m)
1200
1201
    # Calculations for interpolation routine
1202
    pok = P0 ** ka
1203
1204
    # index values for each point for the pressure level nearest to the desired theta level
1205
    minv = np.apply_along_axis(np.searchsorted, axis, pres_theta.m, theta_levels)
1206
1207
    # Create index values for broadcasting arrays
1208
    above = broadcast_indices(tmpk, minv, ndim, axis)
1209
    below = broadcast_indices(tmpk, minv - 1, ndim, axis)
1210
1211
    # calculate constants for the interpolation
1212
    a = (tmpk.m[above] - tmpk.m[below]) / (log_p[above] - log_p[below])
1213
    b = tmpk.m[above] - a * log_p[above]
1214
1215
    # calculate first guess for interpolation
1216
    first_guess = 0.5 * (log_p[above] + log_p[below])
1217
1218
    # iterative interpolation using scipy.optimize.fixed_point and _isen_iter defined above
1219
    log_p_solved = so.fixed_point(_isen_iter, first_guess, args=(isentlevs_nd, ka, a, b,
1220
                                                                 pok.m),
1221
                                  xtol=eps, maxiter=max_iters)
1222
1223
    # get back pressure and assign nan for values with pressure greater than 1000 hPa
1224
    isentprs = np.exp(log_p_solved)
1225
    isentprs[isentprs > np.max(pressure.m)] = np.nan
1226
1227
    # create list for storing output data
1228
    ret = []
1229
    ret.append(isentprs * units.hPa)
1230
1231
    # if tmpk_out = true, calculate temperature and output as last item in list
1232
    if tmpk_out:
1233
        ret.append((isentlevs_nd / ((P0.m / isentprs) ** ka)) * units.kelvin)
1234
1235
    # check to see if any additional arguments were given, if so, interpolate to
1236
    # new isentropic levels
1237
    try:
1238
        args[0]
1239
    except IndexError:
1240
        return ret
1241
    else:
1242
        # do an interpolation for each additional argument
1243
        for arr in args:
1244
            var = arr[sorter]
1245
            # interpolate to isentropic levels and add to temporary output array
1246
            arg_out = interp(isentlevels, pres_theta.m, var, axis=axis)
1247
            ret.append(arg_out)
1248
1249
    # output values as a list
1250
    return ret
1251