Skip to content

Commit 2f8dc02

Browse files
authored
Merge pull request #3121 from alexander-lakocy/feat/gdi
Add Galvez-Davison Index calculation
2 parents e0e24d5 + 83436e8 commit 2f8dc02

5 files changed

Lines changed: 334 additions & 1 deletion

File tree

docs/_templates/overrides/metpy.calc.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ Soundings
7979
cross_totals
8080
downdraft_cape
8181
el
82+
galvez_davison_index
8283
k_index
8384
lcl
8485
lfc

docs/api/references.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ References
8686
Services and Supporting Research, 2003.
8787
`FCM-R19-2003 <../_static/FCM-R19-2003-WindchillReport.pdf>`_, 75 pp.
8888
89+
.. [Galvez2015] Galvez, J. M. and Michel Davison, 2015. “The Gálvez-Davison Index for Tropical
90+
Convection.” `GDI_Manuscript_V20161021
91+
<https://www.wpc.ncep.noaa.gov/international/gdi/GDI_Manuscript_V20161021.pdf>`_.
92+
8993
.. [Galway1956] Galway, J. G., 1956: The Lifted Index as a Predictor of Latent Instability.
9094
*American Meteorology Society*,
9195
doi:`10.1175/1520-0477-37.10.528

examples/calculations/Sounding_Calculations.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ def effective_layer(p, t, td, h, height_layer=False):
8888
sped = df['speed'].values * units.knot
8989
height = df['height'].values * units.meter
9090

91+
###########################################
92+
# Compute needed variables from our data file and attach units
93+
relhum = mpcalc.relative_humidity_from_dewpoint(T, Td)
94+
mixrat = mpcalc.mixing_ratio_from_relative_humidity(p, T, relhum)
95+
9196
###########################################
9297
# Compute the wind components
9398
u, v = mpcalc.wind_components(sped, wdir)
@@ -96,6 +101,7 @@ def effective_layer(p, t, td, h, height_layer=False):
96101
# Compute common sounding index parameters
97102
ctotals = mpcalc.cross_totals(p, T, Td)
98103
kindex = mpcalc.k_index(p, T, Td)
104+
gdi = mpcalc.galvez_davison_index(p, T, mixrat, p[0])
99105
showalter = mpcalc.showalter_index(p, T, Td)
100106
total_totals = mpcalc.total_totals_index(p, T, Td)
101107
vert_totals = mpcalc.vertical_totals(p, T)

src/metpy/calc/thermo.py

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4497,6 +4497,181 @@ def k_index(pressure, temperature, dewpoint, vertical_dim=0):
44974497
return ((t850 - t500) + td850 - (t700 - td700)).to(units.degC)
44984498

44994499

4500+
@exporter.export
4501+
@add_vertical_dim_from_xarray
4502+
@preprocess_and_wrap(broadcast=('pressure', 'temperature', 'mixing_ratio'))
4503+
@check_units('[pressure]', '[temperature]', '[dimensionless]', '[pressure]')
4504+
def galvez_davison_index(pressure, temperature, mixing_ratio, surface_pressure,
4505+
vertical_dim=0):
4506+
"""
4507+
Calculate GDI from the pressure, temperature, mixing ratio, and surface pressure.
4508+
4509+
Calculation of the GDI relies on temperatures and mixing ratios at 950,
4510+
850, 700, and 500 hPa. These four levels define three layers: A) Boundary,
4511+
B) Trade Wind Inversion (TWI), C) Mid-Troposphere.
4512+
4513+
GDI formula derived from [Galvez2015]_:
4514+
4515+
.. math:: GDI = CBI + MWI + II + TC
4516+
4517+
where:
4518+
4519+
* :math:`CBI` is the Column Buoyancy Index
4520+
* :math:`MWI` is the Mid-tropospheric Warming Index
4521+
* :math:`II` is the Inversion Index
4522+
* :math:`TC` is the Terrain Correction [optional]
4523+
4524+
.. list-table:: GDI Values & Corresponding Convective Regimes
4525+
:widths: 15 75
4526+
:header-rows: 1
4527+
4528+
* - GDI Value
4529+
- Expected Convective Regime
4530+
* - >=45
4531+
- Scattered to widespread thunderstorms likely.
4532+
* - 35 to 45
4533+
- Scattered thunderstorms and/or scattered to widespread rain showers.
4534+
* - 25 to 35
4535+
- Isolated to scattered thunderstorms and/or scattered showers.
4536+
* - 15 to 25
4537+
- Isolated thunderstorms and/or isolated to scattered showers.
4538+
* - 5 to 10
4539+
- Isolated to scattered showers.
4540+
* - <5
4541+
- Strong TWI likely, light rain possible.
4542+
4543+
Parameters
4544+
----------
4545+
pressure : `pint.Quantity`
4546+
Pressure level(s)
4547+
4548+
temperature : `pint.Quantity`
4549+
Temperature corresponding to pressure
4550+
4551+
mixing_ratio : `pint.Quantity`
4552+
Mixing ratio values corresponding to pressure
4553+
4554+
surface_pressure : `pint.Quantity`
4555+
Pressure of the surface.
4556+
4557+
vertical_dim : int, optional
4558+
The axis corresponding to vertical, defaults to 0. Automatically determined from
4559+
xarray DataArray arguments.
4560+
4561+
Returns
4562+
-------
4563+
`pint.Quantity`
4564+
GDI Index
4565+
4566+
Examples
4567+
--------
4568+
>>> from metpy.calc import mixing_ratio_from_relative_humidity
4569+
>>> from metpy.units import units
4570+
>>> # pressure
4571+
>>> p = [1008., 1000., 950., 900., 850., 800., 750., 700., 650., 600.,
4572+
... 550., 500., 450., 400., 350., 300., 250., 200.,
4573+
... 175., 150., 125., 100., 80., 70., 60., 50.,
4574+
... 40., 30., 25., 20.] * units.hPa
4575+
>>> # temperature
4576+
>>> T = [29.3, 28.1, 23.5, 20.9, 18.4, 15.9, 13.1, 10.1, 6.7, 3.1,
4577+
... -0.5, -4.5, -9.0, -14.8, -21.5, -29.7, -40.0, -52.4,
4578+
... -59.2, -66.5, -74.1, -78.5, -76.0, -71.6, -66.7, -61.3,
4579+
... -56.3, -51.7, -50.7, -47.5] * units.degC
4580+
>>> # relative humidity
4581+
>>> rh = [.85, .65, .36, .39, .82, .72, .75, .86, .65, .22, .52,
4582+
... .66, .64, .20, .05, .75, .76, .45, .25, .48, .76, .88,
4583+
... .56, .88, .39, .67, .15, .04, .94, .35] * units.dimensionless
4584+
>>> # calculate mixing ratio
4585+
>>> mixrat = mixing_ratio_from_relative_humidity(p, T, rh)
4586+
>>> galvez_davison_index(p, T, mixrat, p[0])
4587+
<Quantity(-8.78797532, 'dimensionless')>
4588+
"""
4589+
if np.any(np.max(pressure, axis=vertical_dim) < 950 * units.hectopascal):
4590+
indices_without_950 = np.where(
4591+
np.max(pressure, axis=vertical_dim) < 950 * units.hectopascal
4592+
)
4593+
raise ValueError(
4594+
f'Data not provided for 950hPa or higher pressure. '
4595+
f'GDI requires 950hPa temperature and dewpoint data, '
4596+
f'see referenced paper section 3.d. in docstring for discussion of'
4597+
f' extrapolating sounding data below terrain surface in high-'
4598+
f'elevation regions.\nIndices without a 950hPa or higher datapoint'
4599+
f':\n{indices_without_950}'
4600+
f'\nMax provided pressures:'
4601+
f'\n{np.max(pressure, axis=0)[indices_without_950]}'
4602+
)
4603+
4604+
potential_temp = potential_temperature(pressure, temperature)
4605+
4606+
# Interpolate to appropriate level with appropriate units
4607+
(
4608+
(t950, t850, t700, t500),
4609+
(r950, r850, r700, r500),
4610+
(th950, th850, th700, th500)
4611+
) = interpolate_1d(
4612+
units.Quantity([950, 850, 700, 500], 'hPa'),
4613+
pressure, temperature.to('K'), mixing_ratio, potential_temp.to('K'),
4614+
axis=vertical_dim,
4615+
)
4616+
4617+
# L_v definition preserved from referenced paper
4618+
# Value differs heavily from metpy.constants.Lv in tropical context
4619+
# and using MetPy value affects resulting GDI
4620+
l_0 = units.Quantity(2.69e6, 'J/kg')
4621+
4622+
# Calculate adjusted equivalent potential temperatures
4623+
alpha = units.Quantity(-10, 'K')
4624+
eptp_a = th950 * np.exp(l_0 * r950 / (mpconsts.Cp_d * t850))
4625+
eptp_b = ((th850 + th700) / 2
4626+
* np.exp(l_0 * (r850 + r700) / 2 / (mpconsts.Cp_d * t850)) + alpha)
4627+
eptp_c = th500 * np.exp(l_0 * r500 / (mpconsts.Cp_d * t850)) + alpha
4628+
4629+
# Calculate Column Buoyanci Index (CBI)
4630+
# Apply threshold to low and mid levels
4631+
beta = units.Quantity(303, 'K')
4632+
l_e = eptp_a - beta
4633+
m_e = eptp_c - beta
4634+
4635+
# Gamma unit - likely a typo from the paper, should be units of K^(-2) to
4636+
# result in dimensionless CBI
4637+
gamma = units.Quantity(6.5e-2, '1/K^2')
4638+
4639+
column_buoyancy_index = np.atleast_1d(gamma * l_e * m_e)
4640+
column_buoyancy_index[l_e <= 0] = 0
4641+
4642+
# Calculate Mid-tropospheric Warming Index (MWI)
4643+
# Apply threshold to 500-hPa temperature
4644+
tau = units.Quantity(263.15, 'K')
4645+
t_diff = t500 - tau
4646+
4647+
mu = units.Quantity(-7, '1/K') # Empirical adjustment
4648+
mid_tropospheric_warming_index = np.atleast_1d(mu * t_diff)
4649+
mid_tropospheric_warming_index[t_diff <= 0] = 0
4650+
4651+
# Calculate Inversion Index (II)
4652+
s = t950 - t700
4653+
d = eptp_b - eptp_a
4654+
inv_sum = s + d
4655+
4656+
sigma = units.Quantity(1.5, '1/K') # Empirical scaling constant
4657+
inversion_index = np.atleast_1d(sigma * inv_sum)
4658+
inversion_index[inv_sum >= 0] = 0
4659+
4660+
# Calculate Terrain Correction
4661+
terrain_correction = 18 - 9000 / (surface_pressure.m_as('hPa') - 500)
4662+
4663+
# Calculate G.D.I.
4664+
gdi = (column_buoyancy_index
4665+
+ mid_tropospheric_warming_index
4666+
+ inversion_index
4667+
+ terrain_correction)
4668+
4669+
if gdi.size == 1:
4670+
return gdi[0]
4671+
else:
4672+
return gdi
4673+
4674+
45004675
@exporter.export
45014676
@add_vertical_dim_from_xarray
45024677
@preprocess_and_wrap(

tests/calc/test_thermo.py

Lines changed: 148 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
vertical_velocity_pressure, virtual_potential_temperature,
3838
virtual_temperature, virtual_temperature_from_dewpoint,
3939
wet_bulb_potential_temperature, wet_bulb_temperature)
40-
from metpy.calc.thermo import _find_append_zero_crossings
40+
from metpy.calc.thermo import _find_append_zero_crossings, galvez_davison_index
4141
from metpy.testing import (assert_almost_equal, assert_array_almost_equal, assert_nan,
4242
version_check)
4343
from metpy.units import is_quantity, masked_array, units
@@ -2309,6 +2309,50 @@ def index_xarray_data():
23092309
coords={'isobaric': pressure, 'time': ['2020-01-01T00:00Z']})
23102310

23112311

2312+
@pytest.fixture()
2313+
def index_xarray_data_expanded():
2314+
"""Create expanded data for testing that index calculations work with xarray data.
2315+
2316+
Specifically for Galvez Davison Index calculation, which requires 950hPa pressure
2317+
"""
2318+
pressure = xr.DataArray(
2319+
[950., 850., 700., 500.], dims=('isobaric',), attrs={'units': 'hPa'}
2320+
)
2321+
temp = xr.DataArray([[[[306., 305., 304.], [303., 302., 301.]],
2322+
[[296., 295., 294.], [293., 292., 291.]],
2323+
[[286., 285., 284.], [283., 282., 281.]],
2324+
[[276., 275., 274.], [273., 272., 271.]]]] * units.K,
2325+
dims=('time', 'isobaric', 'y', 'x'))
2326+
2327+
profile = xr.DataArray([[[[299., 298., 297.], [296., 295., 294.]],
2328+
[[289., 288., 287.], [286., 285., 284.]],
2329+
[[279., 278., 277.], [276., 275., 274.]],
2330+
[[269., 268., 267.], [266., 265., 264.]]]] * units.K,
2331+
dims=('time', 'isobaric', 'y', 'x'))
2332+
2333+
dewp = xr.DataArray([[[[304., 303., 302.], [301., 300., 299.]],
2334+
[[294., 293., 292.], [291., 290., 289.]],
2335+
[[284., 283., 282.], [281., 280., 279.]],
2336+
[[274., 273., 272.], [271., 270., 269.]]]] * units.K,
2337+
dims=('time', 'isobaric', 'y', 'x'))
2338+
2339+
dirw = xr.DataArray([[[[135., 135., 135.], [135., 135., 135.]],
2340+
[[180., 180., 180.], [180., 180., 180.]],
2341+
[[225., 225., 225.], [225., 225., 225.]],
2342+
[[270., 270., 270.], [270., 270., 270.]]]] * units.degree,
2343+
dims=('time', 'isobaric', 'y', 'x'))
2344+
2345+
speed = xr.DataArray([[[[15., 15., 15.], [15., 15., 15.]],
2346+
[[20., 20., 20.], [20., 20., 20.]],
2347+
[[25., 25., 25.], [25., 25., 25.]],
2348+
[[50., 50., 50.], [50., 50., 50.]]]] * units.knots,
2349+
dims=('time', 'isobaric', 'y', 'x'))
2350+
2351+
return xr.Dataset({'temperature': temp, 'profile': profile, 'dewpoint': dewp,
2352+
'wind_direction': dirw, 'wind_speed': speed},
2353+
coords={'isobaric': pressure, 'time': ['2023-01-01T00:00Z']})
2354+
2355+
23122356
def test_lifted_index():
23132357
"""Test the Lifted Index calculation."""
23142358
pressure = np.array([1014., 1000., 997., 981.2, 947.4, 925., 914.9, 911.,
@@ -2398,6 +2442,109 @@ def test_k_index_xarray(index_xarray_data):
23982442
np.array([[[312., 311., 310.], [309., 308., 307.]]]) * units.K)
23992443

24002444

2445+
def test_gdi():
2446+
"""Test the Galvez Davison Index calculation."""
2447+
pressure = np.array([1014., 1000., 997., 981.2, 947.4, 925., 914.9, 911.,
2448+
902., 883., 850., 822.3, 816., 807., 793.2, 770.,
2449+
765.1, 753., 737.5, 737., 713., 700., 688., 685.,
2450+
680., 666., 659.8, 653., 643., 634., 615., 611.8,
2451+
566.2, 516., 500., 487., 484.2, 481., 475., 460.,
2452+
400.]) * units.hPa
2453+
temperature = np.array([24.2, 24.2, 24., 23.1, 21., 19.6, 18.7, 18.4,
2454+
19.2, 19.4, 17.2, 15.3, 14.8, 14.4, 13.4, 11.6,
2455+
11.1, 10., 8.8, 8.8, 8.2, 7., 5.6, 5.6,
2456+
5.6, 4.4, 3.8, 3.2, 3., 3.2, 1.8, 1.5,
2457+
-3.4, -9.3, -11.3, -13.1, -13.1, -13.1, -13.7, -15.1,
2458+
-23.5]) * units.degC
2459+
dewpoint = np.array([23.2, 23.1, 22.8, 22., 20.2, 19., 17.6, 17.,
2460+
16.8, 15.5, 14., 11.7, 11.2, 8.4, 7., 4.6,
2461+
5., 6., 4.2, 4.1, -1.8, -2., -1.4, -0.4,
2462+
-3.4, -5.6, -4.3, -2.8, -7., -25.8, -31.2, -31.4,
2463+
-34.1, -37.3, -32.3, -34.1, -37.3, -41.1, -37.7, -58.1,
2464+
-57.5]) * units.degC
2465+
2466+
relative_humidity = relative_humidity_from_dewpoint(temperature, dewpoint)
2467+
mixrat = mixing_ratio_from_relative_humidity(pressure, temperature, relative_humidity)
2468+
gdi = galvez_davison_index(pressure, temperature, mixrat, pressure[0])
2469+
2470+
# Compare with value from hand calculation
2471+
assert_almost_equal(gdi, 6.635, decimal=1)
2472+
2473+
2474+
def test_gdi_xarray(index_xarray_data_expanded):
2475+
"""Test the GDI calculation with a grid of xarray data."""
2476+
pressure = index_xarray_data_expanded.isobaric
2477+
temperature = index_xarray_data_expanded.temperature
2478+
dewpoint = index_xarray_data_expanded.dewpoint
2479+
mixing_ratio = mixing_ratio_from_relative_humidity(
2480+
pressure, temperature, relative_humidity_from_dewpoint(temperature, dewpoint))
2481+
2482+
result = galvez_davison_index(
2483+
pressure,
2484+
temperature,
2485+
mixing_ratio,
2486+
pressure[0]
2487+
)
2488+
2489+
assert_array_almost_equal(
2490+
result,
2491+
np.array([[[189.5890429, 157.4307982, 129.9739099],
2492+
[106.6763526, 87.0637477, 70.7202505]]])
2493+
)
2494+
2495+
2496+
def test_gdi_arrays(index_xarray_data_expanded):
2497+
"""Test GDI on 3-D Quantity arrays with an array of surface pressure."""
2498+
ds = index_xarray_data_expanded.isel(time=0).squeeze()
2499+
pressure = ds.isobaric.metpy.unit_array[:, None, None]
2500+
temperature = ds.temperature.metpy.unit_array
2501+
dewpoint = ds.dewpoint.metpy.unit_array
2502+
mixing_ratio = mixing_ratio_from_relative_humidity(
2503+
pressure, temperature, relative_humidity_from_dewpoint(temperature, dewpoint))
2504+
surface_pressure = units.Quantity(
2505+
np.broadcast_to(pressure.m, temperature.shape), pressure.units)[0]
2506+
2507+
result = galvez_davison_index(pressure, temperature, mixing_ratio, surface_pressure)
2508+
2509+
assert_array_almost_equal(
2510+
result,
2511+
np.array([[189.5890429, 157.4307982, 129.9739099],
2512+
[106.6763526, 87.0637477, 70.7202505]])
2513+
)
2514+
2515+
2516+
def test_gdi_profile(index_xarray_data_expanded):
2517+
"""Test GDI calculation on an individual profile."""
2518+
ds = index_xarray_data_expanded.isel(time=0, y=0, x=0)
2519+
pressure = ds.isobaric.metpy.unit_array
2520+
temperature = ds.temperature.metpy.unit_array
2521+
dewpoint = ds.dewpoint.metpy.unit_array
2522+
mixing_ratio = mixing_ratio_from_relative_humidity(
2523+
pressure, temperature, relative_humidity_from_dewpoint(temperature, dewpoint))
2524+
2525+
assert_almost_equal(galvez_davison_index(pressure, temperature, mixing_ratio, pressure[0]),
2526+
189.5890429, 4)
2527+
2528+
2529+
def test_gdi_no_950_raises_valueerror(index_xarray_data):
2530+
"""GDI requires a 950hPa or higher measurement.
2531+
2532+
Ensure error is raised if this data is not provided.
2533+
"""
2534+
with pytest.raises(ValueError):
2535+
pressure = index_xarray_data.isobaric
2536+
temperature = index_xarray_data.temperature
2537+
dewpoint = index_xarray_data.dewpoint
2538+
relative_humidity = relative_humidity_from_dewpoint(temperature, dewpoint)
2539+
mixrat = mixing_ratio_from_relative_humidity(pressure, temperature, relative_humidity)
2540+
galvez_davison_index(
2541+
pressure,
2542+
temperature,
2543+
mixrat,
2544+
pressure[0]
2545+
)
2546+
2547+
24012548
def test_gradient_richardson_number():
24022549
"""Test gradient Richardson number calculation."""
24032550
theta = units('K') * np.asarray([254.5, 258.3, 262.2])

0 commit comments

Comments
 (0)