import numpy as np import pandas as pd import pytest from damage_engine.sources import derived # real hours of station 566 (2026-10-19): hourly mean T (°C), mean RH (%), 3A's TBulbMed (°C) STATION_566 = [(13.3, 99, 13.2), (13.8, 90, 12.8), (15.6, 82, 13.8), (15.8, 78, 13.6), (15.9, 76, 13.4), (15.2, 78, 13.0)] @pytest.mark.parametrize("t, rh, tbulb_3a", STATION_566) def test_formula_reproduces_3a_tbulbmed(t, rh, tbulb_3a): # same formula as 3A (document section 4): equal to 3A's value at its 0.1 °C rounding assert abs(float(derived.wet_bulb_c(t, rh)) - tbulb_3a) <= 0.05 def test_saturated_air_wet_bulb_equals_air_temperature(): for t in (-5.0, 0.0, 10.0): assert abs(float(derived.wet_bulb_c(t, 100)) - t) < 1e-6 def test_wet_bulb_below_air_when_dry_and_ice_branch_below_zero(): assert float(derived.wet_bulb_c(20, 50)) < 20 assert float(derived.wet_bulb_c(-3, 80)) < -3 # T <= 0 uses the ice constants: the result is continuous enough across 0 °C assert abs(float(derived.wet_bulb_c(0.0, 90)) - float(derived.wet_bulb_c(0.01, 90))) < 0.2 def test_dew_point_at_saturation_is_air_temperature(): assert abs(float(derived.dew_point_c(12.0, 100)) - 12.0) < 1e-6 def test_add_wet_bulb_pairs_tmin_rhmax_and_tmax_rhmin(): df = pd.DataFrame({derived.T_MIN: [14.6], derived.T_MEAN: [15.6], derived.T_MAX: [17.5], derived.RH_MIN: [70.0], derived.RH_MEAN: [82.0], derived.RH_MAX: [90.0]}) out = derived.add_wet_bulb(df) assert out[derived.WET_BULB_MIN][0] == round(float(derived.wet_bulb_c(14.6, 90)), 2) assert out[derived.WET_BULB_MAX][0] == round(float(derived.wet_bulb_c(17.5, 70)), 2) assert out[derived.WET_BULB_MIN][0] <= out[derived.WET_BULB_MAX][0] assert derived.WET_BULB_MIN not in df # input not changed def test_add_wet_bulb_falls_back_to_means_and_never_fails(): df = pd.DataFrame({derived.T_MEAN: [5.0, None], derived.RH_MEAN: [90.0, 80.0]}) out = derived.add_wet_bulb(df) # no min/max columns at all expected = round(float(derived.wet_bulb_c(5.0, 90)), 2) assert out[derived.WET_BULB_MIN][0] == expected == out[derived.WET_BULB_MAX][0] assert np.isnan(out[derived.WET_BULB_MIN][1]) # no temperature -> NaN assert derived.add_wet_bulb(pd.DataFrame({"x": [1]}))[derived.WET_BULB_MIN].isna().all() def test_frost_point_below_zero_uses_ice_constants(): assert abs(float(derived.dew_point_c(0, 100))) < 1e-9 assert float(derived.dew_point_c(-3, 40)) < -10 # T <= 0 °C: e = RH * 6.1 * 10^(9.5 T / (T + 265.5)), inverted with the same constants e = 0.95 * 6.1 * 10 ** (9.5 * -3 / (-3 + 265.5)) x = np.log10(e / 6.1) assert abs(float(derived.dew_point_c(-3, 95)) - 265.5 * x / (9.5 - x)) < 1e-9