Completed
Push — master ( 6267cd...a45047 )
by Ryan
17s
created

_find_append_zero_crossings()   B

Complexity

Conditions 1

Size

Total Lines 37

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
c 2
b 0
f 0
dl 0
loc 37
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 find_intersections
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(pressure, temperature):
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
542
    temperature: `pint.Quantity`
543
        The temperature
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 = potential_temperature(pressure, temperature)
556
    smixr = saturation_mixing_ratio(pressure, temperature)
557
    return pottemp * np.exp(Lv * smixr / (Cp_d * temperature))
558
559
560
@exporter.export
561
@check_units('[temperature]', '[dimensionless]', '[dimensionless]')
562
def virtual_temperature(temperature, mixing, molecular_weight_ratio=epsilon):
563
    r"""Calculate virtual temperature.
564
565
    This calculation must be given an air parcel's temperature and mixing ratio.
566
    The implementation uses the formula outlined in [Hobbs2006]_ pg.80.
567
568
    Parameters
569
    ----------
570
    temperature: `pint.Quantity`
571
        The temperature
572
    mixing : `pint.Quantity`
573
        dimensionless mass mixing ratio
574
    molecular_weight_ratio : `pint.Quantity` or float, optional
575
        The ratio of the molecular weight of the constituent gas to that assumed
576
        for air. Defaults to the ratio for water vapor to dry air
577
        (:math:`\epsilon\approx0.622`).
578
579
    Returns
580
    -------
581
    `pint.Quantity`
582
        The corresponding virtual temperature of the parcel
583
584
    Notes
585
    -----
586
    .. math:: T_v = T \frac{\text{w} + \epsilon}{\epsilon\,(1 + \text{w})}
587
588
    """
589
    return temperature * ((mixing + molecular_weight_ratio) /
590
                          (molecular_weight_ratio * (1 + mixing)))
591
592
593
@exporter.export
594
@check_units('[pressure]', '[temperature]', '[dimensionless]', '[dimensionless]')
595
def virtual_potential_temperature(pressure, temperature, mixing,
596
                                  molecular_weight_ratio=epsilon):
597
    r"""Calculate virtual potential temperature.
598
599
    This calculation must be given an air parcel's pressure, temperature, and mixing ratio.
600
    The implementation uses the formula outlined in [Markowski2010]_ pg.13.
601
602
    Parameters
603
    ----------
604
    pressure: `pint.Quantity`
605
        Total atmospheric pressure
606
    temperature: `pint.Quantity`
607
        The temperature
608
    mixing : `pint.Quantity`
609
        dimensionless mass mixing ratio
610
    molecular_weight_ratio : `pint.Quantity` or float, optional
611
        The ratio of the molecular weight of the constituent gas to that assumed
612
        for air. Defaults to the ratio for water vapor to dry air
613
        (:math:`\epsilon\approx0.622`).
614
615
    Returns
616
    -------
617
    `pint.Quantity`
618
        The corresponding virtual potential temperature of the parcel
619
620
    Notes
621
    -----
622
    .. math:: \Theta_v = \Theta \frac{\text{w} + \epsilon}{\epsilon\,(1 + \text{w})}
623
624
    """
625
    pottemp = potential_temperature(pressure, temperature)
626
    return virtual_temperature(pottemp, mixing, molecular_weight_ratio)
627
628
629
@exporter.export
630
@check_units('[pressure]', '[temperature]', '[dimensionless]', '[dimensionless]')
631
def density(pressure, temperature, mixing, molecular_weight_ratio=epsilon):
632
    r"""Calculate density.
633
634
    This calculation must be given an air parcel's pressure, temperature, and mixing ratio.
635
    The implementation uses the formula outlined in [Hobbs2006]_ pg.67.
636
637
    Parameters
638
    ----------
639
    temperature: `pint.Quantity`
640
        The temperature
641
    pressure: `pint.Quantity`
642
        Total atmospheric pressure
643
    mixing : `pint.Quantity`
644
        dimensionless mass mixing ratio
645
    molecular_weight_ratio : `pint.Quantity` or float, optional
646
        The ratio of the molecular weight of the constituent gas to that assumed
647
        for air. Defaults to the ratio for water vapor to dry air
648
        (:math:`\epsilon\approx0.622`).
649
650
    Returns
651
    -------
652
    `pint.Quantity`
653
        The corresponding density of the parcel
654
655
    Notes
656
    -----
657
    .. math:: \rho = \frac{p}{R_dT_v}
658
659
    """
