Source code for stilt.transport.hysplit.failures

"""Why a HYSPLIT run failed, read from the messages in its log and its WARNING file."""

from __future__ import annotations

from enum import StrEnum


[docs] class FailureReason(StrEnum): """ Why a HYSPLIT run failed. The driver reads it from HYSPLIT's log and ``WARNING`` file when the run ends, or knows it (a timeout, no particle output, its exit status), and raises a :class:`~stilt.exceptions.SimulationError` with it as ``reason``. The worker records it with the simulation. """ MET_COVERAGE = "MET_COVERAGE" VARYING_MET_INTERVAL = "VARYING_MET_INTERVAL" NO_PARTICLE_DATA = "NO_PARTICLE_DATA" FORTRAN_RUNTIME_ERROR = "FORTRAN_RUNTIME_ERROR" TIMEOUT = "TIMEOUT" PARTICLE_LIMIT = "PARTICLE_LIMIT" EXIT_STATUS = "EXIT_STATUS"
#: Phrases HYSPLIT (or the driver) writes to stilt.log or HYSPLIT's WARNING #: file, mapped to their FailureReason. The four ``metpos`` warnings say a #: particle inside the met's grid reached a time no met file holds: the met #: ran out. HYSPLIT writes only the first such warning of a run. It writes #: ``off spatial domain of all grids`` when every particle left the met's #: domain, which is not a failure, and also when the met ran out, so that #: line tells nothing; ``check_met_stop`` in the driver reads where the #: particles stopped instead. FAILURE_PHRASES: dict[str, FailureReason] = { "start point not within (x,y,t) any data file": FailureReason.MET_COVERAGE, "start time after end of meteorology data": FailureReason.MET_COVERAGE, "no more meteorology": FailureReason.MET_COVERAGE, "too much time between files": FailureReason.MET_COVERAGE, "no meteo data at current time": FailureReason.MET_COVERAGE, "no data at current time on any grid": FailureReason.MET_COVERAGE, "meteorological data time interval varies": FailureReason.VARYING_MET_INTERVAL, "PARTICLE_STILT.DAT does not contain any trajectory data": FailureReason.NO_PARTICLE_DATA, "Fortran runtime error": FailureReason.FORTRAN_RUNTIME_ERROR, # hycs_std stops at the first varsiwant name it does not know, before it # writes a particle; the config refuses such names, so this is for a # custom build that reads fewer. "Weird variable in VARSIWANT": FailureReason.NO_PARTICLE_DATA, } #: What HYSPLIT writes to its WARNING file when it reaches ``MAXPAR`` while #: releasing. Not in :data:`FAILURE_PHRASES`: a release from one location #: writes it too (HYSPLIT's rounding asks for one particle over ``numpar`` #: and keeps ``numpar``), which is harmless. From several locations it means #: the last ones got fewer particles or none (``PARTICLE_LIMIT``). PARTICLE_LIMIT_PHRASE = "exceeding puff limit" def failure_in(text: str) -> tuple[FailureReason, str] | None: """ Return the first known failure in HYSPLIT's messages, as its reason and the line that says it. Phrases are tried in the order of :data:`FAILURE_PHRASES`. ``None`` when the log holds none of them. """ for phrase, reason in FAILURE_PHRASES.items(): if phrase in text: line = next(line for line in text.splitlines() if phrase in line) return reason, line.strip() return None