Skip to content

Commit a69ff0a

Browse files
committed
Control zero humidity in thermo computations
1 parent d6175a8 commit a69ff0a

File tree

2 files changed

+210
-50
lines changed

2 files changed

+210
-50
lines changed

src/earthkit/meteo/thermo/array/thermo.py

Lines changed: 79 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@
1313
from earthkit.meteo import constants
1414

1515

16+
def _valid_number(x):
17+
return x is not None and not np.isnan(x)
18+
19+
1620
def celsius_to_kelvin(t):
1721
"""Converts temperature values from Celsius to Kelvin.
1822
@@ -259,9 +263,21 @@ def compute_slope(self, t, phase):
259263
elif phase == "ice":
260264
return self._es_ice_slope(t)
261265

262-
def t_from_es(self, es):
263-
v = np.log(es / self.c1)
264-
return (v * self.c4w - self.c3w * self.t0) / (v - self.c3w)
266+
def t_from_es(self, es, eps=1e-8, out=None):
267+
def _comp(x):
268+
x = x / self.c1
269+
x = np.log(x)
270+
return (x * self.c4w - self.c3w * self.t0) / (x - self.c3w)
271+
272+
if out is not None:
273+
v = np.asarray(es)
274+
z_mask = v < eps
275+
v_mask = ~z_mask
276+
v[v_mask] = _comp(v[v_mask])
277+
v[z_mask] = out
278+
return v
279+
else:
280+
return _comp(es)
265281

266282
def _es_water(self, t):
267283
return self.c1 * np.exp(self.c3w * (t - self.t0) / (t - self.c4w))
@@ -556,13 +572,21 @@ def saturation_specific_humidity_slope(t, p, es=None, es_slope=None, phase="mixe
556572
return constants.epsilon * es_slope * p / v
557573

558574

559-
def temperature_from_saturation_vapour_pressure(es):
560-
r"""Computes the temperature from saturation vapour pressure.
575+
def temperature_from_saturation_vapour_pressure(es, eps=1e-8, out=None):
576+
r"""Compute the temperature from saturation vapour pressure.
561577
562578
Parameters
563579
----------
564580
es: ndarray
565581
:func:`saturation_vapour_pressure` (Pa)
582+
eps: number
583+
If ``out`` is not None, return ``out`` when ``es`` < ``eps``. If out
584+
is None, ``eps`` is ignored and return np.nan for ``es`` values very
585+
close to zero.
586+
out: number or None
587+
If not None, return ``out`` when ``es`` < ``eps``. If None, ``eps`` is
588+
ignored and return np.nan for ``es`` values very close to zero.
589+
566590
567591
Returns
568592
-------
@@ -575,7 +599,7 @@ def temperature_from_saturation_vapour_pressure(es):
575599
phase ``es`` was computed to.
576600
577601
"""
578-
return _EsComp().t_from_es(es)
602+
return _EsComp().t_from_es(es, eps=eps, out=out)
579603

580604

581605
def relative_humidity_from_dewpoint(t, td):
@@ -750,20 +774,27 @@ def specific_humidity_from_relative_humidity(t, r, p):
750774
return specific_humidity_from_vapour_pressure(e, p)
751775

752776

