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