Functional Weave
Code in Python

agri.spray-mix@1.0.1

impl/python.py

3,177 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 .agri_spray_mix_types import DoseUnit, SprayMix, TankLoad
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 _scaled(name: str, value: float, decimals: int, low: int, high: int) -> int:
    if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value < low or value > high:
        raise ValueError("%s must be a finite number from %d to %d, received %r" % (name, low, high, value))
    return int(round(round_float(float(value), decimals) * 10**decimals))


def spray_mix(
    area_hectares: float,
    water_volume_litres_per_hectare: float,
    product_dose_per_hectare: float,
    dose_unit: DoseUnit,
    tank_litres: float,
) -> SprayMix:
    """Tank mix for a sprayer.

    Area (0.0001 ha), water volume (0.1 L/ha), dose (0.001/ha) and tank
    (0.1 L) are held as integers, so the job's spray volume is exact and the
    count of full tanks cannot be thrown by a float: 129.2 ha at 250 L/ha is
    exactly 19 tanks of 1700 L, where 129.2 x 250 in floating point is
    32299.999999999996 and makes it 18 and a sliver.
    """
    area = _scaled("areaHectares", area_hectares, 4, 0, 100000)
    rate = _scaled("waterVolumeLitresPerHectare", water_volume_litres_per_hectare, 1, 0, 10000)
    dose = _scaled("productDosePerHectare", product_dose_per_hectare, 3, 0, 100)
    tank = _scaled("tankLitres", tank_litres, 1, 0, 100000)
    if area < 1:
        raise ValueError("areaHectares must be at least 0.0001 hectares")
    if rate < 1:
        raise ValueError("waterVolumeLitresPerHectare must be at least 0.1 litres")
    if tank < 1:
        raise ValueError("tankLitres must be at least 0.1 litres")
    if dose_unit not in ("litres", "kilograms"):
        raise ValueError('unknown doseUnit "%s": use litres or kilograms' % (dose_unit,))
    # A litre of product per litre of spray would leave no room for water.
    if dose_unit == "litres" and dose >= rate * 100:
        raise ValueError("productDosePerHectare must be less than waterVolumeLitresPerHectare for a liquid product")

    spray = area * rate  # litres x 10^5
    tank_spray = tank * 10000  # litres x 10^5
    full_tanks = spray // tank_spray
    remainder = spray - full_tanks * tank_spray

    # A load from its spray volume in litres x 10^5: area = spray / rate,
    # product = spray x dose / (rate x 10^4) in thousandths.
    def load(s: int) -> TankLoad:
        spray_ml = round_div(s, 100, "half-up")
        product = round_div(s * dose, rate * 10000, "half-up")
        return TankLoad(
            area_hectares=round_div(s, rate, "half-up") / 10000,
            spray_litres=spray_ml / 1000,
            product=product / 1000,
            water_litres=(spray_ml - product if dose_unit == "litres" else spray_ml) / 1000,
        )

    return SprayMix(
        tank_fills=full_tanks + (1 if remainder > 0 else 0),
        full_tanks=full_tanks,
        full_tank=load(tank_spray),
        partial_tank=load(remainder) if remainder > 0 else None,
        total_spray_litres=round_div(spray, 100, "half-up") / 1000,
        total_product=round_div(area * dose, 10000, "half-up") / 1000,
    )