Functional Weave
Code in Rust

agri.growing-degree-days@1.0.0

impl/python.py

2,370 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

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-div ^1.0.0 · built alongside by fune
from .math_round_float import round_float  ← from math.round-float ^1.0.0 · built alongside by fune


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)