Completed
Pull Request — master (#532)
by
unknown
01:28
created

saturation_vapor_pressure_lv()   B

Complexity

Conditions 1

Size

Total Lines 42

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