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

get_isentropic_pressure()   F

Complexity

Conditions 13

Size

Total Lines 168

Duplication

Lines 0
Ratio 0 %

Importance

Changes 25
Bugs 0 Features 3
Metric Value
cc 13
dl 0
loc 168
rs 2
c 25
b 0
f 3

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('[temperature]', '[pressure]', '[temperature]')
743
def get_isentropic_pressure(isentlevs, lev, tmp, *args, tmp_out=False, max_iters=50, eps=1e-6):
744
    r"""Compute the pressure on isentropic surfaces from isobaric surfaces.
745
746
    Parameters
747
    ----------
748
    lev : array
749
        One-dimensional array of pressure levels
750
    tmp : array
751
        Array of temperature
752
    isentlevs : array
753
        One-dimensional array of desired theta surfaces
754
755
    Returns
756
    -------
757
    isentpr
758
759
    isentpr : tuple
760
        Tuple list with pressure at each isentropic level, followed by each additional
761
        argument interpolated to isentopic coordinates.
762
763
764
    Other Parameters
765
    ----------------
766
    args : array, optional
767
        Any additional variables will be interpolated to each isentropic level.
768
769
    tmp_out : bool, optional
770
        If true, will calculate temperature and output as the last item in the output tuple
771
        list. Defaults to False.
772
    max_iters : int, optional
773
        The maximum number of iterations to use in calculation, defaults to 50.
774
    eps : float, optional
775
        The desired absolute error in the calculated value, defaults to 1e-6.
776
777
    Notes
778
    -----
779
    Assumes input data in 3-dimensions (no time dimension).Input variable arrays must have the
780
    same number of vertical levels as the pressure levels array. Returns calculations for a
781
    single timestep. Presure is calculated on isentropic surfaces by assuming that tempertaure
782
    varies linearly with the natural log of pressure. Linear interpolation is then used in the
783
    vertical to find the pressure at each isentropic level. Interpolation method from Hoerling
784
    and Sanford 1993. Any additional arguments are assumed to vary linearlly with ln(p) and
785
    will be linearlly interpolated to the new isentropic levels.
786
787
    See Also
788
    --------
789
    potential_temperature
790
791
    """
792
    # iteration function to be used later
793
    def _isen_iter(pk1, isentlevs2, ka, a, b, pok):
794
        ekp = np.exp((-ka) * pk1)
795
        t = a * pk1 + b
796
        f = isentlevs2 - pok * t * ekp
797
        fp = pok * ekp * ((ka) * t - a)
798
        return (pk1 - (f / fp))
799
800
    if tmp.ndim != 3:
801
        raise ValueError(str(tmp.ndim) +
802
                         ' dimensional data input, 3 dimensional data expected')
803
    # check for pressure in decreasing order, if not, flip.
804
    if lev[1] > lev[0]:
805
        lev = np.flip(lev, axis=0)
806
        tmp = np.flip(tmp, axis=-3)
807
808
    # convert to appropriate units and check for meteorologically sound data,
809
    # if not, raise warning to the user.
810
    tmpk = tmp.to('kelvin')
811
    levels = lev.to('hPa')
812
    if levels[0] <= 10 * units.hPa or levels[0] >= 100000 * units.hPa:
813
        warnings.warn('Unexpected value encountered, check units for accuracy')
814
    isentlevels = isentlevs.to('kelvin')
815
816
    # Make the pressure levels and desired isentropic levels the same shape as temperature
817
    levs = np.repeat(np.repeat(levels[:, np.newaxis],
818
                     tmpk.shape[-2], axis=1)[:, :, np.newaxis],
819
                     tmpk.shape[-1], axis=2)
820
    isentlevs2 = np.repeat(np.repeat(isentlevels[:, np.newaxis],
821
                           tmpk.shape[-2], axis=1)[:, :, np.newaxis],
822
                           tmpk.shape[-1], axis=2)
823
824
    # exponent to Poisson's Equation, which is imported above
825
    ka = kappa * 1000 * units('g/kg')
826
827
    # calculate theta for each point
828
    thtalevs = potential_temperature(levs, tmpk)