753-
def dewpoint_from_relative_humidity(t, r):
754-
r"""Computes the dewpoint temperature from relative humidity.
777+
def dewpoint_from_relative_humidity(t, r, eps=1e-8, out=None):
778+
r"""Compute the dewpoint temperature from relative humidity.
755779
756780
Parameters
757781
----------
758782
t: ndarray
759783
Temperature (K)
760784
r: ndarray
761785
Relative humidity (%)
786+
eps: number
787+
If ``out`` is not None, return ``out`` when ``r`` < ``eps``.
788+
If out is None, ``eps`` is ignored and return np.nan for ``q``
789+
values very close to zero.
790+
out: number or None
791+
If not None, return ``out`` when ``r`` < ``eps``. If None, ``eps`` is
792+
ignored and return np.nan for ``r`` values very close to zero.
762793
763794
Returns
764795
-------
765796
ndarray
766-
Dewpoint (K)
797+
Dewpoint temperature (K)
767798
768799
769800
The computation starts with determining the the saturation vapour pressure over
@@ -782,11 +813,23 @@ def dewpoint_from_relative_humidity(t, r):
782813
equations used in :func:`saturation_vapour_pressure`.
783814
784815
"""
816+
# by masking upfront we avoid RuntimeWarning when calling log() in
817+
# the computation of td when r is very small
818+
if out is not None:
819+
r = np.asarray(r)
820+
mask = r < eps
821+
r[mask] = np.nan
822+
785823
es = saturation_vapour_pressure(t, phase="water") * r / 100.0
786-
return temperature_from_saturation_vapour_pressure(es)
824+
td = temperature_from_saturation_vapour_pressure(es)
787825

826+
if out is not None and not np.isnan(out):
827+
td[mask] = out
788828

789-
def dewpoint_from_specific_humidity(q, p):
829+
return td
830+
831+
832+
def dewpoint_from_specific_humidity(q, p, eps=1e-8, out=None):
790833
r"""Computes the dewpoint temperature from specific humidity.
791834
792835
Parameters
@@ -795,11 +838,20 @@ def dewpoint_from_specific_humidity(q, p):
795838
Specific humidity (kg/kg)
796839
p: ndarray
797840
Pressure (Pa)
841+
eps: number
842+
As an intermediate result the saturation vapour pressure
843+
(``es``) is computed (see details below). If ``out`` is not
844+
None, return ``out`` when ``es`` < ``eps``. If out is None,
845+
``eps`` is ignored and return np.nan for ``es`` values very
846+
close to zero.
847+
out: number or None
848+
If not None, return ``out`` when ``es`` < ``eps``. If None, ``eps`` is
849+
ignored and return np.nan for ``es`` values very close to zero.
798850
799851
Returns
800852
-------
801853
ndarray
802-
Dewpoint (K)
854+
Dewpoint temperature (K)
803855
804856
805857
The computation starts with determining the the saturation vapour pressure over
@@ -819,7 +871,20 @@ def dewpoint_from_specific_humidity(q, p):
819871
used in :func:`saturation_vapour_pressure`.
820872
821873
"""
822-
return temperature_from_saturation_vapour_pressure(vapour_pressure_from_specific_humidity(q, p))
874+
# by masking upfront we avoid RuntimeWarning when calling log() in
875+
# the computation of td when q is very small
876+
if out is not None:
877+
q = np.asarray(q)
878+
mask = q < eps
879+
q[mask] = np.nan
880+
881+
es = vapour_pressure_from_specific_humidity(q, p)
882+
td = temperature_from_saturation_vapour_pressure(es)
883+
884+
if out is not None and not np.isnan(out):
885+
td[mask] = out
886+
887+
return td
823888

824889

825890
def virtual_temperature(t, q):
@@ -828,7 +893,7 @@ def virtual_temperature(t, q):
828893
Parameters
829894
----------
830895
t: number or ndarray
831-
Temperature (K)
896+
Temperature (K)s
832897
q: number or ndarray
833898
Specific humidity (kg/kg)
834899

tests/thermo/test_thermo_array.py

Lines changed: 131 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import os
1111

1212
import numpy as np
13+
import pytest
1314

1415
from earthkit.meteo import thermo
1516

@@ -320,7 +321,7 @@ def test_saturation_specific_humidity_slope():
320321
np.testing.assert_allclose(svp, v_ref[i])
321322

322323

323-
def test_temperature_from_saturation_vapour_pressure():
324+
def test_temperature_from_saturation_vapour_pressure_1():
324325
ref_file = "sat_vp.csv"
325326
d = np.genfromtxt(
326327
data_file(ref_file),
@@ -329,7 +330,32 @@ def test_temperature_from_saturation_vapour_pressure():
329330
)
330331

331332
t = thermo.array.temperature_from_saturation_vapour_pressure(d["water"])
332-
np.testing.assert_allclose(t, d["t"])
333+
assert np.allclose(t, d["t"], equal_nan=True)
334+
335+
336+
@pytest.mark.parametrize(
337+
"es,kwargs,expected_values",
338+
[
339+
(4.2, {}, 219.7796336743947),
340+
([4.2, 0, 0.001, np.nan], {"eps": 1e-2, "out": 100}, [219.7796336743947, 100, 100, np.nan]),
341+
([4.2, 0, 0.001, np.nan], {"eps": 1e-2, "out": np.nan}, [219.7796336743947, np.nan, np.nan, np.nan]),
342+
(0, {}, np.nan),
343+
(0.001, {"eps": 1e-2, "out": 100}, 100.0),
344+
(0.001, {"eps": 1e-2, "out": np.nan}, np.nan),
345+
],
346+
)
347+
def test_temperature_from_saturation_vapour_pressure_2(es, kwargs, expected_values):
348+
349+
multi = isinstance(es, list)
350+
if multi:
351+
es = np.array(es)
352+
expected_values = np.array(expected_values)
353+
354+
t = thermo.array.temperature_from_saturation_vapour_pressure(es, **kwargs)
355+
if multi:
356+
np.testing.assert_allclose(t, expected_values, equal_nan=True)
357+
else:
358+
assert np.isclose(t, expected_values, equal_nan=True)
333359

334360

335361
def test_relative_humidity_from_dewpoint():
@@ -421,43 +447,112 @@ def test_specific_humidity_from_relative_humidity():
421447
np.testing.assert_allclose(q, v_ref)
422448

423449

424-
def test_dewpoint_from_relative_humidity():
425-
t = thermo.array.celsius_to_kelvin(np.array([20.0, 20, 0, 35, 5, -15, 25]))
426-
r = np.array(
427-
[
428-
100.0000000000,
429-
52.5224541378,
430-
46.8714823296,
431-
84.5391163313,
432-
21.9244774232,
433-
46.1081101229,
434-
15.4779832381,
435-
]
436-
)
437-
v_ref = thermo.array.celsius_to_kelvin(np.array([20.0, 10, -10, 32, -15, -24, -3]))
450+
@pytest.mark.parametrize(
451+
"t,r,kwargs,expected_values",
452+
[
453+
(
454+
[20.0, 20, 0, 35, 5, -15, 25, 25],
455+
[
456+
100.0000000000,
457+
52.5224541378,
458+
46.8714823296,
459+
84.5391163313,
460+
21.9244774232,
461+
46.1081101229,
462+
15.4779832381,
463+
0,
464+
],
465+
{},
466+
[20.0, 10, -10, 32, -15, -24, -3, np.nan],
467+
),
468+
(
469+
[20.0, 20.0, 20.0],
470+
[
471+
52.5224541378,
472+
0.0,
473+
0.000001,
474+
],
475+
{"eps": 1e-3, "out": thermo.array.celsius_to_kelvin(100)},
476+
[10, 100, 100],
477+
),
478+
(
479+
[20.0, 20.0, 20.0],
480+
[
481+
52.5224541378,
482+
0.0,
483+
0.000001,
484+
],
485+
{"eps": 1e-3, "out": np.nan},
486+
[10, np.nan, np.nan],
487+
),
488+
],
489+
)
490+
def test_dewpoint_from_relative_humidity(t, r, kwargs, expected_values):
438491
# reference was tested with an online relhum calculator at:
439492
# https://bmcnoldy.rsmas.miami.edu/Humidity.html
440-
td = thermo.array.dewpoint_from_relative_humidity(t, r)
441-
np.testing.assert_allclose(td, v_ref)
442-
443493

444-
def test_dewpoint_from_specific_humidity():
445-
p = np.array([967.5085, 936.3775, 872.248, 756.1647, 649.157, 422.4207]) * 100
446-
q = np.array(
447-
[
448-
0.0169461501,
449-
0.0155840075,
450-
0.0134912382,
451-
0.0083409720,
452-
0.0057268584,
453-
0.0025150791,
454-
]
455-
)
456-
v_ref = thermo.array.celsius_to_kelvin(
457-
np.array([21.78907, 19.90885, 16.50236, 7.104064, -0.3548709, -16.37916])
458-
)
459-
td = thermo.array.dewpoint_from_specific_humidity(q, p)
460-
np.testing.assert_allclose(td, v_ref)
494+
multi = isinstance(t, list)
495+
if multi:
496+
t = np.array(t)
497+
r = np.array(r)
498+
expected_values = np.array(expected_values)
499+
500+
t = thermo.array.celsius_to_kelvin(t)
501+
v_ref = thermo.array.celsius_to_kelvin(expected_values)
502+
503+
td = thermo.array.dewpoint_from_relative_humidity(t, r, **kwargs)
504+
if multi:
505+
assert np.allclose(td, v_ref, equal_nan=True)
506+
else:
507+
assert np.isclose(td, v_ref, equal_nan=True)
508+
509+
510+
@pytest.mark.parametrize(
511+
"q,p,kwargs,expected_values",
512+
[
513+
(
514+
[0.0169461501, 0.0155840075, 0.0134912382, 0.0083409720, 0.0057268584, 0.0025150791, 0],
515+
[967.5085, 936.3775, 872.248, 756.1647, 649.157, 422.4207, 422.4207],
516+
{},
517+
[21.78907, 19.90885, 16.50236, 7.104064, -0.3548709, -16.37916, np.nan],
518+
),
519+
(
520+
[
521+
0.0169461501,
522+
0.0,
523+
0.000001,
524+
],
525+
[967.5085, 967.5085, 967.5085],
526+
{"eps": 1e-3, "out": thermo.array.celsius_to_kelvin(100)},
527+
[21.78907, 100, 100],
528+
),
529+
(
530+
[
531+
0.0169461501,
532+
0.0,
533+
0.000001,
534+
],
535+
[967.5085, 967.5085, 967.5085],
536+
{"eps": 1e-3, "out": np.nan},
537+
[21.78907, np.nan, np.nan],
538+
),
539+
],
540+
)
541+
def test_dewpoint_from_specific_humidity(q, p, kwargs, expected_values):
542+
multi = isinstance(q, list)
543+
if multi:
544+
q = np.array(q)
545+
p = np.array(p)
546+
expected_values = np.array(expected_values)
547+
548+
p = p * 100.0
549+
v_ref = thermo.array.celsius_to_kelvin(expected_values)
550+
551+
td = thermo.array.dewpoint_from_specific_humidity(q, p, **kwargs)
552+
if multi:
553+
assert np.allclose(td, v_ref, equal_nan=True)
554+
else:
555+
assert np.isclose(td, v_ref, equal_nan=True)
461556

462557

463558
def test_virtual_temperature():

0 commit comments

Comments
 (0)