660
    virttemp = virtual_temperature(temperature, mixing, molecular_weight_ratio)
661
    return (pressure / (Rd * virttemp)).to(units.kilogram / units.meter ** 3)
662
663
664
@exporter.export
665
@check_units('[temperature]', '[temperature]', '[pressure]')
666
def relative_humidity_wet_psychrometric(dry_bulb_temperature, web_bulb_temperature,
667
                                        pressure, **kwargs):
668
    r"""Calculate the relative humidity with wet bulb and dry bulb temperatures.
669
670
    This uses a psychrometric relationship as outlined in [WMO8-2008]_, with
671
    coefficients from [Fan1987]_.
672
673
    Parameters
674
    ----------
675
    dry_bulb_temperature: `pint.Quantity`
676
        Dry bulb temperature
677
    web_bulb_temperature: `pint.Quantity`
678
        Wet bulb temperature
679
    pressure: `pint.Quantity`
680
        Total atmospheric pressure
681
682
    Returns
683
    -------
684
    `pint.Quantity`
685
        Relative humidity
686
687
    Notes
688
    -----
689
    .. math:: RH = 100 \frac{e}{e_s}
690
691
    * :math:`RH` is relative humidity
692
    * :math:`e` is vapor pressure from the wet psychrometric calculation
693
    * :math:`e_s` is the saturation vapor pressure
694
695
    See Also
696
    --------
697
    psychrometric_vapor_pressure_wet, saturation_vapor_pressure
698
699
    """
700
    return (100 * units.percent * psychrometric_vapor_pressure_wet(dry_bulb_temperature,
701
            web_bulb_temperature, pressure, **kwargs) /
702
            saturation_vapor_pressure(dry_bulb_temperature))
703
704
705
@exporter.export
706
@check_units('[temperature]', '[temperature]', '[pressure]')
707
def psychrometric_vapor_pressure_wet(dry_bulb_temperature, wet_bulb_temperature, pressure,
708
                                     psychrometer_coefficient=6.21e-4 / units.kelvin):
709
    r"""Calculate the vapor pressure with wet bulb and dry bulb temperatures.
710
711
    This uses a psychrometric relationship as outlined in [WMO8-2008]_, with
712
    coefficients from [Fan1987]_.
713
714
    Parameters
715
    ----------
716
    dry_bulb_temperature: `pint.Quantity`
717
        Dry bulb temperature
718
    wet_bulb_temperature: `pint.Quantity`
719
        Wet bulb temperature
720
    pressure: `pint.Quantity`
721
        Total atmospheric pressure
722
    psychrometer_coefficient: `pint.Quantity`
723
        Psychrometer coefficient
724
725
    Returns
726
    -------
727
    `pint.Quantity`
728
        Vapor pressure
729
730
    Notes
731
    -----
732
    .. math:: e' = e'_w(T_w) - A p (T - T_w)
733
734
    * :math:`e'` is vapor pressure
735
    * :math:`e'_w(T_w)` is the saturation vapor pressure with respect to water at temperature
736
      :math:`T_w`
737
    * :math:`p` is the pressure of the wet bulb
738
    * :math:`T` is the temperature of the dry bulb
739
    * :math:`T_w` is the temperature of the wet bulb
740
    * :math:`A` is the psychrometer coefficient
741
742
    Psychrometer coefficient depends on the specific instrument being used and the ventilation
743
    of the instrument.
744
745
    See Also
746
    --------
747
    saturation_vapor_pressure
748
749
    """
750
    return (saturation_vapor_pressure(wet_bulb_temperature) - psychrometer_coefficient *
751
            pressure * (dry_bulb_temperature - wet_bulb_temperature).to('kelvin'))
