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

_lcl_iter()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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