import math from typing import List, Optional, Sequence from .agri_growing_degree_days_types import DailyTemperature, DegreeDayMethod, DegreeDays from .math_round_div import round_div from .math_round_float import round_float def _tenths(value: float, message: str) -> int: if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value < -100 or value > 100: raise ValueError(message) return int(round(round_float(float(value), 1) * 10)) def growing_degree_days( days: Sequence[DailyTemperature], base_celsius: float, upper_celsius: Optional[float], method: DegreeDayMethod, ) -> DegreeDays: """Growing degree days, McMaster and Wilhelm (1997), in tenths of a degree. Each day is (max + min) / 2 - base with the temperatures taken to 0.1 C, so it is an exact multiple of 0.05 and the total is summed exactly and rounded once. Rounding each day to 0.1 first and adding drifts: three days of 0.05 are 0.15 (0.2 to one place), not 0.3. """ base = _tenths(base_celsius, "baseCelsius must be a finite number from -100 to 100") upper: Optional[int] = None if upper_celsius is not None: upper = _tenths(upper_celsius, "upperCelsius must be a finite number from -100 to 100") if upper <= base: raise ValueError("upperCelsius must be above baseCelsius") if method not in ("average", "clamped"): raise ValueError('unknown method "%s": use average or clamped' % (method,)) def cap(t: int) -> int: return upper if upper is not None and t > upper else t daily: List[float] = [] total = 0 for i, day in enumerate(days): message = "day %d: temperatures must be finite numbers from -100 to 100 °C" % (i + 1,) lo = _tenths(day.min, message) hi = _tenths(day.max, message) if lo > hi: raise ValueError("day %d: min must not be above max" % (i + 1,)) lo = cap(lo) hi = cap(hi) if method == "clamped": twice = max(lo, base) + max(hi, base) - 2 * base else: twice = max(lo + hi - 2 * base, 0) # twice is 2 x (mean - base) in tenths, so the day is twice / 20 degrees. daily.append(twice / 20) total += twice return DegreeDays(total=round_div(total, 2, "half-up") / 10, daily=daily)