752
753
754
@exporter.export
755
@check_units('[dimensionless]', '[temperature]', '[pressure]')
756
def relative_humidity_from_mixing_ratio(mixing_ratio, temperature, pressure):
757
    r"""Calculate the relative humidity from mixing ratio, temperature, and pressure.
758
759
    Parameters
760
    ----------
761
    mixing_ratio: `pint.Quantity`
762
        Dimensionless mass mixing ratio
763
    temperature: `pint.Quantity`
764
        Air temperature
765
    pressure: `pint.Quantity`
766
        Total atmospheric pressure
767
768
    Returns
769
    -------
770
    `pint.Quantity`
771
        Relative humidity
772
773
    Notes
774
    -----
775
    Formula from [Hobbs1977]_ pg. 74.
776
777
    .. math:: RH = 100 \frac{w}{w_s}
778
779
    * :math:`RH` is relative humidity
780
    * :math:`w` is mxing ratio
781
    * :math:`w_s` is the saturation mixing ratio
782
783
    See Also
784
    --------
785
    saturation_mixing_ratio
786
787
    """
788
    return (100 * units.percent *
789
            mixing_ratio / saturation_mixing_ratio(pressure, temperature))
790
791
792
@exporter.export
793
@check_units('[dimensionless]')
794
def mixing_ratio_from_specific_humidity(specific_humidity):
795
    r"""Calculate the mixing ratio from specific humidity.
796
797
    Parameters
798
    ----------
799
    specific_humidity: `pint.Quantity`
800
        Specific humidity of air
801
802
    Returns
803
    -------
804
    `pint.Quantity`
805
        Mixing ratio
806
807
    Notes
808
    -----
809
    Formula from [Salby1996]_ pg. 118.
810
811
    .. math:: w = \frac{q}{1-q}
812
813
    * :math:`w` is mxing ratio
814
    * :math:`q` is the specific humidity
815
816
    See Also
817
    --------
818
    mixing_ratio
819
820
    """
821
    return specific_humidity / (1 - specific_humidity)
822
823
824
@exporter.export
825
@check_units('[dimensionless]', '[temperature]', '[pressure]')
826
def relative_humidity_from_specific_humidity(specific_humidity, temperature, pressure):
827
    r"""Calculate the relative humidity from specific humidity, temperature, and pressure.
828
829
    Parameters
830
    ----------
831
    specific_humidity: `pint.Quantity`
832
        Specific humidity of air
833
    temperature: `pint.Quantity`
834
        Air temperature
835
    pressure: `pint.Quantity`
836
        Total atmospheric pressure
837
838
    Returns
839
    -------
840
    `pint.Quantity`
841
        Relative humidity
842
843
    Notes
844
    -----
845
    Formula from [Hobbs1977]_ pg. 74. and [Salby1996]_ pg. 118.
846
847
    .. math:: RH = 100 \frac{q}{(1-q)w_s}
848
849
    * :math:`RH` is relative humidity
850
    * :math:`q` is specific humidity
851
    * :math:`w_s` is the saturation mixing ratio
852
853
    See Also
854
    --------
855
    relative_humidity_from_mixing_ratio
856
857
    """
858
    return (100 * units.percent *
859
            mixing_ratio_from_specific_humidity(specific_humidity) /
860
            saturation_mixing_ratio(pressure, temperature))
861
862
863
@exporter.export
864
@check_units('[pressure]', '[temperature]', '[temperature]', '[temperature]')
865
def cape_cin(pressure, temperature, dewpt, parcel_profile):
866
    r"""Calculate CAPE and CIN.
867
868
    Calculate the convective available potential energy (CAPE) and convective inhibition (CIN)
869
    of a given upper air profile and parcel path. CIN is integrated between the surface and
870
    LFC, CAPE is integrated between the LFC and EL (or top of sounding). Intersection points of
871
    the measured temperature profile and parcel profile are linearly interpolated.
872
873
    Parameters
874
    ----------
875
    pressure : `pint.Quantity`
876
        The atmospheric pressure level(s) of interest. The first entry should be the starting
877
        point pressure.
878
    temperature : `pint.Quantity`
879
        The starting temperature
880
    dewpt : `pint.Quantity`
881
        The starting dew point
882
    parcel_profile : `pint.Quantity`
883
        The temperature profile of the parcel
884
885
    Returns
886
    -------
887
    `pint.Quantity`
888
        Convective available potential energy (CAPE).
889
    `pint.Quantity`
890
        Convective inhibition (CIN).
891
892
    Notes
893
    -----
894
    Formula adopted from [Hobbs1977]_.
895
896
    .. math:: \text{CAPE} = -R_d \int_{LFC}^{EL} (T_{parcel} - T_{env}) d\text{ln}(p)
897
898
    .. math:: \text{CIN} = -R_d \int_{SFC}^{LFC} (T_{parcel} - T_{env}) d\text{ln}(p)
899
900
901
    * :math:`CAPE` Convective available potential energy
902
    * :math:`CIN` Convective inhibition
903
    * :math:`LFC` Pressure of the level of free convection
904
    * :math:`EL` Pressure of the equilibrium level
905
    * :math:`SFC` Level of the surface or beginning of parcel path
906
    * :math:`R_d` Gas constant
907
    * :math:`g` Gravitational acceleration
908
    * :math:`T_{parcel}` Parcel temperature
909
    * :math:`T_{env}` Environment temperature
910
    * :math:`p` Atmospheric pressure
911
912
    See Also
913
    --------
914
    lfc, el
915
916
    """
