Completed
Pull Request — master (#496)
by
unknown
48s
created

equivalent_potential_temperature_fromLCL()   B

Complexity

Conditions 1

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 28
rs 8.8571
c 0
b 0
f 0
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 find_intersections, get_layer
13
from ..constants import Cp_d, epsilon, kappa, Lv, P0, Rd
14
from ..package_tools import Exporter
15
from ..units import atleast_1d, check_units, concatenate, units
16
17
exporter = Exporter(globals())
18
19
sat_pressure_0c = 6.112 * units.millibar
20
21
22
@exporter.export
23
@check_units('[pressure]', '[temperature]')
24
def potential_temperature(pressure, temperature):
25
    r"""Calculate the potential temperature.
26
27
    Uses the Poisson equation to calculation the potential temperature
28
    given `pressure` and `temperature`.
29
30
    Parameters
31
    ----------
32
    pressure : `pint.Quantity`
33
        The total atmospheric pressure
34
    temperature : `pint.Quantity`
35
        The temperature
36
37
    Returns
38
    -------
39
    `pint.Quantity`
40
        The potential temperature corresponding to the the temperature and
41
        pressure.
42
43
    See Also
44
    --------
45
    dry_lapse
46
47
    Notes
48
    -----
49
    Formula:
50
51
    .. math:: \Theta = T (P_0 / P)^\kappa
52
53
    Examples
54
    --------
55
    >>> from metpy.units import units
56
    >>> metpy.calc.potential_temperature(800. * units.mbar, 273. * units.kelvin)
57
    <Quantity(290.96653180346203, 'kelvin')>
58
59
    """
60
    return temperature * (P0 / pressure).to('dimensionless')**kappa
61
62
63
@exporter.export
64
@check_units('[pressure]', '[temperature]')
65
def dry_lapse(pressure, temperature):
66
    r"""Calculate the temperature at a level assuming only dry processes.
67
68
    This function lifts a parcel starting at `temperature`, conserving
69
    potential temperature. The starting pressure should be the first item in
70
    the `pressure` array.
71
72
    Parameters
73
    ----------
74
    pressure : `pint.Quantity`
75
        The atmospheric pressure level(s) of interest
76
    temperature : `pint.Quantity`
77
        The starting temperature
78
79
    Returns
80
    -------
81
    `pint.Quantity`
82
       The resulting parcel temperature at levels given by `pressure`
83
84
    See Also
85
    --------
86
    moist_lapse : Calculate parcel temperature assuming liquid saturation
87
                  processes
88
    parcel_profile : Calculate complete parcel profile
89
    potential_temperature
90
91
    """
92
    return temperature * (pressure / pressure[0])**kappa
93
94
95
@exporter.export
96
@check_units('[pressure]', '[temperature]')
97
def moist_lapse(pressure, temperature):
98
    r"""Calculate the temperature at a level assuming liquid saturation processes.
99
100
    This function lifts a parcel starting at `temperature`. The starting
101
    pressure should be the first item in the `pressure` array. Essentially,
102
    this function is calculating moist pseudo-adiabats.
103
104
    Parameters
105
    ----------
106
    pressure : `pint.Quantity`
107
        The atmospheric pressure level(s) of interest
108
    temperature : `pint.Quantity`
109
        The starting temperature
110
111
    Returns
112
    -------
113
    `pint.Quantity`
114
       The temperature corresponding to the the starting temperature and
115
       pressure levels.
116
117
    See Also
118
    --------
119
    dry_lapse : Calculate parcel temperature assuming dry adiabatic processes
120
    parcel_profile : Calculate complete parcel profile
121
122
    Notes
123
    -----
124
    This function is implemented by integrating the following differential
125
    equation:
126
127
    .. math:: \frac{dT}{dP} = \frac{1}{P} \frac{R_d T + L_v r_s}
128
                                {C_{pd} + \frac{L_v^2 r_s \epsilon}{R_d T^2}}
129
130
    This equation comes from [Bakhshaii2013]_.
131
132
    """
133
    def dt(t, p):
134
        t = units.Quantity(t, temperature.units)
135
        p = units.Quantity(p, pressure.units)
136
        rs = saturation_mixing_ratio(p, t)
137
        frac = ((Rd * t + Lv * rs) /
138
                (Cp_d + (Lv * Lv * rs * epsilon / (Rd * t * t)))).to('kelvin')
139
        return frac / p
140
    return units.Quantity(si.odeint(dt, atleast_1d(temperature).squeeze(),
141
                                    pressure.squeeze()).T.squeeze(), temperature.units)
142
143
144
@exporter.export
145
@check_units('[pressure]', '[temperature]', '[temperature]')
146
def lcl(pressure, temperature, dewpt, max_iters=50, eps=1e-5):
147
    r"""Calculate the lifted condensation level (LCL) using from the starting point.
148
149
    The starting state for the parcel is defined by `temperature`, `dewpt`,
150
    and `pressure`.
151
152
    Parameters
153
    ----------
154
    pressure : `pint.Quantity`
155
        The starting atmospheric pressure
156
    temperature : `pint.Quantity`
157
        The starting temperature
158
    dewpt : `pint.Quantity`
159
        The starting dew point
160
161
    Returns
162
    -------
163
    `(pint.Quantity, pint.Quantity)`
164
        The LCL pressure and temperature
165
166
    Other Parameters
167
    ----------------
168
    max_iters : int, optional
169
        The maximum number of iterations to use in calculation, defaults to 50.
170
    eps : float, optional
171
        The desired relative error in the calculated value, defaults to 1e-5.
172
173
    See Also
174
    --------
175
    parcel_profile
176
177
    Notes
178
    -----
179
    This function is implemented using an iterative approach to solve for the
180
    LCL. The basic algorithm is:
181
182
    1. Find the dew point from the LCL pressure and starting mixing ratio
183
    2. Find the LCL pressure from the starting temperature and dewpoint
184
    3. Iterate until convergence
185
186
    The function is guaranteed to finish by virtue of the `max_iters` counter.
187
188
    """
189
    def _lcl_iter(p, p0, w, t):
190
        td = dewpoint(vapor_pressure(units.Quantity(p, pressure.units), w))
191
        return (p0 * (td / t) ** (1. / kappa)).m
192
193
    w = mixing_ratio(saturation_vapor_pressure(dewpt), pressure)
194
    fp = so.fixed_point(_lcl_iter, pressure.m, args=(pressure.m, w, temperature),
195
                        xtol=eps, maxiter=max_iters)
196
    lcl_p = units.Quantity(fp, pressure.units)
197
    return lcl_p, dewpoint(vapor_pressure(lcl_p, w))
198
199
200 View Code Duplication
@exporter.export
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
201
@check_units('[pressure]', '[temperature]', '[temperature]')
202
def lfc(pressure, temperature, dewpt):
203
    r"""Calculate the level of free convection (LFC).
204
205
    This works by finding the first intersection of the ideal parcel path and
206
    the measured parcel temperature.
207
208
    Parameters
209
    ----------
210
    pressure : `pint.Quantity`
211
        The atmospheric pressure
212
    temperature : `pint.Quantity`
213
        The temperature at the levels given by `pressure`
214
    dewpt : `pint.Quantity`
215
        The dew point at the levels given by `pressure`
216
217
    Returns
218
    -------
219
    `pint.Quantity`
220
        The LFC pressure and temperature
221
222
    See Also
223
    --------
224
    parcel_profile
225
226
    """
227
    ideal_profile = parcel_profile(pressure, temperature[0], dewpt[0]).to('degC')
228
229
    # The parcel profile and data have the same first data point, so we ignore
230
    # that point to get the real first intersection for the LFC calculation.
231
    x, y = find_intersections(pressure[1:], ideal_profile[1:], temperature[1:],
232
                              direction='increasing')
233
    # Two possible cases here: LFC = LCL, or LFC doesn't exist
234
    if len(x) == 0:
235
        if np.any(ideal_profile > temperature):
236
            # LFC = LCL
237
            x, y = lcl(pressure[0], temperature[0], dewpt[0])
238
            return x, y
239
        # LFC doesn't exist
240
        else:
241
            return np.nan * pressure.units, np.nan * temperature.units
242
    else:
243
        return x[0], y[0]
244
245
246 View Code Duplication
@exporter.export
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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
277
    # If there is only one intersection, there are two possibilities:
278
    # the dataset does not contain the EL, or the LFC = LCL.
279
    if len(x) <= 1:
280
        if (ideal_profile[-1] < temperature[-1]) and (len(x) == 1):
281
            # Profile top colder than environment with one
282
            # intersection, EL exists and LFC = LCL
283
            return x[-1], y[-1]
284
        else:
285
            # The EL does not exist, either due to incomplete data
286
            # or no intersection occurring.
287
            return np.nan * pressure.units, np.nan * temperature.units
288
    else:
289
        return x[-1], y[-1]
290
291
292
@exporter.export
293
@check_units('[pressure]', '[temperature]', '[temperature]')
294
def parcel_profile(pressure, temperature, dewpt):
295
    r"""Calculate the profile a parcel takes through the atmosphere.
296
297
    The parcel starts at `temperature`, and `dewpt`, lifted up
298
    dry adiabatically to the LCL, and then moist adiabatically from there.
299
    `pressure` specifies the pressure levels for the profile.
300
301
    Parameters
302
    ----------
303
    pressure : `pint.Quantity`
304
        The atmospheric pressure level(s) of interest. The first entry should be the starting
305
        point pressure.
306
    temperature : `pint.Quantity`
307
        The starting temperature
308
    dewpt : `pint.Quantity`
309
        The starting dew point
310
311
    Returns
312
    -------
313
    `pint.Quantity`
314
        The parcel temperatures at the specified pressure levels.
315
316
    See Also
317
    --------
318
    lcl, moist_lapse, dry_lapse
319
320
    """
321
    # Find the LCL
322
    l = lcl(pressure[0], temperature, dewpt)[0].to(pressure.units)
323
324
    # Find the dry adiabatic profile, *including* the LCL. We need >= the LCL in case the
325
    # LCL is included in the levels. It's slightly redundant in that case, but simplifies
326
    # the logic for removing it later.
327
    press_lower = concatenate((pressure[pressure >= l], l))
328
    t1 = dry_lapse(press_lower, temperature)
329
330
    # Find moist pseudo-adiabatic profile starting at the LCL
331
    press_upper = concatenate((l, pressure[pressure < l]))
332
    t2 = moist_lapse(press_upper, t1[-1]).to(t1.units)
333
334
    # Return LCL *without* the LCL point
335
    return concatenate((t1[:-1], t2[1:]))
336
337
338
@exporter.export
339
@check_units('[pressure]', '[dimensionless]')
340
def vapor_pressure(pressure, mixing):
341
    r"""Calculate water vapor (partial) pressure.
342
343
    Given total `pressure` and water vapor `mixing` ratio, calculates the
344
    partial pressure of water vapor.
345
346
    Parameters
347
    ----------
348
    pressure : `pint.Quantity`
349
        total atmospheric pressure
350
    mixing : `pint.Quantity`
351
        dimensionless mass mixing ratio
352
353
    Returns
354
    -------
355
    `pint.Quantity`
356
        The ambient water vapor (partial) pressure in the same units as
357
        `pressure`.
358
359
    Notes
360
    -----
361
    This function is a straightforward implementation of the equation given in many places,
362
    such as [Hobbs1977]_ pg.71:
363
364
    .. math:: e = p \frac{r}{r + \epsilon}
365
366
    See Also
367
    --------
368
    saturation_vapor_pressure, dewpoint
369
370
    """
371
    return pressure * mixing / (epsilon + mixing)
372
373
374
@exporter.export
375
@check_units('[temperature]')
376
def saturation_vapor_pressure(temperature):
377
    r"""Calculate the saturation water vapor (partial) pressure.
378
379
    Parameters
380
    ----------
381
    temperature : `pint.Quantity`
382
        The temperature
383
384
    Returns
385
    -------
386
    `pint.Quantity`
387
        The saturation water vapor (partial) pressure
388
389
    See Also
390
    --------
391
    vapor_pressure, dewpoint
392
393
    Notes
394
    -----
395
    Instead of temperature, dewpoint may be used in order to calculate
396
    the actual (ambient) water vapor (partial) pressure.
397
398
    The formula used is that from [Bolton1980]_ for T in degrees Celsius:
399
400
    .. math:: 6.112 e^\frac{17.67T}{T + 243.5}
401
402
    """
403
    # Converted from original in terms of C to use kelvin. Using raw absolute values of C in
404
    # a formula plays havoc with units support.
405
    return sat_pressure_0c * np.exp(17.67 * (temperature - 273.15 * units.kelvin) /
406
                                    (temperature - 29.65 * units.kelvin))
407
408
409
@exporter.export
410
@check_units('[temperature]', '[dimensionless]')
411
def dewpoint_rh(temperature, rh):
412
    r"""Calculate the ambient dewpoint given air temperature and relative humidity.
413
414
    Parameters
415
    ----------
416
    temperature : `pint.Quantity`
417
        Air temperature
418
    rh : `pint.Quantity`
419
        Relative humidity expressed as a ratio in the range [0, 1]
420
421
    Returns
422
    -------
423
    `pint.Quantity`
424
        The dew point temperature
425
426
    See Also
427
    --------
428
    dewpoint, saturation_vapor_pressure
429
430
    """
431
    return dewpoint(rh * saturation_vapor_pressure(temperature))
432
433
434
@exporter.export
435
@check_units('[pressure]')
436
def dewpoint(e):
437
    r"""Calculate the ambient dewpoint given the vapor pressure.
438
439
    Parameters
440
    ----------
441
    e : `pint.Quantity`
442
        Water vapor partial pressure
443
444
    Returns
445
    -------
446
    `pint.Quantity`
447
        Dew point temperature
448
449
    See Also
450
    --------
451
    dewpoint_rh, saturation_vapor_pressure, vapor_pressure
452
453
    Notes
454
    -----
455
    This function inverts the [Bolton1980]_ formula for saturation vapor
456
    pressure to instead calculate the temperature. This yield the following
457
    formula for dewpoint in degrees Celsius:
458
459
    .. math:: T = \frac{243.5 log(e / 6.112)}{17.67 - log(e / 6.112)}
460
461
    """
462
    val = np.log(e / sat_pressure_0c)
463
    return 0. * units.degC + 243.5 * units.delta_degC * val / (17.67 - val)
464
465
466
@exporter.export
467
@check_units('[pressure]', '[pressure]', '[dimensionless]')
468
def mixing_ratio(part_press, tot_press, molecular_weight_ratio=epsilon):
469
    r"""Calculate the mixing ratio of a gas.
470
471
    This calculates mixing ratio given its partial pressure and the total pressure of
472
    the air. There are no required units for the input arrays, other than that
473
    they have the same units.
474
475
    Parameters
476
    ----------
477
    part_press : `pint.Quantity`
478
        Partial pressure of the constituent gas
479
    tot_press : `pint.Quantity`
480
        Total air pressure
481
    molecular_weight_ratio : `pint.Quantity` or float, optional
482
        The ratio of the molecular weight of the constituent gas to that assumed
483
        for air. Defaults to the ratio for water vapor to dry air
484
        (:math:`\epsilon\approx0.622`).
485
486
    Returns
487
    -------
488
    `pint.Quantity`
489
        The (mass) mixing ratio, dimensionless (e.g. Kg/Kg or g/g)
490
491
    Notes
492
    -----
493
    This function is a straightforward implementation of the equation given in many places,
494
    such as [Hobbs1977]_ pg.73:
495
496
    .. math:: r = \epsilon \frac{e}{p - e}
497
498
    See Also
499
    --------
500
    saturation_mixing_ratio, vapor_pressure
501
502
    """
503
    return molecular_weight_ratio * part_press / (tot_press - part_press)
504
505
506
@exporter.export
507
@check_units('[pressure]', '[temperature]')
508
def saturation_mixing_ratio(tot_press, temperature):
509
    r"""Calculate the saturation mixing ratio of water vapor.
510
511
    This calculation is given total pressure and the temperature. The implementation
512
    uses the formula outlined in [Hobbs1977]_ pg.73.
513
514
    Parameters
515
    ----------
516
    tot_press: `pint.Quantity`
517
        Total atmospheric pressure
518
    temperature: `pint.Quantity`
519
        The temperature
520
521
    Returns
522
    -------
523
    `pint.Quantity`
524
        The saturation mixing ratio, dimensionless
525
526
    """
527
    return mixing_ratio(saturation_vapor_pressure(temperature), tot_press)
528
529
530
@exporter.export
531
@check_units('[pressure]', '[temperature]')
532
def equivalent_potential_temperature_fromLCL(pressure_LCL, temperature_LCL):
533
    r"""Calculate equivalent potential temperature.
534
535
    This calculation must be given an air parcel's pressure and temperature.
536
    The implementation uses the formula outlined in [Hobbs1977]_ pg.78-79.
537
538
    Parameters
539
    ----------
540
    pressure: `pint.Quantity`
541
        Total atmospheric pressure at the LCL
542
    temperature: `pint.Quantity`
543
        The temperature at the LCL
544
545
    Returns
546
    -------
547
    `pint.Quantity`
548
        The corresponding equivalent potential temperature of the parcel
549
550
    Notes
551
    -----
552
    .. math:: \Theta_e = \Theta e^\frac{L_v r_s}{C_{pd} T}
553
554
    """
555
    pottemp_LCL = potential_temperature(pressure_LCL, temperature_LCL)
556
    smixr_LCL = saturation_mixing_ratio(pressure_LCL, temperature_LCL)
557
    return pottemp_LCL * np.exp(Lv * smixr_LCL / (Cp_d * temperature_LCL))
558
559
@exporter.export
560
@check_units('[pressure]', '[temperature]', '[temperature]')
561
def equivalent_potential_temperature_fromTd(pressure, temperature, dewpoint):
562
    r"""Calculate equivalent potential temperature.
563
564
    This calculation must be given an air parcel's pressure, temperature, dewpoint
565
    The implementation uses the formula outlined in Bolton 1980 with mixing ratio 
566
    r(Td,p) computed by a formula saturation_mixing_ratio(pressure, dewpoint)
567
568
    Parameters
569
    ----------
570
    pressure: `pint.Quantity`
571
        Total atmospheric pressure
572
    temperature: `pint.Quantity`
573
        The temperature
574
    dewpoint: `pint.Quantity`
575
        The dewpoint
576
577
    Returns
578
    -------
579
    `pint.Quantity`
580
        The corresponding equivalent potential temperature of the parcel
581
582
    Notes
583
    -----
584
    See https://en.wikipedia.org/wiki/Equivalent_potential_temperature
585
    Bolton is most accurate according to 
586
       http://journals.ametsoc.org/doi/pdf/10.1175/2009MWR2774.1
587
    """
588
    # Formulas from Bolton (1980), transcribed from Wikipedia page above
589
590
    T = temperature/units.kelvin
591
    Td = dewpoint/units.kelvin
592
    p = pressure/units.hPa 
593
    e = saturation_vapor_pressure(dewpoint)/units.hPa
594
    r = saturation_mixing_ratio(pressure, dewpoint) # /units.dimensionless
595
596
    T_L = 56 + 1./( 1./(Td-56) + np.log(T/Td)/800. )
597
    Th_L= T * (1000/(p-e))**kappa * (T/T_L)**(0.28*r)
598
    Th_e= Th_L * np.exp( (3036./T_L -1.78)*r*(1+0.448*r) )
599
600
    return Th_e *units.kelvin
601
602
@exporter.export
603
@check_units('[temperature]', '[dimensionless]', '[dimensionless]')
604
def virtual_temperature(temperature, mixing, molecular_weight_ratio=epsilon):
605
    r"""Calculate virtual temperature.
606
607
    This calculation must be given an air parcel's temperature and mixing ratio.
608
    The implementation uses the formula outlined in [Hobbs2006]_ pg.80.
609
610
    Parameters
611
    ----------
612
    temperature: `pint.Quantity`
613
        The temperature
614
    mixing : `pint.Quantity`
615
        dimensionless mass mixing ratio
616
    molecular_weight_ratio : `pint.Quantity` or float, optional
617
        The ratio of the molecular weight of the constituent gas to that assumed
618
        for air. Defaults to the ratio for water vapor to dry air
619
        (:math:`\epsilon\approx0.622`).
620
621
    Returns
622
    -------
623
    `pint.Quantity`
624
        The corresponding virtual temperature of the parcel
625
626
    Notes
627
    -----
628
    .. math:: T_v = T \frac{\text{w} + \epsilon}{\epsilon\,(1 + \text{w})}
629
630
    """
631
    return temperature * ((mixing + molecular_weight_ratio) /
632
                          (molecular_weight_ratio * (1 + mixing)))
633
634
635
@exporter.export
636
@check_units('[pressure]', '[temperature]', '[dimensionless]', '[dimensionless]')
637
def virtual_potential_temperature(pressure, temperature, mixing,
638
                                  molecular_weight_ratio=epsilon):
639
    r"""Calculate virtual potential temperature.
640
641
    This calculation must be given an air parcel's pressure, temperature, and mixing ratio.
642
    The implementation uses the formula outlined in [Markowski2010]_ pg.13.
643
644
    Parameters
645
    ----------
646
    pressure: `pint.Quantity`
647
        Total atmospheric pressure
648
    temperature: `pint.Quantity`
649
        The temperature
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 virtual potential temperature of the parcel
661
662
    Notes
663
    -----
664
    .. math:: \Theta_v = \Theta \frac{\text{w} + \epsilon}{\epsilon\,(1 + \text{w})}
665
666
    """
667
    pottemp = potential_temperature(pressure, temperature)
668
    return virtual_temperature(pottemp, mixing, molecular_weight_ratio)
669
670
671
@exporter.export
672
@check_units('[pressure]', '[temperature]', '[dimensionless]', '[dimensionless]')
673
def density(pressure, temperature, mixing, molecular_weight_ratio=epsilon):
674
    r"""Calculate density.
675
676
    This calculation must be given an air parcel's pressure, temperature, and mixing ratio.
677
    The implementation uses the formula outlined in [Hobbs2006]_ pg.67.
678
679
    Parameters
680
    ----------
681
    temperature: `pint.Quantity`
682
        The temperature
683
    pressure: `pint.Quantity`
684
        Total atmospheric pressure
685
    mixing : `pint.Quantity`
686
        dimensionless mass mixing ratio
687
    molecular_weight_ratio : `pint.Quantity` or float, optional
688
        The ratio of the molecular weight of the constituent gas to that assumed
689
        for air. Defaults to the ratio for water vapor to dry air
690
        (:math:`\epsilon\approx0.622`).
691
692
    Returns
693
    -------
694
    `pint.Quantity`
695
        The corresponding density of the parcel
696
697
    Notes
698
    -----
699
    .. math:: \rho = \frac{p}{R_dT_v}
700
701
    """
702
    virttemp = virtual_temperature(temperature, mixing, molecular_weight_ratio)
703
    return (pressure / (Rd * virttemp)).to(units.kilogram / units.meter ** 3)
704
705
706
@exporter.export
707
@check_units('[temperature]', '[temperature]', '[pressure]')
708
def relative_humidity_wet_psychrometric(dry_bulb_temperature, web_bulb_temperature,
709
                                        pressure, **kwargs):
710
    r"""Calculate the relative humidity with wet bulb and dry bulb temperatures.
711
712
    This uses a psychrometric relationship as outlined in [WMO8-2008]_, with
713
    coefficients from [Fan1987]_.
714
715
    Parameters
716
    ----------
717
    dry_bulb_temperature: `pint.Quantity`
718
        Dry bulb temperature
719
    web_bulb_temperature: `pint.Quantity`
720
        Wet bulb temperature
721
    pressure: `pint.Quantity`
722
        Total atmospheric pressure
723
724
    Returns
725
    -------
726
    `pint.Quantity`
727
        Relative humidity
728
729
    Notes
730
    -----
731
    .. math:: RH = 100 \frac{e}{e_s}
732
733
    * :math:`RH` is relative humidity
734
    * :math:`e` is vapor pressure from the wet psychrometric calculation
735
    * :math:`e_s` is the saturation vapor pressure
736
737
    See Also
738
    --------
739
    psychrometric_vapor_pressure_wet, saturation_vapor_pressure
740
741
    """
742
    return (100 * units.percent * psychrometric_vapor_pressure_wet(dry_bulb_temperature,
743
            web_bulb_temperature, pressure, **kwargs) /
744
            saturation_vapor_pressure(dry_bulb_temperature))
745
746
747
@exporter.export
748
@check_units('[temperature]', '[temperature]', '[pressure]')
749
def psychrometric_vapor_pressure_wet(dry_bulb_temperature, wet_bulb_temperature, pressure,
750
                                     psychrometer_coefficient=6.21e-4 / units.kelvin):
751
    r"""Calculate the vapor pressure with wet bulb and dry bulb temperatures.
752
753
    This uses a psychrometric relationship as outlined in [WMO8-2008]_, with
754
    coefficients from [Fan1987]_.
755
756
    Parameters
757
    ----------
758
    dry_bulb_temperature: `pint.Quantity`
759
        Dry bulb temperature
760
    wet_bulb_temperature: `pint.Quantity`
761
        Wet bulb temperature
762
    pressure: `pint.Quantity`
763
        Total atmospheric pressure
764
    psychrometer_coefficient: `pint.Quantity`
765
        Psychrometer coefficient
766
767
    Returns
768
    -------
769
    `pint.Quantity`
770
        Vapor pressure
771
772
    Notes
773
    -----
774
    .. math:: e' = e'_w(T_w) - A p (T - T_w)
775
776
    * :math:`e'` is vapor pressure
777
    * :math:`e'_w(T_w)` is the saturation vapor pressure with respect to water at temperature
778
      :math:`T_w`
779
    * :math:`p` is the pressure of the wet bulb
780
    * :math:`T` is the temperature of the dry bulb
781
    * :math:`T_w` is the temperature of the wet bulb
782
    * :math:`A` is the psychrometer coefficient
783
784
    Psychrometer coefficient depends on the specific instrument being used and the ventilation
785
    of the instrument.
786
787
    See Also
788
    --------
789
    saturation_vapor_pressure
790
791
    """
792
    return (saturation_vapor_pressure(wet_bulb_temperature) - psychrometer_coefficient *
793
            pressure * (dry_bulb_temperature - wet_bulb_temperature).to('kelvin'))
794
795
796
@exporter.export
797
@check_units('[dimensionless]', '[temperature]', '[pressure]')
798
def relative_humidity_from_mixing_ratio(mixing_ratio, temperature, pressure):
799
    r"""Calculate the relative humidity from mixing ratio, temperature, and pressure.
800
801
    Parameters
802
    ----------
803
    mixing_ratio: `pint.Quantity`
804
        Dimensionless mass mixing ratio
805
    temperature: `pint.Quantity`
806
        Air temperature
807
    pressure: `pint.Quantity`
808
        Total atmospheric pressure
809
810
    Returns
811
    -------
812
    `pint.Quantity`
813
        Relative humidity
814
815
    Notes
816
    -----
817
    Formula from [Hobbs1977]_ pg. 74.
818
819
    .. math:: RH = 100 \frac{w}{w_s}
820
821
    * :math:`RH` is relative humidity
822
    * :math:`w` is mxing ratio
823
    * :math:`w_s` is the saturation mixing ratio
824
825
    See Also
826
    --------
827
    saturation_mixing_ratio
828
829
    """
830
    return (100 * units.percent *
831
            mixing_ratio / saturation_mixing_ratio(pressure, temperature))
832
833
834
@exporter.export
835
@check_units('[dimensionless]')
836
def mixing_ratio_from_specific_humidity(specific_humidity):
837
    r"""Calculate the mixing ratio from specific humidity.
838
839
    Parameters
840
    ----------
841
    specific_humidity: `pint.Quantity`
842
        Specific humidity of air
843
844
    Returns
845
    -------
846
    `pint.Quantity`
847
        Mixing ratio
848
849
    Notes
850
    -----
851
    Formula from [Salby1996]_ pg. 118.
852
853
    .. math:: w = \frac{q}{1-q}
854
855
    * :math:`w` is mxing ratio
856
    * :math:`q` is the specific humidity
857
858
    See Also
859
    --------
860
    mixing_ratio
861
862
    """
863
    return specific_humidity / (1 - specific_humidity)
864
865
866
@exporter.export
867
@check_units('[dimensionless]', '[temperature]', '[pressure]')
868
def relative_humidity_from_specific_humidity(specific_humidity, temperature, pressure):
869
    r"""Calculate the relative humidity from specific humidity, temperature, and pressure.
870
871
    Parameters
872
    ----------
873
    specific_humidity: `pint.Quantity`
874
        Specific humidity of air
875
    temperature: `pint.Quantity`
876
        Air temperature
877
    pressure: `pint.Quantity`
878
        Total atmospheric pressure
879
880
    Returns
881
    -------
882
    `pint.Quantity`
883
        Relative humidity
884
885
    Notes
886
    -----
887
    Formula from [Hobbs1977]_ pg. 74. and [Salby1996]_ pg. 118.
888
889
    .. math:: RH = 100 \frac{q}{(1-q)w_s}
890
891
    * :math:`RH` is relative humidity
892
    * :math:`q` is specific humidity
893
    * :math:`w_s` is the saturation mixing ratio
894
895
    See Also
896
    --------
897
    relative_humidity_from_mixing_ratio
898
899
    """
900
    return (100 * units.percent *
901
            mixing_ratio_from_specific_humidity(specific_humidity) /
902
            saturation_mixing_ratio(pressure, temperature))
903
904
905
@exporter.export
906
@check_units('[pressure]', '[temperature]', '[temperature]', '[temperature]')
907
def cape_cin(pressure, temperature, dewpt, parcel_profile):
908
    r"""Calculate CAPE and CIN.
909
910
    Calculate the convective available potential energy (CAPE) and convective inhibition (CIN)
911
    of a given upper air profile and parcel path. CIN is integrated between the surface and
912
    LFC, CAPE is integrated between the LFC and EL (or top of sounding). Intersection points of
913
    the measured temperature profile and parcel profile are linearly interpolated.
914
915
    Parameters
916
    ----------
917
    pressure : `pint.Quantity`
918
        The atmospheric pressure level(s) of interest. The first entry should be the starting
919
        point pressure.
920
    temperature : `pint.Quantity`
921
        The starting temperature
922
    dewpt : `pint.Quantity`
923
        The starting dew point
924
    parcel_profile : `pint.Quantity`
925
        The temperature profile of the parcel
926
927
    Returns
928
    -------
929
    `pint.Quantity`
930
        Convective available potential energy (CAPE).
931
    `pint.Quantity`
932
        Convective inhibition (CIN).
933
934
    Notes
935
    -----
936
    Formula adopted from [Hobbs1977]_.
937
938
    .. math:: \text{CAPE} = -R_d \int_{LFC}^{EL} (T_{parcel} - T_{env}) d\text{ln}(p)
939
940
    .. math:: \text{CIN} = -R_d \int_{SFC}^{LFC} (T_{parcel} - T_{env}) d\text{ln}(p)
941
942
943
    * :math:`CAPE` Convective available potential energy
944
    * :math:`CIN` Convective inhibition
945
    * :math:`LFC` Pressure of the level of free convection
946
    * :math:`EL` Pressure of the equilibrium level
947
    * :math:`SFC` Level of the surface or beginning of parcel path
948
    * :math:`R_d` Gas constant
949
    * :math:`g` Gravitational acceleration
950
    * :math:`T_{parcel}` Parcel temperature
951
    * :math:`T_{env}` Environment temperature
952
    * :math:`p` Atmospheric pressure
953
954
    See Also
955
    --------
956
    lfc, el
957
958
    """
959
    # Calculate LFC limit of integration
960
    lfc_pressure = lfc(pressure, temperature, dewpt)[0]
961
962
    # If there is no LFC, no need to proceed.
963
    if np.isnan(lfc_pressure):
964
        return 0 * units('J/kg'), 0 * units('J/kg')
965
    else:
966
        lfc_pressure = lfc_pressure.magnitude
967
968
    # Calculate the EL limit of integration
969
    el_pressure = el(pressure, temperature, dewpt)[0]
970
971
    # No EL and we use the top reading of the sounding.
972
    if np.isnan(el_pressure):
973
        el_pressure = pressure[-1].magnitude
974
    else:
975
        el_pressure = el_pressure.magnitude
976
977
    # Difference between the parcel path and measured temperature profiles
978
    y = (parcel_profile - temperature).to(units.degK)
979
980
    # Estimate zero crossings
981
    x, y = _find_append_zero_crossings(np.copy(pressure), y)
982
983
    # CAPE (temperature parcel < temperature environment)
984
    # Only use data between the LFC and EL for calculation
985
    p_mask = (x <= lfc_pressure) & (x >= el_pressure)
986
    x_clipped = x[p_mask]
987
    y_clipped = y[p_mask]
988
989
    y_clipped[y_clipped <= 0 * units.degK] = 0 * units.degK
990
    cape = (Rd * (np.trapz(y_clipped, np.log(x_clipped)) * units.degK)).to(units('J/kg'))
991
992
    # CIN (temperature parcel < temperature environment)
993
    # Only use data between the surface and LFC for calculation
994
    p_mask = (x >= lfc_pressure)
995
    x_clipped = x[p_mask]
996
    y_clipped = y[p_mask]
997
998
    y_clipped[y_clipped >= 0 * units.degK] = 0 * units.degK
999
    cin = (Rd * (np.trapz(y_clipped, np.log(x_clipped)) * units.degK)).to(units('J/kg'))
1000
1001
    return cape, cin
1002
1003
1004
def _find_append_zero_crossings(x, y):
1005
    r"""
1006
    Find and interpolate zero crossings.
1007
1008
    Estimate the zero crossings of an x,y series and add estimated crossings to series,
1009
    returning a sorted array with no duplicate values.
1010
1011
    Parameters
1012
    ----------
1013
    x : `pint.Quantity`
1014
        x values of data
1015
    y : `pint.Quantity`
1016
        y values of data
1017
1018
    Returns
1019
    -------
1020
    x : `pint.Quantity`
1021
        x values of data
1022
    y : `pint.Quantity`
1023
        y values of data
1024
1025
    """
1026
    # Find and append crossings to the data
1027
    crossings = find_intersections(x[1:], y[1:], np.zeros_like(y[1:]) * y.units)
1028
    x = concatenate((x, crossings[0]))
1029
    y = concatenate((y, crossings[1]))
1030
1031
    # Resort so that data are in order
1032
    sort_idx = np.argsort(x)
1033
    x = x[sort_idx]
1034
    y = y[sort_idx]
1035
1036
    # Remove duplicate data points if there are any
1037
    keep_idx = np.ediff1d(x, to_end=[1]) > 0
1038
    x = x[keep_idx]
1039
    y = y[keep_idx]
1040
    return x, y
1041
1042
1043
@exporter.export
1044
@check_units('[pressure]', '[temperature]', '[temperature]')
1045
def most_unstable_parcel(p, temperature, dewpoint, heights=None,
1046
                         bottom=None, depth=300 * units.hPa):
1047
    """
1048
    Determine the most unstable parcel in a layer.
1049
1050
    Determines the most unstable parcle of air by calculating the equivalent
1051
    potential temperature and finding its maximum in the specified layer.
1052
1053
    Parameters
1054
    ----------
1055
    p: `pint.Quantity`
1056
        Atmospheric pressure profile
1057
    temperature: `pint.Quantity`
1058
        Atmospheric temperature profile
1059
    dewpoint: `pint.Quantity`
1060
        Atmospheric dewpoint profile
1061
    heights: `pint.Quantity`
1062
        Atmospheric height profile. Standard atmosphere assumed when None.
1063
    bottom: `pint.Quantity`
1064
        Bottom of the layer to consider for the calculation in pressure or height
1065
    depth: `pint.Quantity`
1066
        Depth of the layer to consider for the calculation in pressure or height
1067
1068
    Returns
1069
    -------
1070
    `pint.Quantity`
1071
        Pressure, temperature, and dew point of most unstable parcel in the profile.
1072
1073
    See Also
1074
    --------
1075
    get_layer
1076
1077
    """
1078
    p_layer, T_layer, Td_layer = get_layer(p, temperature, dewpoint, bottom=bottom,
1079
                                           depth=depth, heights=heights)
1080
    theta_e = equivalent_potential_temperature_fromLCL(p_layer, T_layer)
1081
    max_idx = np.argmax(theta_e)
1082
    return p_layer[max_idx], T_layer[max_idx], Td_layer[max_idx]
1083