Skip to content

Commit

Permalink
Add vapour pressure deficit (#2072)
Browse files Browse the repository at this point in the history
### What kind of change does this PR introduce?

* Added `vapor_pressure_deficit` indice and indicator.

### Does this PR introduce a breaking change?

Nope.

### Other information:

Standard name: `water_vapor_saturation_deficit_in_air`

https://en.wikipedia.org/wiki/Vapour-pressure_deficit
  • Loading branch information
Zeitsperre authored Feb 12, 2025
2 parents a912e8a + bb052d9 commit 7a523f3
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 4 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ New indicators
^^^^^^^^^^^^^^
* Added ``xclim.indices.holiday_snow_days`` to compute the number of days with snow on the ground during holidays ("Christmas Days"). (:issue:`2029`, :pull:`2030`).
* Added ``xclim.indices.holiday_snow_and_snowfall_days`` to compute the number of days with snow on the ground and measurable snowfall during holidays ("Perfect Christmas Days"). (:issue:`2029`, :pull:`2030`).
* Added ``xclim.indices.vapor_pressure_deficit`` to compute the vapor pressure deficit from temperature and relative humidity. (:issue:`1917`, :pull:`2072`).

New features and enhancements
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Expand Down
6 changes: 6 additions & 0 deletions src/xclim/data/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,12 @@
"title": "Humidité spécifique calculée à partir de la température du point de rosée et de la pression",
"abstract": "Humidité spécifique calculée à partir de la température du point de rosée et de la pression à l'aide de la pression de vapeur saturante."
},
"VAPOR_PRESSURE_DEFICIT": {
"long_name": "Déficit de pression de vapeur (méthode \"{method}\")",
"description": "Déficit de pression de vapeur calculé à partir de la température et de l'humidité relative à l'aide de la pression de vapeur saturante, laquelle fut calculée en suivant la méthode {method}.",
"title": "Déficit de pression de vapeur calculé à partir de la température et de l'humidité relative",
"abstract": "Déficit de pression de vapeur calculé à partir de la température et de l'humidité relative à l'aide de la pression de vapeur saturante."
},
"FIRST_DAY_TG_BELOW": {
"long_name": "Premier jour de l'année avec une température moyenne quotidienne sous {thresh} durant au moins {window} jours",
"description": "Premier jour de l'année avec une température moyenne quotidienne sous {thresh} durant au moins {window} jours.",
Expand Down
21 changes: 21 additions & 0 deletions src/xclim/indicators/atmos/_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"specific_humidity_from_dewpoint",
"tg",
"universal_thermal_climate_index",
"vapor_pressure_deficit",
"water_budget",
"water_budget_from_tas",
"wind_chill_index",
Expand Down Expand Up @@ -270,6 +271,26 @@ def cfcheck(self, **das) -> None:
compute=indices.specific_humidity_from_dewpoint,
)


vapor_pressure_deficit = Converter(
title="Water vapour pressure deficit",
identifier="vapor_pressure_deficit",
units="Pa",
long_name='Vapour pressure deficit ("{method}" method)',
standard_name="water_vapor_saturation_deficit_in_air",
description=lambda **kws: (
"The difference between the saturation vapour pressure and the actual vapour pressure,"
"calculated from temperature and relative humidity according to the {method} method."
)
+ (
" The computation was done in reference to ice for temperatures below {ice_thresh}."
if kws["ice_thresh"] is not None
else ""
),
abstract="Difference between the saturation vapour pressure and the actual vapour pressure.",
compute=indices.vapor_pressure_deficit,
)