829
830
    # Find log of pressure to implement assumption of linear temperature dependence on
831
    # ln(p)
832
    pk = np.log(levs.m)
833
834
    # Calculations for interpolation routine
835
    pok = 1000. ** (ka)
836
837
    # index values for each point for the pressure level nearest to the desired theta level
838
    minv = np.apply_along_axis(np.searchsorted, 0, thtalevs.m, isentlevs.m)
839
    ar = np.arange
840
841
    # calculate constants for the interpolation
842
    a = (tmpk.m[minv, ar(tmpk.shape[-2]).reshape(-1, 1), ar(tmpk.shape[-1])] -
843
         tmpk.m[minv - 1, ar(tmpk.shape[-2]).reshape(-1, 1),
844
                ar(tmpk.shape[-1])]) / (pk[minv, ar(tmpk.shape[-2]).reshape(-1, 1),
845
                                           ar(tmpk.shape[-1])] -
846
                                        pk[minv - 1, ar(tmpk.shape[-2]).reshape(-1, 1),
847
                                           ar(tmpk.shape[-1])])
848
849
    b = tmpk.m[minv, ar(tmpk.shape[-2]).reshape(-1, 1),
850
               ar(tmpk.shape[-1])] - a * pk[minv,
851
                                            ar(tmpk.shape[-2]).reshape(-1, 1),
852
                                            ar(tmpk.shape[-1])]
853
854
    # calculate first guess for interpolation
855
    pk1 = (pk[minv, ar(tmpk.shape[-2]).reshape(-1, 1), ar(tmpk.shape[-1])] +
856
           0.5 * (pk[minv + 1, ar(tmpk.shape[-2]).reshape(-1, 1), ar(tmpk.shape[-1])] -
857
                  pk[minv, ar(tmpk.shape[-2]).reshape(-1, 1), ar(tmpk.shape[-1])]))
858
859
    # iterative interpolation useing scipy.optimize.fixed_point and _isen_iter defined above
860
    pk2 = so.fixed_point(_isen_iter, pk1, args=(isentlevs2.m, ka, a, b, pok),
861
                         xtol=eps, maxiter=max_iters, method='del2')
862
863
    # get back pressure and assign nan for values with pressure greater than 1000 hPa
864
    isentprs = np.exp(pk2)
865
    isentprs[isentprs > 1000.] = np.nan
866
867
    # create dictionary for storing output data
868
    dictionary = {}
869
    dictionary['pressure'] = isentprs * units.hPa
870
871
    # check to see if any additional arguments were given, if so, interpolate to
872
    # new isentropic levels
873
    try:
874
        args[0]
875
    except IndexError:
876
        return list(dictionary.items())
877
    else:
878
        # Flip data for pressure in increasing order
879
        pk3 = np.flip(pk2, axis=-3)
880
881
        # do an interpolation for each additional argument
882
        for i in range(0, len(args)):
883
            c = args[i]
884
885
            # check for pressure decreasing with height and invert arguments if needed
886
            if lev[1] > lev[0]:
887
                d = c
888
                llev = np.log(levels.m)
889
            else:
890
                d = np.flip(c, axis=-3)
891
                llev = np.flip(np.log(levels.m), axis=0)
892
893
            # interpolate to isentropic levels and add to temporary output array
894
            arg_out = np.empty((isentlevs.shape[0], tmpk.shape[-2], tmpk.shape[-1]))
895
            for j in range(0, d.shape[-2]):
896
                for k in range(0, d.shape[-1]):
897
                    arg = np.interp(pk3[:, j, k], llev, d[:, j, k])
898
                    arg_out[:, j, k] = np.flip(arg, axis=0)
899
900
            # assign nan values where surface is undefined
901
            arg_out[np.isnan(isentprs)] = np.nan
902
            dictionary['arg{0}'.format(i + 1)] = arg_out * args[i].units
903
    # if tmp_out = true, calculate temperature and output as last item in list
904
    if tmp_out:
905
        dictionary['temperature'] = (isentlevs2.m / ((1000. / isentprs) ** kappa)
906
                                     ) * units.kelvin
907
    # output dictionary values as a tuple list
908
    return list(dictionary.values())
909