Completed
Pull Request — master (#322)
by
unknown
02:02 queued 36s
created

el()   B

Complexity

Conditions 2

Size

Total Lines 35

Duplication

Lines 35
Ratio 100 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
c 1
b 0
f 0
dl 35
loc 35
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
11
from .tools import find_intersections
12
from ..constants import Cp_d, epsilon, kappa, Lv, P0, Rd
13
from ..package_tools import Exporter
14
from ..units import atleast_1d, concatenate, units
15
16
exporter = Exporter(globals())
17
18
sat_pressure_0c = 6.112 * units.millibar
19
20
21
@exporter.export
22
def potential_temperature(pressure, temperature):
23
    r"""Calculate the potential temperature.
24
25
    Uses the Poisson equation to calculation the potential temperature
26
    given `pressure` and `temperature`.
27
28
    Parameters
29
    ----------
30
    pressure : `pint.Quantity`
31
        The total atmospheric pressure
32
    temperature : `pint.Quantity`
33
        The temperature
34
35
    Returns
36
    -------
37
    `pint.Quantity`
38
        The potential temperature corresponding to the the temperature and
39
        pressure.
40
41
    See Also
42
    --------
43
    dry_lapse
44
45
    Notes
46
    -----
47
    Formula:
48
49
    .. math:: \Theta = T (P_0 / P)^\kappa
50
51
    Examples
52
    --------
53
    >>> from metpy.units import units
54
    >>> metpy.calc.potential_temperature(800. * units.mbar, 273. * units.kelvin)
55
    290.9814150577374
56
    """
57
    return temperature * (P0 / pressure).to('dimensionless')**kappa
58
59
60
@exporter.export
61
def dry_lapse(pressure, temperature):
62
    r"""Calculate the temperature at a level assuming only dry processes.
63
64
    This function lifts a parcel starting at `temperature`, conserving
65
    potential temperature. The starting pressure should be the first item in
66
    the `pressure` array.
67
68
    Parameters
69
    ----------
70
    pressure : `pint.Quantity`
71
        The atmospheric pressure level(s) of interest
72
    temperature : `pint.Quantity`
73
        The starting temperature
74
75
    Returns
76
    -------
77
    `pint.Quantity`
78
       The resulting parcel temperature at levels given by `pressure`
79
80
    See Also
81
    --------
82
    moist_lapse : Calculate parcel temperature assuming liquid saturation
83
                  processes
84
    parcel_profile : Calculate complete parcel profile
85
    potential_temperature
86
    """
87
    return temperature * (pressure / pressure[0])**kappa
88
89
90
@exporter.export
91
def moist_lapse(pressure, temperature):
92
    r"""Calculate the temperature at a level assuming liquid saturation processes.
93
94
    This function lifts a parcel starting at `temperature`. The starting
95
    pressure should be the first item in the `pressure` array. Essentially,
96
    this function is calculating moist pseudo-adiabats.
97
98
    Parameters
99
    ----------
100
    pressure : `pint.Quantity`
101
        The atmospheric pressure level(s) of interest
102
    temperature : `pint.Quantity`
103
        The starting temperature
104
105
    Returns
106
    -------
107
    `pint.Quantity`
108
       The temperature corresponding to the the starting temperature and
109
       pressure levels.
110
111
    See Also
112
    --------
113
    dry_lapse : Calculate parcel temperature assuming dry adiabatic processes
114
    parcel_profile : Calculate complete parcel profile
115
116
    Notes
117
    -----
118
    This function is implemented by integrating the following differential
119
    equation:
120
121
    .. math:: \frac{dT}{dP} = \frac{1}{P} \frac{R_d T + L_v r_s}
122
                                {C_{pd} + \frac{L_v^2 r_s \epsilon}{R_d T^2}}
123
124
    This equation comes from [1]_.
125
126
    References
127
    ----------
128
    .. [1] Bakhshaii, A. and R. Stull, 2013: Saturated Pseudoadiabats--A
129
           Noniterative Approximation. J. Appl. Meteor. Clim., 52, 5-15.
130
    """
131
    def dt(t, p):
132
        t = units.Quantity(t, temperature.units)
133
        p = units.Quantity(p, pressure.units)
134
        rs = saturation_mixing_ratio(p, t)
135
        frac = ((Rd * t + Lv * rs) /
136
                (Cp_d + (Lv * Lv * rs * epsilon / (Rd * t * t)))).to('kelvin')
137
        return frac / p
138
    return units.Quantity(si.odeint(dt, atleast_1d(temperature).squeeze(),
139
                                    pressure.squeeze()).T.squeeze(), temperature.units)
140
141
142
@exporter.export
143
def lcl(pressure, temperature, dewpt, max_iters=50, eps=1e-2):
144
    r"""Calculate the lifted condensation level (LCL) using from the starting point.
145
146
    The starting state for the parcel is defined by `temperature`, `dewpt`,
147
    and `pressure`.
148
149
    Parameters
150
    ----------
151
    pressure : `pint.Quantity`
152
        The starting atmospheric pressure
153
    temperature : `pint.Quantity`
154
        The starting temperature
155
    dewpt : `pint.Quantity`
156
        The starting dew point
157
158
    Returns
159
    -------
160
    `(pint.Quantity, pint.Quantity)`
161
        The LCL pressure and temperature
162
163
    Other Parameters
164
    ----------------
165
    max_iters : int, optional
166
        The maximum number of iterations to use in calculation, defaults to 50.
167
    eps : float, optional
168
        The desired absolute error in the calculated value, defaults to 1e-2.
169
170
    See Also
171
    --------
172
    parcel_profile
173
174
    Notes
175
    -----
176
    This function is implemented using an iterative approach to solve for the
177
    LCL. The basic algorithm is:
178
179
    1. Find the dew point from the LCL pressure and starting mixing ratio
180
    2. Find the LCL pressure from the starting temperature and dewpoint
181
    3. Iterate until convergence
182
183
    The function is guaranteed to finish by virtue of the `max_iters` counter.
184
    """
185
    w = mixing_ratio(saturation_vapor_pressure(dewpt), pressure)
186
    new_p = p = pressure
187
    eps = units.Quantity(eps, p.units)
188
    while max_iters:
189
        td = dewpoint(vapor_pressure(p, w))
190
        new_p = pressure * (td / temperature) ** (1. / kappa)
191
        if np.abs(new_p - p).max() < eps:
192
            break
193
        p = new_p
194
        max_iters -= 1
195
196
    return new_p, td
197
198
199 View Code Duplication
@exporter.export
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
200
def lfc(pressure, temperature, dewpt):
201
    r"""Calculate the level of free convection (LFC).
202
203
    This works by finding the first intersection of the ideal parcel path and
204
    the measured parcel temperature.
205
206
    Parameters
207
    ----------
208
    pressure : `pint.Quantity`
209
        The atmospheric pressure
210
    temperature : `pint.Quantity`
211
        The temperature at the levels given by `pressure`
212
    dewpt : `pint.Quantity`
213
        The dew point at the levels given by `pressure`
214
215
    Returns
216
    -------
217
    `pint.Quantity`
218
        The LFC
219
220
    See Also
221
    --------
222
    parcel_profile
223
    """
224
    ideal_profile = parcel_profile(pressure, temperature[0], dewpt[0]).to('degC')
225
226
    # The parcel profile and data have the same first data point, so we ignore
227
    # that point to get the real first intersection for the LFC calculation.
228
    x, y = find_intersections(pressure[1:], ideal_profile[1:], temperature[1:])
229
    return x[0], y[0]
230
231
232 View Code Duplication
@exporter.export
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
233
def el(pressure, temperature, dewpt):
234
    r"""Calculate the equilibrium level (EL).
235
236
    This works by finding the last intersection of the ideal parcel path and
237
    the measured parcel temperature. If there is one or fewer intersections, there is
238
    no equilibrium level.
239
240
    Parameters
241
    ----------
242
    pressure : `pint.Quantity`
243
        The atmospheric pressure
244
    temperature : `pint.Quantity`
245
        The temperature at the levels given by `pressure`
246
    dewpt : `pint.Quantity`
247
        The dew point at the levels given by `pressure`
248
249
    Returns
250
    -------
251
    `pint.Quantity, pint.Quantity`
252
        The EL pressure and temperature
253
254
    See Also
255
    --------
256
    parcel_profile
257
    """
258
    ideal_profile = parcel_profile(pressure, temperature[0], dewpt[0]).to('degC')
259
    x, y = find_intersections(pressure[1:], ideal_profile[1:], temperature[1:])
260
261
    # If there is only one intersection, it's the LFC and we return None.
262
    if len(x) <= 1:
263
        return None, None
264
265
    else:
266
        return x[-1], y[-1]
267
268
269
@exporter.export
270
def parcel_profile(pressure, temperature, dewpt):
271
    r"""Calculate the profile a parcel takes through the atmosphere.
272
273
    The parcel starts at `temperature`, and `dewpt`, lifted up
274
    dry adiabatically to the LCL, and then moist adiabatically from there.
275
    `pressure` specifies the pressure levels for the profile.
276
277
    Parameters
278
    ----------
279
    pressure : `pint.Quantity`
280
        The atmospheric pressure level(s) of interest. The first entry should be the starting
281
        point pressure.
282
    temperature : `pint.Quantity`
283
        The starting temperature
284
    dewpt : `pint.Quantity`
285
        The starting dew point
286
287
    Returns
288
    -------
289
    `pint.Quantity`
290
        The parcel temperatures at the specified pressure levels.
291
292
    See Also
293
    --------
294
    lcl, moist_lapse, dry_lapse
295
    """
296
    # Find the LCL
297
    l = lcl(pressure[0], temperature, dewpt)[0].to(pressure.units)
298
299
    # Find the dry adiabatic profile, *including* the LCL. We need >= the LCL in case the
300
    # LCL is included in the levels. It's slightly redundant in that case, but simplifies
301
    # the logic for removing it later.
302
    press_lower = concatenate((pressure[pressure >= l], l))
303
    t1 = dry_lapse(press_lower, temperature)
304
305
    # Find moist pseudo-adiabatic profile starting at the LCL
306
    press_upper = concatenate((l, pressure[pressure < l]))
307
    t2 = moist_lapse(press_upper, t1[-1]).to(t1.units)
308
309
    # Return LCL *without* the LCL point
310
    return concatenate((t1[:-1], t2[1:]))
311
312
313
@exporter.export
314
def virtual_temperature(pressure, temperature, dewpt):
315
    r"""Calculate virtual temperature.
316
317
    Description
318
319
    Parameters
320
    ----------
321
    pressure : `pint.Quantity`
322
        The atmospheric pressure level(s) of interest. The first entry should be the starting
323
        point pressure.
324
    temperature : `pint.Quantity`
325
        The starting temperature
326
    dewpt : `pint.Quantity`
327
        The starting dew point
328
329
    Returns
330
    -------
331
    `pint.Quantity`
332
        The virtual temperature.
333
334
    See Also
335
    --------
336
    mixing_ratio, saturation_vapor_pressure
337
    """
338
    Rv = mixing_ratio(saturation_vapor_pressure(dewpt), pressure)
339
    return temperature * (1 + Rv / epsilon) / (1 + Rv)
340
341
def _find_append_zero_crossings(x, y):
342
    r"""
343
    Find and interpolate zero crossings
344
345
    Estimate the zero crossings of an x,y series and add estimated crossings to series,
346
    returning a sorted array with no duplicate values.
347
348
    Parameters
349
    ----------
350
    x : `pint.Quantity`
351
        x values of data
352
    y : `pint.Quantity`
353
        y values of data
354
355
    Returns
356
    -------
357
    x : `pint.Quantity`
358
        x values of data
359
    y : `pint.Quantity`
360
        y values of data
361
362
    """
363
    # Find and append crossings to the data
364
    crossings = find_intersections(x[1:], y[1:], np.zeros_like(y[1:]))
365
    x = concatenate((x, crossings[0]))
366
    y = concatenate((y, crossings[1]))
367
368
    # Resort so that data are in order
369
    sort_idx = np.argsort(x)
370
    x = x[sort_idx]
371
    y = y[sort_idx]
372
373
    # Remove duplicate data points if there are any
374
    keep_idx = np.ediff1d(x, to_end=[1]) > 0
375
    x = x[keep_idx]
376
    y = y[keep_idx]
377
    return x, y
378
379 View Code Duplication
@exporter.export
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
380
def cape(pressure, temperature, dewpt, parcel_temperature=None, use_virtual_temperature=False):
381
    r"""Calculate convective available potential energy (CAPE).
382
383
        Description
384
385
        Parameters
386
        ----------
387
        pressure : `pint.Quantity`
388
            The atmospheric pressure level(s) of interest. The first entry should be the starting
389
            point pressure.
390
        temperature : `pint.Quantity`
391
            The starting temperature
392
        dewpt : `pint.Quantity`
393
            The starting dew point
394
        parcel_temperature : `pint.Quantity`
395
            The temperature of the parcel
396
397
        Returns
398
        -------
399
        `pint.Quantity`
400
            Convective available potential energy (CAPE).
401
402
        See Also
403
        --------
404
        virtual_temperature, find_intersections, lfc, el
405
        """
406
    if parcel_temperature is None:
407
        parcel_temperature = parcel_profile(pressure, temperature[0], dewpt[0])
408
409
    if use_virtual_temperature:
410
        temperature = virtual_temperature(pressure, temperature, dewpt)
411
        parcel_temperature = virtual_temperature(pressure, parcel_temperature, dewpt)
412
413
    y = (parcel_temperature - temperature).to(units.degK)
414
415
    x,y = _find_append_zero_crossings(np.copy(pressure),y)
416
417
    # Clip out negative area (temperature parcel < temperature environment)
418
    y[y <= 0 * units.degK] = 0 * units.degK
419
420
    # Only use data between the LFC and EL for calculation
421
    lfc_pressure = lfc(pressure, temperature, dewpt)[0].magnitude
422
    el_pressure = el(pressure, temperature, dewpt)[0].magnitude
423
    p_mask = (x <= lfc_pressure) & (x >= el_pressure)
424
    x = x[p_mask]
425
    y = y[p_mask]
426
427
    return (Rd * (np.trapz(y, np.log(x)) * units.degK)).to(units('J/kg'))
428
429
430 View Code Duplication
@exporter.export
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
431
def cin(pressure, temperature, dewpt, parcel_temperature=None, use_virtual_temperature=False):
432
    r"""Calculate convective inhibition (CIN).
433
434
        Description
435
436
        Parameters
437
        ----------
438
        pressure : `pint.Quantity`
439
            The atmospheric pressure level(s) of interest. The first entry should be the starting
440
            point pressure.
441
        temperature : `pint.Quantity`
442
            The starting temperature
443
        dewpt : `pint.Quantity`
444
            The starting dew point
445
        parcel_temperature : `pint.Quantity`
446
            The temperature of the parcel
447
448
        Returns
449
        -------
450
        `pint.Quantity`
451
            Convective inhibition (CIN).
452
453
        See Also
454
        --------
455
        virtual_temperature, find_intersections, lfc
456
        """
457
    if parcel_temperature is None:
458
        parcel_temperature = parcel_profile(pressure, temperature[0], dewpt[0])
459
460
    if use_virtual_temperature:
461
        temperature = virtual_temperature(pressure, temperature, dewpt)
462
        parcel_temperature = virtual_temperature(pressure, parcel_temperature, dewpt)
463
464
    y = (parcel_temperature - temperature).to(units.degK)
465
466
    x,y = _find_append_zero_crossings(np.copy(pressure),y)
467
468
    # Clip out positive area (temperature parcel > temperature environment)
469
    y[y >= 0 * units.degK] = 0 * units.degK
470
471
    # Only use data between the surface and the LFC for calculation
472
    lfc_pressure = lfc(pressure, temperature, dewpt)[0].magnitude
473
    p_mask = (x >= lfc_pressure)
474
    x = x[p_mask]
475
    y = y[p_mask]
476
477
    return (Rd * (np.trapz(y, np.log(x)) * units.degK)).to(units('J/kg'))
478
479
480
@exporter.export
481
def vapor_pressure(pressure, mixing):
482
    r"""Calculate water vapor (partial) pressure.
483
484
    Given total `pressure` and water vapor `mixing` ratio, calculates the
485
    partial pressure of water vapor.
486
487
    Parameters
488
    ----------
489
    pressure : `pint.Quantity`
490
        total atmospheric pressure
491
    mixing : `pint.Quantity`
492
        dimensionless mass mixing ratio
493
494
    Returns
495
    -------
496
    `pint.Quantity`
497
        The ambient water vapor (partial) pressure in the same units as
498
        `pressure`.
499
500
    Notes
501
    -----
502
    This function is a straightforward implementation of the equation given in many places,
503
    such as [2]_:
504
505
    .. math:: e = p \frac{r}{r + \epsilon}
506
507
    References
508
    ----------
509
    .. [2] Hobbs, Peter V. and Wallace, John M., 1977: Atmospheric Science, an Introductory
510
           Survey. 71.
511
512
    See Also
513
    --------
514
    saturation_vapor_pressure, dewpoint
515
    """
516
    return pressure * mixing / (epsilon + mixing)
517
518
519
@exporter.export
520
def saturation_vapor_pressure(temperature):
521
    r"""Calculate the saturation water vapor (partial) pressure.
522
523
    Parameters
524
    ----------
525
    temperature : `pint.Quantity`
526
        The temperature
527
528
    Returns
529
    -------
530
    `pint.Quantity`
531
        The saturation water vapor (partial) pressure
532
533
    See Also
534
    --------
535
    vapor_pressure, dewpoint
536
537
    Notes
538
    -----
539
    Instead of temperature, dewpoint may be used in order to calculate
540
    the actual (ambient) water vapor (partial) pressure.
541
542
    The formula used is that from Bolton 1980 [3]_ for T in degrees Celsius:
543
544
    .. math:: 6.112 e^\frac{17.67T}{T + 243.5}
545
546
    References
547
    ----------
548
    .. [3] Bolton, D., 1980: The Computation of Equivalent Potential
549
           Temperature. Mon. Wea. Rev., 108, 1046-1053.
550
    """
551
    # Converted from original in terms of C to use kelvin. Using raw absolute values of C in
552
    # a formula plays havoc with units support.
553
    return sat_pressure_0c * np.exp(17.67 * (temperature - 273.15 * units.kelvin) /
554
                                    (temperature - 29.65 * units.kelvin))
555
556
557
@exporter.export
558
def dewpoint_rh(temperature, rh):
559
    r"""Calculate the ambient dewpoint given air temperature and relative humidity.
560
561
    Parameters
562
    ----------
563
    temperature : `pint.Quantity`
564
        Air temperature
565
    rh : `pint.Quantity`
566
        Relative humidity expressed as a ratio in the range [0, 1]
567
568
    Returns
569
    -------
570
    `pint.Quantity`
571
        The dew point temperature
572
573
    See Also
574
    --------
575
    dewpoint, saturation_vapor_pressure
576
    """
577
    return dewpoint(rh * saturation_vapor_pressure(temperature))
578
579
580
@exporter.export
581
def dewpoint(e):
582
    r"""Calculate the ambient dewpoint given the vapor pressure.
583
584
    Parameters
585
    ----------
586
    e : `pint.Quantity`
587
        Water vapor partial pressure
588
589
    Returns
590
    -------
591
    `pint.Quantity`
592
        Dew point temperature
593
594
    See Also
595
    --------
596
    dewpoint_rh, saturation_vapor_pressure, vapor_pressure
597
598
    Notes
599
    -----
600
    This function inverts the Bolton 1980 [4]_ formula for saturation vapor
601
    pressure to instead calculate the temperature. This yield the following
602
    formula for dewpoint in degrees Celsius:
603
604
    .. math:: T = \frac{243.5 log(e / 6.112)}{17.67 - log(e / 6.112)}
605
606
    References
607
    ----------
608
    .. [4] Bolton, D., 1980: The Computation of Equivalent Potential
609
           Temperature. Mon. Wea. Rev., 108, 1046-1053.
610
    """
611
    val = np.log(e / sat_pressure_0c)
612
    return 0. * units.degC + 243.5 * units.delta_degC * val / (17.67 - val)
613
614
615
@exporter.export
616
def mixing_ratio(part_press, tot_press, molecular_weight_ratio=epsilon):
617
    r"""Calculate the mixing ratio of a gas.
618
619
    This calculates mixing ratio given its partial pressure and the total pressure of
620
    the air. There are no required units for the input arrays, other than that
621
    they have the same units.
622
623
    Parameters
624
    ----------
625
    part_press : `pint.Quantity`
626
        Partial pressure of the constituent gas
627
    tot_press : `pint.Quantity`
628
        Total air pressure
629
    molecular_weight_ratio : `pint.Quantity` or float, optional
630
        The ratio of the molecular weight of the constituent gas to that assumed
631
        for air. Defaults to the ratio for water vapor to dry air
632
        (:math:`\epsilon\approx0.622`).
633
634
    Returns
635
    -------
636
    `pint.Quantity`
637
        The (mass) mixing ratio, dimensionless (e.g. Kg/Kg or g/g)
638
639
    Notes
640
    -----
641
    This function is a straightforward implementation of the equation given in many places,
642
    such as [5]_:
643
644
    .. math:: r = \epsilon \frac{e}{p - e}
645
646
    References
647
    ----------
648
    .. [5] Hobbs, Peter V. and Wallace, John M., 1977: Atmospheric Science, an Introductory
649
           Survey. 73.
650
651
    See Also
652
    --------
653
    saturation_mixing_ratio, vapor_pressure
654
    """
655
    return (molecular_weight_ratio * part_press / (tot_press - part_press)).to("dimensionless")
656
657
658
@exporter.export
659
def saturation_mixing_ratio(tot_press, temperature):
660
    r"""Calculate the saturation mixing ratio of water vapor.
661
662
    This calculation is given total pressure and the temperature. The implementation
663
    uses the formula outlined in [6]_.
664
665
    Parameters
666
    ----------
667
    tot_press: `pint.Quantity`
668
        Total atmospheric pressure
669
    temperature: `pint.Quantity`
670
        The temperature
671
672
    Returns
673
    -------
674
    `pint.Quantity`
675
        The saturation mixing ratio, dimensionless
676
677
    References
678
    ----------
679
    .. [6] Hobbs, Peter V. and Wallace, John M., 1977: Atmospheric Science, an Introductory
680
           Survey. 73.
681
    """
682
    return mixing_ratio(saturation_vapor_pressure(temperature), tot_press)
683
684
685
@exporter.export
686
def equivalent_potential_temperature(pressure, temperature):
687
    r"""Calculate equivalent potential temperature.
688
689
    This calculation must be given an air parcel's pressure and temperature.
690
    The implementation uses the formula outlined in [7]_.
691
692
    Parameters
693
    ----------
694
    pressure: `pint.Quantity`
695
        Total atmospheric pressure
696
    temperature: `pint.Quantity`
697
        The temperature
698
699
    Returns
700
    -------
701
    `pint.Quantity`
702
        The corresponding equivalent potential temperature of the parcel
703
704
    Notes
705
    -----
706
    .. math:: \Theta_e = \Theta e^\frac{L_v r_s}{C_{pd} T}
707
708
    References
709
    ----------
710
    .. [7] Hobbs, Peter V. and Wallace, John M., 1977: Atmospheric Science, an Introductory
711
           Survey. 78-79.
712
    """
713
    pottemp = potential_temperature(pressure, temperature)
714
    smixr = saturation_mixing_ratio(pressure, temperature)
715
    return pottemp * np.exp(Lv * smixr / (Cp_d * temperature))
716