Completed
Pull Request — master (#454)
by
unknown
01:36
created

get_isentropic_pressure()   F

Complexity

Conditions 13

Size

Total Lines 175

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 0 Features 1
Metric Value
cc 13
c 5
b 0
f 1
dl 0
loc 175
rs 2

1 Method

Rating   Name   Duplication   Size   Complexity  
A _isen_iter() 0 6 1

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Complexity

Complex classes like get_isentropic_pressure() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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 find_intersections
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
201
202 View Code Duplication
@exporter.export
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
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
    if len(x) == 0:
236
        return np.nan * pressure.units, np.nan * temperature.units
237
    else:
238
        return x[0], y[0]
239
240 View Code Duplication
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
241
@exporter.export
242
@check_units('[pressure]', '[temperature]', '[temperature]')
243
def el(pressure, temperature, dewpt):
244
    r"""Calculate the equilibrium level.
245
246
    This works by finding the last intersection of the ideal parcel path and
247
    the measured environmental temperature. If there is one or fewer intersections, there is
248
    no equilibrium level.
249
250
    Parameters
251
    ----------
252
    pressure : `pint.Quantity`
253
        The atmospheric pressure
254
    temperature : `pint.Quantity`
255
        The temperature at the levels given by `pressure`
256
    dewpt : `pint.Quantity`
257
        The dew point at the levels given by `pressure`
258
259
    Returns
260
    -------
261
    `pint.Quantity, pint.Quantity`
262
        The EL pressure and temperature
263
264
    See Also
265
    --------
266
    parcel_profile
267
268
    """
269
    ideal_profile = parcel_profile(pressure, temperature[0], dewpt[0]).to('degC')
270
    x, y = find_intersections(pressure[1:], ideal_profile[1:], temperature[1:])
271
272
    # If there is only one intersection, it's the LFC and we return None.
273
    if len(x) <= 1:
274
        return np.nan * pressure.units, np.nan * temperature.units
275
    else:
276
        return x[-1], y[-1]
277
278
279
@exporter.export
280
@check_units('[pressure]', '[temperature]', '[temperature]')
281
def parcel_profile(pressure, temperature, dewpt):
282
    r"""Calculate the profile a parcel takes through the atmosphere.
283
284
    The parcel starts at `temperature`, and `dewpt`, lifted up
285
    dry adiabatically to the LCL, and then moist adiabatically from there.
286
    `pressure` specifies the pressure levels for the profile.
287
288
    Parameters
289
    ----------
290
    pressure : `pint.Quantity`
291
        The atmospheric pressure level(s) of interest. The first entry should be the starting
292
        point pressure.
293
    temperature : `pint.Quantity`
294
        The starting temperature
295
    dewpt : `pint.Quantity`
296
        The starting dew point
297
298
    Returns
299
    -------
300
    `pint.Quantity`
301
        The parcel temperatures at the specified pressure levels.
302
303
    See Also
304
    --------
305
    lcl, moist_lapse, dry_lapse
306
307
    """
308
    # Find the LCL
309
    l = lcl(pressure[0], temperature, dewpt)[0].to(pressure.units)
310
311
    # Find the dry adiabatic profile, *including* the LCL. We need >= the LCL in case the
312
    # LCL is included in the levels. It's slightly redundant in that case, but simplifies
313
    # the logic for removing it later.
314
    press_lower = concatenate((pressure[pressure >= l], l))
315
    t1 = dry_lapse(press_lower, temperature)
316
317
    # Find moist pseudo-adiabatic profile starting at the LCL
318
    press_upper = concatenate((l, pressure[pressure < l]))
319
    t2 = moist_lapse(press_upper, t1[-1]).to(t1.units)
320
321
    # Return LCL *without* the LCL point
322
    return concatenate((t1[:-1], t2[1:]))
323
324
325
@exporter.export
326
@check_units('[pressure]', '[dimensionless]')
327
def vapor_pressure(pressure, mixing):
328
    r"""Calculate water vapor (partial) pressure.
329
330
    Given total `pressure` and water vapor `mixing` ratio, calculates the
331
    partial pressure of water vapor.
332
333
    Parameters
334
    ----------
335
    pressure : `pint.Quantity`
336
        total atmospheric pressure
337
    mixing : `pint.Quantity`
338
        dimensionless mass mixing ratio
339
340
    Returns
341
    -------
342
    `pint.Quantity`
343
        The ambient water vapor (partial) pressure in the same units as
344
        `pressure`.
345
346
    Notes
347
    -----
348
    This function is a straightforward implementation of the equation given in many places,
349
    such as [Hobbs1977]_ pg.71:
350
351
    .. math:: e = p \frac{r}{r + \epsilon}
352
353
    See Also
354
    --------
355
    saturation_vapor_pressure, dewpoint
356
357
    """
358
    return pressure * mixing / (epsilon + mixing)
359
360
361
@exporter.export
362
@check_units('[temperature]')
363
def saturation_vapor_pressure(temperature):
364
    r"""Calculate the saturation water vapor (partial) pressure.
365
366
    Parameters
367
    ----------
368
    temperature : `pint.Quantity`
369
        The temperature
370
371
    Returns
372
    -------
373
    `pint.Quantity`
374
        The saturation water vapor (partial) pressure
375
376
    See Also
377
    --------
378
    vapor_pressure, dewpoint
379
380
    Notes
381
    -----
382
    Instead of temperature, dewpoint may be used in order to calculate
383
    the actual (ambient) water vapor (partial) pressure.
384
385
    The formula used is that from [Bolton1980]_ for T in degrees Celsius:
386
387
    .. math:: 6.112 e^\frac{17.67T}{T + 243.5}
388
389
    """
390
    # Converted from original in terms of C to use kelvin. Using raw absolute values of C in
391
    # a formula plays havoc with units support.
392
    return sat_pressure_0c * np.exp(17.67 * (temperature - 273.15 * units.kelvin) /
393
                                    (temperature - 29.65 * units.kelvin))
394
395
396
@exporter.export
397
@check_units('[temperature]', '[dimensionless]')
398
def dewpoint_rh(temperature, rh):
399
    r"""Calculate the ambient dewpoint given air temperature and relative humidity.
400
401
    Parameters
402
    ----------
403
    temperature : `pint.Quantity`
404
        Air temperature
405
    rh : `pint.Quantity`
406
        Relative humidity expressed as a ratio in the range [0, 1]
407
408
    Returns
409
    -------
410
    `pint.Quantity`
411
        The dew point temperature
412
413
    See Also
414
    --------
415
    dewpoint, saturation_vapor_pressure
416
417
    """
418
    return dewpoint(rh * saturation_vapor_pressure(temperature))
419
420
421
@exporter.export
422
@check_units('[pressure]')
423
def dewpoint(e):
424
    r"""Calculate the ambient dewpoint given the vapor pressure.
425
426
    Parameters
427
    ----------
428
    e : `pint.Quantity`
429
        Water vapor partial pressure
430
431
    Returns
432
    -------
433
    `pint.Quantity`
434
        Dew point temperature
435
436
    See Also
437
    --------
438
    dewpoint_rh, saturation_vapor_pressure, vapor_pressure
439
440
    Notes
441
    -----
442
    This function inverts the [Bolton1980]_ formula for saturation vapor
443
    pressure to instead calculate the temperature. This yield the following
444
    formula for dewpoint in degrees Celsius:
445
446
    .. math:: T = \frac{243.5 log(e / 6.112)}{17.67 - log(e / 6.112)}
447
448
    """
449
    val = np.log(e / sat_pressure_0c)
450
    return 0. * units.degC + 243.5 * units.delta_degC * val / (17.67 - val)
451
452
453
@exporter.export
454
@check_units('[pressure]', '[pressure]', '[dimensionless]')
455
def mixing_ratio(part_press, tot_press, molecular_weight_ratio=epsilon):
456
    r"""Calculate the mixing ratio of a gas.
457
458
    This calculates mixing ratio given its partial pressure and the total pressure of
459
    the air. There are no required units for the input arrays, other than that
460
    they have the same units.
461
462
    Parameters
463
    ----------
464
    part_press : `pint.Quantity`
465
        Partial pressure of the constituent gas
466
    tot_press : `pint.Quantity`
467
        Total air pressure
468
    molecular_weight_ratio : `pint.Quantity` or float, optional
469
        The ratio of the molecular weight of the constituent gas to that assumed
470
        for air. Defaults to the ratio for water vapor to dry air
471
        (:math:`\epsilon\approx0.622`).
472
473
    Returns
474
    -------
475
    `pint.Quantity`
476
        The (mass) mixing ratio, dimensionless (e.g. Kg/Kg or g/g)
477
478
    Notes
479
    -----
480
    This function is a straightforward implementation of the equation given in many places,
481
    such as [Hobbs1977]_ pg.73:
482
483
    .. math:: r = \epsilon \frac{e}{p - e}
484
485
    See Also
486
    --------
487
    saturation_mixing_ratio, vapor_pressure
488
489
    """
490
    return molecular_weight_ratio * part_press / (tot_press - part_press)
491
492
493
@exporter.export
494
@check_units('[pressure]', '[temperature]')
495
def saturation_mixing_ratio(tot_press, temperature):
496
    r"""Calculate the saturation mixing ratio of water vapor.
497
498
    This calculation is given total pressure and the temperature. The implementation
499
    uses the formula outlined in [Hobbs1977]_ pg.73.
500
501
    Parameters
502
    ----------
503
    tot_press: `pint.Quantity`
504
        Total atmospheric pressure
505
    temperature: `pint.Quantity`
506
        The temperature
507
508
    Returns
509
    -------
510
    `pint.Quantity`
511
        The saturation mixing ratio, dimensionless
512
513
    """
514
    return mixing_ratio(saturation_vapor_pressure(temperature), tot_press)
515
516
517
@exporter.export
518
@check_units('[pressure]', '[temperature]')
519
def equivalent_potential_temperature(pressure, temperature):
520
    r"""Calculate equivalent potential temperature.
521
522
    This calculation must be given an air parcel's pressure and temperature.
523
    The implementation uses the formula outlined in [Hobbs1977]_ pg.78-79.
524
525
    Parameters
526
    ----------
527
    pressure: `pint.Quantity`
528
        Total atmospheric pressure
529
    temperature: `pint.Quantity`
530
        The temperature
531
532
    Returns
533
    -------
534
    `pint.Quantity`
535
        The corresponding equivalent potential temperature of the parcel
536
537
    Notes
538
    -----
539
    .. math:: \Theta_e = \Theta e^\frac{L_v r_s}{C_{pd} T}
540
541
    """
542
    pottemp = potential_temperature(pressure, temperature)
543
    smixr = saturation_mixing_ratio(pressure, temperature)
544
    return pottemp * np.exp(Lv * smixr / (Cp_d * temperature))
545
546
547
@exporter.export
548
@check_units('[temperature]', '[dimensionless]', '[dimensionless]')
549
def virtual_temperature(temperature, mixing, molecular_weight_ratio=epsilon):
550
    r"""Calculate virtual temperature.
551
552
    This calculation must be given an air parcel's temperature and mixing ratio.
553
    The implementation uses the formula outlined in [Hobbs2006]_ pg.80.
554
555
    Parameters
556
    ----------
557
    temperature: `pint.Quantity`
558
        The temperature
559
    mixing : `pint.Quantity`
560
        dimensionless mass mixing ratio
561
    molecular_weight_ratio : `pint.Quantity` or float, optional
562
        The ratio of the molecular weight of the constituent gas to that assumed
563
        for air. Defaults to the ratio for water vapor to dry air
564
        (:math:`\epsilon\approx0.622`).
565
566
    Returns
567
    -------
568
    `pint.Quantity`
569
        The corresponding virtual temperature of the parcel
570
571
    Notes
572
    -----
573
    .. math:: T_v = T \frac{\text{w} + \epsilon}{\epsilon\,(1 + \text{w})}
574
575
    """
576
    return temperature * ((mixing + molecular_weight_ratio) /
577
                          (molecular_weight_ratio * (1 + mixing)))
578
579
580
@exporter.export
581
@check_units('[pressure]', '[temperature]', '[dimensionless]', '[dimensionless]')
582
def virtual_potential_temperature(pressure, temperature, mixing,
583
                                  molecular_weight_ratio=epsilon):
584
    r"""Calculate virtual potential temperature.
585
586
    This calculation must be given an air parcel's pressure, temperature, and mixing ratio.
587
    The implementation uses the formula outlined in [Markowski2010]_ pg.13.
588
589
    Parameters
590
    ----------
591
    pressure: `pint.Quantity`
592
        Total atmospheric pressure
593
    temperature: `pint.Quantity`
594
        The temperature
595
    mixing : `pint.Quantity`
596
        dimensionless mass mixing ratio
597
    molecular_weight_ratio : `pint.Quantity` or float, optional
598
        The ratio of the molecular weight of the constituent gas to that assumed
599
        for air. Defaults to the ratio for water vapor to dry air
600
        (:math:`\epsilon\approx0.622`).
601
602
    Returns
603
    -------
604
    `pint.Quantity`
605
        The corresponding virtual potential temperature of the parcel
606
607
    Notes
608
    -----
609
    .. math:: \Theta_v = \Theta \frac{\text{w} + \epsilon}{\epsilon\,(1 + \text{w})}
610
611
    """
612
    pottemp = potential_temperature(pressure, temperature)
613
    return virtual_temperature(pottemp, mixing, molecular_weight_ratio)
614
615
616
@exporter.export
617
@check_units('[pressure]', '[temperature]', '[dimensionless]', '[dimensionless]')
618
def density(pressure, temperature, mixing, molecular_weight_ratio=epsilon):
619
    r"""Calculate density.
620
621
    This calculation must be given an air parcel's pressure, temperature, and mixing ratio.
622
    The implementation uses the formula outlined in [Hobbs2006]_ pg.67.
623
624
    Parameters
625
    ----------
626
    temperature: `pint.Quantity`
627
        The temperature
628
    pressure: `pint.Quantity`
629
        Total atmospheric pressure
630
    mixing : `pint.Quantity`
631
        dimensionless mass mixing ratio
632
    molecular_weight_ratio : `pint.Quantity` or float, optional
633
        The ratio of the molecular weight of the constituent gas to that assumed
634
        for air. Defaults to the ratio for water vapor to dry air
635
        (:math:`\epsilon\approx0.622`).
636
637
    Returns
638
    -------
639
    `pint.Quantity`
640
        The corresponding density of the parcel
641
642
    Notes
643
    -----
644
    .. math:: \rho = \frac{p}{R_dT_v}
645
646
    """
647
    virttemp = virtual_temperature(temperature, mixing, molecular_weight_ratio)
648
    return (pressure / (Rd * virttemp)).to(units.kilogram / units.meter ** 3)
649
650
651
@exporter.export
652
@check_units('[temperature]', '[temperature]', '[pressure]')
653
def relative_humidity_wet_psychrometric(dry_bulb_temperature, web_bulb_temperature,
654
                                        pressure, **kwargs):
655
    r"""Calculate the relative humidity with wet bulb and dry bulb temperatures.
656
657
    This uses a psychrometric relationship as outlined in [WMO8-2008]_, with
658
    coefficients from [Fan1987]_.
659
660
    Parameters
661
    ----------
662
    dry_bulb_temperature: `pint.Quantity`
663
        Dry bulb temperature
664
    web_bulb_temperature: `pint.Quantity`
665
        Wet bulb temperature
666
    pressure: `pint.Quantity`
667
        Total atmospheric pressure
668
669
    Returns
670
    -------
671
    `pint.Quantity`
672
        Relative humidity
673
674
    Notes
675
    -----
676
    .. math:: RH = 100 \frac{e}{e_s}
677
678
    * :math:`RH` is relative humidity
679
    * :math:`e` is vapor pressure from the wet psychrometric calculation
680
    * :math:`e_s` is the saturation vapor pressure
681
682
    See Also
683
    --------
684
    psychrometric_vapor_pressure_wet, saturation_vapor_pressure
685
686
    """
687
    return (100 * units.percent * psychrometric_vapor_pressure_wet(dry_bulb_temperature,
688
            web_bulb_temperature, pressure, **kwargs) /
689
            saturation_vapor_pressure(dry_bulb_temperature))
690
691
692
@exporter.export
693
@check_units('[temperature]', '[temperature]', '[pressure]')
694
def psychrometric_vapor_pressure_wet(dry_bulb_temperature, wet_bulb_temperature, pressure,
695
                                     psychrometer_coefficient=6.21e-4 / units.kelvin):
696
    r"""Calculate the vapor pressure with wet bulb and dry bulb temperatures.
697
698
    This uses a psychrometric relationship as outlined in [WMO8-2008]_, with
699
    coefficients from [Fan1987]_.
700
701
    Parameters
702
    ----------
703
    dry_bulb_temperature: `pint.Quantity`
704
        Dry bulb temperature
705
    wet_bulb_temperature: `pint.Quantity`
706
        Wet bulb temperature
707
    pressure: `pint.Quantity`
708
        Total atmospheric pressure
709
    psychrometer_coefficient: `pint.Quantity`
710
        Psychrometer coefficient
711
712
    Returns
713
    -------
714
    `pint.Quantity`
715
        Vapor pressure
716
717
    Notes
718
    -----
719
    .. math:: e' = e'_w(T_w) - A p (T - T_w)
720
721
    * :math:`e'` is vapor pressure
722
    * :math:`e'_w(T_w)` is the saturation vapor pressure with respect to water at temperature
723
      :math:`T_w`
724
    * :math:`p` is the pressure of the wet bulb
725
    * :math:`T` is the temperature of the dry bulb
726
    * :math:`T_w` is the temperature of the wet bulb
727
    * :math:`A` is the psychrometer coefficient
728
729
    Psychrometer coefficient depends on the specific instrument being used and the ventilation
730
    of the instrument.
731
732
    See Also
733
    --------
734
    saturation_vapor_pressure
735
736
    """
737
    return (saturation_vapor_pressure(wet_bulb_temperature) - psychrometer_coefficient *
738
            pressure * (dry_bulb_temperature - wet_bulb_temperature).to('kelvin'))
739
740
741
@exporter.export
742
@check_units('[dimensionless]', '[temperature]', '[pressure]')
743
def relative_humidity_from_mixing_ratio(mixing_ratio, temperature, pressure):
744
    r"""Calculate the relative humidity from mixing ratio, temperature, and pressure.
745
746
    Parameters
747
    ----------
748
    mixing_ratio: `pint.Quantity`
749
        Dimensionless mass mixing ratio
750
    temperature: `pint.Quantity`
751
        Air temperature
752
    pressure: `pint.Quantity`
753
        Total atmospheric pressure
754
755
    Returns
756
    -------
757
    `pint.Quantity`
758
        Relative humidity
759
760
    Notes
761
    -----
762
    Formula from [Hobbs 1977]_ pg. 74.
763
    .. math:: RH = 100 \frac{w}{w_s}
764
765
    * :math:`RH` is relative humidity
766
    * :math:`w` is mxing ratio
767
    * :math:`w_s` is the saturation mixing ratio
768
769
    See Also
770
    --------
771
    saturation_mixing_ratio
772
773
    """
774
    return (100 * units.percent *
775
            mixing_ratio / saturation_mixing_ratio(pressure, temperature))
776
777
778
@exporter.export
779
@check_units('[dimensionless]')
780
def mixing_ratio_from_specific_humidity(specific_humidity):
781
    r"""Calculate the mixing ratio from specific humidity.
782
783
    Parameters
784
    ----------
785
    specific_humidity: `pint.Quantity`
786
        Specific humidity of air
787
788
    Returns
789
    -------
790
    `pint.Quantity`
791
        Mixing ratio
792
793
    Notes
794
    -----
795
    Formula from [Salby1996]_ pg. 118.
796
    .. math:: w = \frac{q}{1-q}
797
798
    * :math:`w` is mxing ratio
799
    * :math:`q` is the specific humidity
800
801
    See Also
802
    --------
803
    mixing_ratio
804
805
    """
806
    return specific_humidity / (1 - specific_humidity)
807
808
809
@exporter.export
810
@check_units('[dimensionless]', '[temperature]', '[pressure]')
811
def relative_humidity_from_specific_humidity(specific_humidity, temperature, pressure):
812
    r"""Calculate the relative humidity from specific humidity, temperature, and pressure.
813
814
    Parameters
815
    ----------
816
    specific_humidity: `pint.Quantity`
817
        Specific humidity of air
818
    temperature: `pint.Quantity`
819
        Air temperature
820
    pressure: `pint.Quantity`
821
        Total atmospheric pressure
822
823
    Returns
824
    -------
825
    `pint.Quantity`
826
        Relative humidity
827
828
    Notes
829
    -----
830
    Formula from [Hobbs 1977]_ pg. 74. and [Salby1996]_ pg. 118.
831
    .. math:: RH = 100 \frac{q}{(1-q)w_s}
832
833
    * :math:`RH` is relative humidity
834
    * :math:`q` is specific humidity
835
    * :math:`w_s` is the saturation mixing ratio
836
837
    See Also
838
    --------
839
    relative_humidity_from_mixing_ratio
840
841
    """
842
    return (100 * units.percent *
843
            mixing_ratio_from_specific_humidity(specific_humidity) /
844
            saturation_mixing_ratio(pressure, temperature))
845
846
847
@exporter.export
848
@check_units('[temperature]', '[pressure]', '[temperature]')
849
def get_isentropic_pressure(isentlevs, lev, tmp, *args, **kwargs):
850
    r"""Perform isentropic analysis with data in isobaric coordinates.
851
852
    Parameters
853
    ----------
854
    lev : array
855
        One-dimensional array of pressure levels
856
    tmp : array
857
        Array of temperature
858
    isentlevs : array
859
        One-dimensional array of desired theta surfaces
860
861
    Returns
862
    -------
863
    isentpr : tuple
864
        Tuple list with pressure at each isentropic level, followed by each additional
865
        argument interpolated to isentopic coordinates.
866
867
868
    Other Parameters
869
    ----------------
870
    args : array, optional
871
        Any additional variables will be interpolated to each isentropic level.
872
873
    tmpk_out : bool, optional
874
        If true, will calculate temperature and output as the last item in the output tuple
875
        list. Defaults to False.
876
    max_iters : int, optional
877
        The maximum number of iterations to use in calculation, defaults to 50.
878
    eps : float, optional
879
        The desired absolute error in the calculated value, defaults to 1e-6.
880
881
    Notes
882
    -----
883
    Assumes input data in 3-dimensions (no time dimension).Input variable arrays must have the
884
    same number of vertical levels as the pressure levels array. Returns calculations for a
885
    single timestep. Presure is calculated on isentropic surfaces by assuming that tempertaure
886
    varies linearly with the natural log of pressure. Linear interpolation is then used in the
887
    vertical to find the pressure at each isentropic level. Interpolation method from
888
    [Hoerling1993]_. Any additional arguments are assumed to vary linearlly with ln(p) and will
889
    be linearlly interpolated to the new isentropic levels.
890
891
    See Also
892
    --------
893
    potential_temperature
894
895
    """
896
    # iteration function to be used later
897
    def _isen_iter(pk1, isentlevs2, ka, a, b, pok):
898
        ekp = np.exp((-ka) * pk1)
899
        t = a * pk1 + b
900
        f = isentlevs2 - pok * t * ekp
901
        fp = pok * ekp * ((ka) * t - a)
902
        return pk1 - (f / fp)
903
904
    # Raise an error if input data doesn't have 3 dimensions.
905
    if tmp.ndim != 3:
906
        raise ValueError(str(tmp.ndim) +
907
                         ' dimensional data input, 3 dimensional data expected')
908
909
    # Change when Python 2.7 no longer supported
910
    # Pull out keyword arguments
911
    tmpk_out = kwargs.pop('tmpk_out', False)
912
    max_iters = kwargs.pop('max_iters', 50)
913
    eps = kwargs.pop('eps', 1e-6)
914
    # check for pressure in decreasing order, if not, flip.
915
    if lev[1] > lev[0]:
916
        level = np.flipud(lev) * lev.units
917
        tmp = np.flipud(tmp) * tmp.units
918
    else:
919
        level = lev
920
    # convert to appropriate units and check for meteorologically sound data,
921
    # if not, raise warning to the user.
922
    tmpk = tmp.to('kelvin')
923
    levels = level.to('hPa')
924
    if levels[0] <= 10 * units.hPa or levels[0] >= 100000 * units.hPa:
925
        warnings.warn('Unexpected value encountered, check units for accuracy')
926
    isentlevels = isentlevs.to('kelvin')
927
928
    # Make the pressure levels and desired isentropic levels the same shape as temperature
929
    levs = np.repeat(np.repeat(levels[:, np.newaxis],
930
                     tmpk.shape[-2], axis=1)[:, :, np.newaxis],
931
                     tmpk.shape[-1], axis=2)
932
    isentlevs2 = np.repeat(np.repeat(isentlevels[:, np.newaxis],
933
                           tmpk.shape[-2], axis=1)[:, :, np.newaxis],
934
                           tmpk.shape[-1], axis=2)
935
936
    # exponent to Poisson's Equation, which is imported above
937
    ka = kappa * 1000 * units('g/kg')
938
939
    # calculate theta for each point
940
    thtalevs = potential_temperature(levs, tmpk)
941
942
    # Find log of pressure to implement assumption of linear temperature dependence on
943
    # ln(p)
944
    pk = np.log(levs.m)
945
946
    # Calculations for interpolation routine
947
    pok = 1000. ** (ka)
948
949
    # index values for each point for the pressure level nearest to the desired theta level
950
    minv = np.apply_along_axis(np.searchsorted, 0, thtalevs.m, isentlevs.m)
951
    ar = np.arange
952
953
    # calculate constants for the interpolation
954
    a = (tmpk.m[minv, ar(tmpk.shape[-2]).reshape(-1, 1), ar(tmpk.shape[-1])] -
955
         tmpk.m[minv - 1, ar(tmpk.shape[-2]).reshape(-1, 1),
956
                ar(tmpk.shape[-1])]) / (pk[minv, ar(tmpk.shape[-2]).reshape(-1, 1),
957
                                           ar(tmpk.shape[-1])] -
958
                                        pk[minv - 1, ar(tmpk.shape[-2]).reshape(-1, 1),
959
                                           ar(tmpk.shape[-1])])
960
961
    b = tmpk.m[minv, ar(tmpk.shape[-2]).reshape(-1, 1),
962
               ar(tmpk.shape[-1])] - a * pk[minv,
963
                                            ar(tmpk.shape[-2]).reshape(-1, 1),
964
                                            ar(tmpk.shape[-1])]
965
966
    # calculate first guess for interpolation
967
    pk1 = (pk[minv, ar(tmpk.shape[-2]).reshape(-1, 1), ar(tmpk.shape[-1])] +
968
           0.5 * (pk[minv + 1, ar(tmpk.shape[-2]).reshape(-1, 1), ar(tmpk.shape[-1])] -
969
                  pk[minv, ar(tmpk.shape[-2]).reshape(-1, 1), ar(tmpk.shape[-1])]))
970
971
    # iterative interpolation useing scipy.optimize.fixed_point and _isen_iter defined above
972
    pk2 = so.fixed_point(_isen_iter, pk1, args=(isentlevs2.m, ka, a, b, pok),
973
                         xtol=eps, maxiter=max_iters)
974
975
    # get back pressure and assign nan for values with pressure greater than 1000 hPa
976
    isentprs = np.exp(pk2)
977
    isentprs[isentprs > 1000.] = np.nan
978
979
    # create list for storing output data
980
    ret = []
981
    ret.append(isentprs * units.hPa)
982
983
    # if tmpk_out = true, calculate temperature and output as last item in list
984
    if tmpk_out:
985
        ret.append((isentlevs2.m / ((1000. / isentprs) ** kappa)) * units.kelvin)
986
987
    # check to see if any additional arguments were given, if so, interpolate to
988
    # new isentropic levels
989
    try:
990
        args[0]
991
    except IndexError:
992
        return ret
993
    else:
994
        # Flip data for pressure in increasing order
995
        pk3 = np.flipud(pk2)
996
997
        # do an interpolation for each additional argument
998
        for i in range(0, len(args)):
999
            c = args[i]
1000
1001
            # check for pressure decreasing with height and invert arguments if needed
1002
            if lev[1] > lev[0]:
1003
                d = c
1004
                llev = np.log(levels.m)
1005
            else:
1006
                d = np.flipud(c)
1007
                llev = np.flipud(np.log(levels.m))
1008
1009
            # interpolate to isentropic levels and add to temporary output array
1010
            arg_out = np.empty((isentlevs.shape[0], tmpk.shape[-2], tmpk.shape[-1]))
1011
            for j in range(0, d.shape[-2]):
1012
                for k in range(0, d.shape[-1]):
1013
                    arg = np.interp(pk3[:, j, k], llev, d[:, j, k])
1014
                    arg_out[:, j, k] = arg
1015
            arg_out_flip = np.flipud(arg_out)
1016
            # assign nan values where surface is undefined
1017
            arg_out_flip[np.isnan(isentprs)] = np.nan
1018
            ret.append(arg_out_flip * args[i].units)
1019
1020
    # output values as a list
1021
    return ret
1022