Functional Weave
Code in Python

agri.fertiliser-application@1.0.0

impl/python.py

3,204 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

from .agri_fertiliser_application_types import FertiliserPlan, NutrientAnalysis, NutrientSupply, NutrientTargets
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

NUTRIENTS = ("nitrogen", "phosphate", "potash")


def _tenths_of_percent(name: str, value: float) -> int:
    if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value < 0 or value > 100:
        raise ValueError("analysis %s must be a finite percentage from 0 to 100, received %r" % (name, value))
    return int(round(round_float(float(value), 1) * 10))


def fertiliser_application(analysis: NutrientAnalysis, targets: NutrientTargets, area_hectares: float) -> FertiliserPlan:
    """Product rate for a fertiliser, set by the limiting nutrient.

    For each targeted nutrient the product needed is target / analysis; the
    rate is the smallest of those, so no nutrient is applied beyond its target,
    and the others show as a shortfall. The arithmetic is exact in tenths (of a
    percent and of a kilogram) and the rate is rounded once, half up.
    """
    a = [_tenths_of_percent(n, getattr(analysis, n)) for n in NUTRIENTS]
    if sum(a) > 1000:
        raise ValueError("analysis must not total more than 100%")
    t: List[int] = []
    for n in NUTRIENTS:
        v = getattr(targets, n)
        if isinstance(v, bool) or not isinstance(v, int) or v < 0 or v > 1000:
            raise ValueError("target %s must be a whole number of kg/ha from 0 to 1000, received %r" % (n, v))
        t.append(v)
    if (
        isinstance(area_hectares, bool)
        or not isinstance(area_hectares, (int, float))
        or not math.isfinite(area_hectares)
        or area_hectares <= 0
        or area_hectares > 10000
    ):
        raise ValueError("areaHectares must be a finite number greater than 0 and at most 10000, received %r" % (area_hectares,))
    area = int(round(round_float(float(area_hectares), 4) * 10000))
    if area < 1:
        raise ValueError("areaHectares must be at least 0.0001 hectares, received %r" % (area_hectares,))
    if sum(t) == 0:
        raise ValueError("at least one target must be above zero")

    # Least product: target_i / a_i smallest, compared by cross-multiplying.
    limit = -1
    for i in range(3):
        if t[i] == 0 or a[i] == 0:
            continue
        if limit < 0 or t[i] * a[limit] < t[limit] * a[i]:
            limit = i
    if limit < 0:
        raise ValueError("the product contains none of the targeted nutrients")

    # kg/ha in tenths = target x 100 / (a / 10) x 10.
    rate = round_div(t[limit] * 10000, a[limit], "half-up")
    s = [round_div(rate * ai, 1000, "half-up") for ai in a]
    return FertiliserPlan(
        limiting_nutrient=NUTRIENTS[limit],
        product_kg_per_hectare=rate / 10,
        total_product_kg=round_div(rate * area, 10000, "half-up") / 10,
        supplied=NutrientSupply(nitrogen=s[0] / 10, phosphate=s[1] / 10, potash=s[2] / 10),
        balance=NutrientSupply(
            nitrogen=(s[0] - t[0] * 10) / 10,
            phosphate=(s[1] - t[1] * 10) / 10,
            potash=(s[2] - t[2] * 10) / 10,
        ),
    )