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

isentropic_interpolation()   F

Complexity

Conditions 13

Size

Total Lines 179

Duplication

Lines 0
Ratio 0 %

Importance

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