Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add vapour pressure deficit #2072

Merged
merged 10 commits into from
Feb 12, 2025
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
Loading