Completed
Push — master ( 8d2b75...91ff03 )
by Ryan
01:53
created

_isen_iter()   A

Complexity

Conditions 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

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