917
    # Calculate LFC limit of integration
918
    lfc_pressure = lfc(pressure, temperature, dewpt)[0]
919
920
    # If there is no LFC, no need to proceed.
921
    if np.isnan(lfc_pressure):
922
        return 0 * units('J/kg'), 0 * units('J/kg')
923
    else:
924
        lfc_pressure = lfc_pressure.magnitude
925
926
    # Calculate the EL limit of integration
927
    el_pressure = el(pressure, temperature, dewpt)[0]
928
929
    # No EL and we use the top reading of the sounding.
930
    if np.isnan(el_pressure):
931
        el_pressure = pressure[-1].magnitude
932
    else:
933
        el_pressure = el_pressure.magnitude
934
935
    # Difference between the parcel path and measured temperature profiles
936
    y = (parcel_profile - temperature).to(units.degK)
937
938
    # Estimate zero crossings
939
    x, y = _find_append_zero_crossings(np.copy(pressure), y)
940
941
    # CAPE (temperature parcel < temperature environment)
942
    # Only use data between the LFC and EL for calculation
943
    p_mask = (x <= lfc_pressure) & (x >= el_pressure)
944
    x_clipped = x[p_mask]
945
    y_clipped = y[p_mask]
946
947
    y_clipped[y_clipped <= 0 * units.degK] = 0 * units.degK
948
    cape = (Rd * (np.trapz(y_clipped, np.log(x_clipped)) * units.degK)).to(units('J/kg'))
949
950
    # CIN (temperature parcel < temperature environment)
951
    # Only use data between the surface and LFC for calculation
952
    p_mask = (x >= lfc_pressure)
953
    x_clipped = x[p_mask]
954
    y_clipped = y[p_mask]
955
956
    y_clipped[y_clipped >= 0 * units.degK] = 0 * units.degK
957
    cin = (Rd * (np.trapz(y_clipped, np.log(x_clipped)) * units.degK)).to(units('J/kg'))
958
959
    return cape, cin
960
961
962
def _find_append_zero_crossings(x, y):
963
    r"""
964
    Find and interpolate zero crossings.
965
966
    Estimate the zero crossings of an x,y series and add estimated crossings to series,
967
    returning a sorted array with no duplicate values.
968
969
    Parameters
970
    ----------
971
    x : `pint.Quantity`
972
        x values of data
973
    y : `pint.Quantity`
974
        y values of data
975
976
    Returns
977
    -------
978
    x : `pint.Quantity`
979
        x values of data
980
    y : `pint.Quantity`
981
        y values of data
982
983
    """
984
    # Find and append crossings to the data
985
    crossings = find_intersections(x[1:], y[1:], np.zeros_like(y[1:]) * y.units)
986
    x = concatenate((x, crossings[0]))
987
    y = concatenate((y, crossings[1]))
988
989
    # Resort so that data are in order
990
    sort_idx = np.argsort(x)
991
    x = x[sort_idx]
992
    y = y[sort_idx]
993
994
    # Remove duplicate data points if there are any
995
    keep_idx = np.ediff1d(x, to_end=[1]) > 0
996
    x = x[keep_idx]
997
    y = y[keep_idx]
998
    return x, y
999