import math from .agri_spray_mix_types import DoseUnit, SprayMix, TankLoad from .math_round_div import round_div from .math_round_float import round_float 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, )