"""Synthetic tests for generic temporal alignment (metadata-independent). No GRIB, no real data -- exercises valid_date = init + lead by ACTUAL date, UTC-vs-local-day attribution, alignment by date (never index) with a missing observation, and the partial-first-lead flag. Runnable directly or via pytest. """ import os import sys import numpy as np sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src")) from utils.timealign import ( # noqa: E402 valid_time, local_calendar_day, align_by_date, align_forecast_to_observation, ) def test_valid_time_hours_and_leap(): # non-leap: 27 Feb + 48h -> 1 Mar v = valid_time(np.datetime64("2003-02-27T00:00:00"), 48, "h") assert v == np.datetime64("2003-03-01T00:00:00") # leap: 27 Feb 2004 + 48h -> 29 Feb 2004 (real date arithmetic) v = valid_time(np.datetime64("2004-02-27T00:00:00"), 48, "h") assert v == np.datetime64("2004-02-29T00:00:00") def test_valid_time_array_and_days_unit(): v = valid_time(np.datetime64("2003-12-30"), np.array([0, 1, 2]), "D") assert list(v.astype("datetime64[D]").astype(str)) == ["2003-12-30", "2003-12-31", "2004-01-01"] def test_local_calendar_day_utc_plus_7_boundary(): # 17:00 UTC + 7h = 24:00 -> next local day assert local_calendar_day(np.datetime64("2003-05-01T17:00:00"), 7) == np.datetime64("2003-05-02") # 16:59 UTC + 7h = 23:59 -> same local day assert local_calendar_day(np.datetime64("2003-05-01T16:59:00"), 7) == np.datetime64("2003-05-01") def test_align_by_date_handles_missing_and_order(): obs = np.array(["2003-05-01", "2003-05-02", "2003-05-04"], dtype="datetime64[D]") fcst = np.array(["2003-05-01", "2003-05-02", "2003-05-03", "2003-05-04"], dtype="datetime64[D]") idx, matched = align_by_date(fcst, obs) assert list(idx) == [0, 1, -1, 2] # 05-03 missing -> -1, by DATE not position assert list(matched) == [True, True, False, True] def test_align_forecast_to_observation_end_to_end(): obs = np.array(["2003-12-31", "2004-01-01", "2004-01-02"], dtype="datetime64[D]") # init at 00 UTC, leads 0/24/48 h ; UTC+7 keeps 00Z on the same local day out = align_forecast_to_observation(np.datetime64("2003-12-31T00:00:00"), [0, 24, 48], obs, lead_unit="h", tz_hours=7) assert list(out["local_day"].astype(str)) == ["2003-12-31", "2004-01-01", "2004-01-02"] assert list(out["obs_index"]) == [0, 1, 2] assert out["matched"].all() # a 00Z init under UTC+7 lands at 07:00 local -> the first local day began # 7 h earlier, so LT0's local day is only partially covered. assert out["partial_first"][0] == True def test_partial_first_lead_boundary_vs_midday(): obs = np.array(["2003-05-01", "2003-05-02", "2003-05-03"], dtype="datetime64[D]") # init at 17:00 UTC == 00:00 local (UTC+7): first lead is on the day boundary out = align_forecast_to_observation(np.datetime64("2003-05-01T17:00:00"), [0, 24], obs, lead_unit="h", tz_hours=7) assert out["partial_first"][0] == False assert out["local_day"][0] == np.datetime64("2003-05-02") # init at 12:00 UTC -> mid local day -> partial out2 = align_forecast_to_observation(np.datetime64("2003-05-01T12:00:00"), [0, 24], obs, lead_unit="h", tz_hours=7) assert out2["partial_first"][0] == True def _run_all(): fns = [v for k, v in sorted(globals().items()) if k.startswith("test_")] for fn in fns: fn(); print("PASS", fn.__name__) print(f"\n{len(fns)}/{len(fns)} temporal-alignment tests passed") if __name__ == "__main__": _run_all()