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