snowfall_approximation = Converter(
title="Snowfall approximation",
identifier="prsn",
Expand Down
45 changes: 44 additions & 1 deletion src/xclim/indices/_conversion.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"tas",
"uas_vas_2_sfcwind",
"universal_thermal_climate_index",
"vapor_pressure_deficit",
"wind_chill_index",
"wind_power_potential",
"wind_profile",
Expand Down Expand Up @@ -356,7 +357,7 @@ def sfcwind_2_uas_vas(
def saturation_vapor_pressure(
tas: xr.DataArray,
ice_thresh: Quantified | None = None,
method: str = "sonntag90", # noqa
method: str = "sonntag90",
) -> xr.DataArray:
"""
Saturation vapour pressure from temperature.
Expand Down Expand Up @@ -492,6 +493,48 @@ def saturation_vapor_pressure(
return e_sat


@declare_units(tas="[temperature]", hurs="[]", ice_thresh="[temperature]")
def vapor_pressure_deficit(
tas: xr.DataArray,
hurs: xr.DataArray,
ice_thresh: Quantified | None = None,
method: str = "sonntag90",
) -> xr.DataArray:
"""
Vapour pressure deficit.
The measure of the moisture deficit of the air.
Parameters
----------
tas : xarray.DataArray
Mean daily temperature.
hurs : xarray.DataArray
Relative humidity.
ice_thresh : Quantified, optional
Threshold temperature under which to switch to equations in reference to ice instead of water.
If None (default) everything is computed with reference to water.
method : {"goffgratch46", "sonntag90", "tetens30", "wmo08", "its90"}
Method used to calculate saturation vapour pressure, see notes of :py:func:`saturation_vapor_pressure`.
Default is "sonntag90".
Returns
-------
xarray.DataArray, [Pa]
Vapour pressure deficit.
See Also
--------
saturation_vapor_pressure : Vapour pressure at saturation.
"""
svp = saturation_vapor_pressure(tas, ice_thresh=ice_thresh, method=method)

vpd = cast(xr.DataArray, (1 - (hurs / 100)) * svp)

vpd = vpd.assign_attrs(units=svp.attrs["units"])
return vpd


@declare_units(
tas="[temperature]",
tdps="[temperature]",
Expand Down
26 changes: 23 additions & 3 deletions tests/test_indices.py
Original file line number Diff line number Diff line change
Expand Up @@ -2837,10 +2837,11 @@ def test_specific_humidity_from_dewpoint(tas_series, ps_series):
@pytest.mark.parametrize(
"ice_thresh,exp0", [(None, [125, 286, 568]), ("0 degC", [103, 260, 563])]
)
@pytest.mark.parametrize("units", ["degC", "degK"])
def test_saturation_vapor_pressure(tas_series, method, ice_thresh, exp0, units):
@pytest.mark.parametrize("temp_units", ["degC", "degK"])
def test_saturation_vapor_pressure(tas_series, method, ice_thresh, exp0, temp_units):
tas = tas_series(np.array([-20, -10, -1, 10, 20, 25, 30, 40, 60]) + K2C)
tas = convert_units_to(tas, units)
tas = convert_units_to(tas, temp_units)

# Expected values obtained with the Sonntag90 method
e_sat_exp = exp0 + [1228, 2339, 3169, 4247, 7385, 19947]

Expand All @@ -2852,6 +2853,24 @@ def test_saturation_vapor_pressure(tas_series, method, ice_thresh, exp0, units):
np.testing.assert_allclose(e_sat, e_sat_exp, atol=0.5, rtol=0.005)


@pytest.mark.parametrize(
"method", ["tetens30", "sonntag90", "goffgratch46", "wmo08", "its90"]
)
def test_vapor_pressure_deficit(tas_series, hurs_series, method):
tas = tas_series(np.array([-1, 10, 20, 25, 30, 40, 60]) + K2C)
hurs = hurs_series(np.array([0, 0.5, 0.8, 0.9, 0.95, 0.99, 1]))

# Expected values obtained with the GoffGratch46 method
svp_exp = [567, 1220, 2317, 3136, 4200, 7300, 19717]

vpd = xci.vapor_pressure_deficit(
tas=tas,
hurs=hurs,
method=method,
)
np.testing.assert_allclose(vpd, svp_exp, atol=0.5, rtol=0.005)


@pytest.mark.parametrize("method", ["tetens30", "sonntag90", "goffgratch46", "wmo08"])
@pytest.mark.parametrize(
"invalid_values,exp0", [("clip", 100), ("mask", np.nan), (None, 188)]
Expand All @@ -2860,6 +2879,7 @@ def test_relative_humidity(
tas_series, hurs_series, huss_series, ps_series, method, invalid_values, exp0
):
tas = tas_series(np.array([-10, -10, 10, 20, 35, 50, 75, 95]) + K2C)

# Expected values obtained with the Sonntag90 method
hurs_exp = hurs_series([exp0, 63.0, 66.0, 34.0, 14.0, 6.0, 1.0, 0.0])
ps = ps_series([101325] * 8)
Expand Down

0 comments on commit 7a523f3

Please sign in to comment.