agri.spray-mix Unreviewed
Sprayer tank mix: tank fills, and product and water per full and part tank, for an area, water volume and dose.
1.0.1 · published 2026-10-03 by charlie · Anterra
Pinned by 14 tests, run in TypeScript, Python and Rust.
Unreviewed. This capability’s implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified BASIS-qualified agronomist has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
Not professional advice. This capability calculates pesticide figures from published rules. It is a software component for developers, not agronomic advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have a BASIS-qualified agronomist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.
What it does
**Status: needs review by a qualified agronomist/BASIS adviser before it is published; always follow the product label.**
How to fill a sprayer for a job: how many tank fills, and how much product and water go into each full tank and into the last, part-filled one. 24 ha at a water volume of 200 L/ha with a product at 2.5 L/ha and a 1000 L tank is 5 fills: four full tanks of 12.5 L product and 987.5 L water, each covering 5 ha, and a last tank of 10 L product and 790 L water for the remaining 4 ha.
For example
spray_mix(24, 200, 2.5, litres, 1,000)→ tank fills 5, full tanks 4, full tank …, partial tank …, total spray litres 4,800, total product 60 24 ha at 200 L/ha and 2.5 L/ha with a 1000 L tank: four full tanks and 800 Lspray_mix(10, 100, 1, litres, 500)→ tank fills 2, full tanks 2, full tank …, partial tank —, total spray litres 1,000, total product 10 a job of exactly two tanks has no part tankspray_mix(2, 200, 1.5, litres, 1,000)→ tank fills 1, full tanks 0, full tank …, partial tank …, total spray litres 400, total product 3 less than one tank: the full-tank recipe is still given
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
def spray_mix(area_hectares: float, water_volume_litres_per_hectare: float, product_dose_per_hectare: float, dose_unit: DoseUnit, tank_litres: float) -> SprayMix
| area_hectares | float | area to treat, taken to 0.0001 ha |
| water_volume_litres_per_hectare | float | the application rate: total spray liquid per hectare, taken to 0.1 L |
| product_dose_per_hectare | float | product per hectare from the label, taken to 0.001 (1 mL or 1 g); 0 to 100 |
| dose_unit | DoseUnit | litres (a liquid, which displaces water) or kilograms (a solid, which does not) |
| tank_litres | float | usable tank capacity, taken to 0.1 L |
| returns | SprayMix |
The types it declares, generated into your project
DoseUnit = Literal["litres", "kilograms"]
@dataclass(frozen=True)
class TankLoad:
"""What goes into one tank and what it covers."""
#: area it covers, to 0.0001 ha
area_hectares: float
#: total liquid, to 0.001 L
spray_litres: float
#: litres or kilograms of product, to 0.001
product: float
#: spray less product for a liquid, all of it for a solid
water_litres: float
@dataclass(frozen=True)
class SprayMix:
"""The whole job."""
#: full tanks plus a part tank if there is one
tank_fills: int
full_tanks: int
#: the recipe for a full tank, even when the job needs less than one
full_tank: TankLoad
#: the last, part-filled tank; null when the job is whole tanks
partial_tank: Optional[TankLoad]
#: to 0.001 L
total_spray_litres: float
#: to 0.001
total_product: float
Your code names it in one line, in the file that uses it
from fune.agri.spray_mix import spray_mix # agri.spray-mix@^1
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,
)Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Python package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add agri.spray-mix
The manifest, vectors and README with only the Python implementation. Install it without the registry with fune add ./agri.spray-mix-1.0.1-python.fune, or fetch it from a terminal with fune pull agri.spray-mix@1.0.1:python.
The whole function, every language, is one file too: agri.spray-mix-1.0.1.fune, 23,979 bytes, sha256 865c75eea05b131780d875c1023f8d889054971285cf0ac22b48355d174d2bda. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
# fune: before agri.spray-mix
after — your function gets the result and the arguments, and returns the final result.
# fune: after agri.spray-mix
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
# fune: replace math.round-div in agri.spray-mix
# fune: replace math.round-float in agri.spray-mix
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show agri.spray-mix --steps.
# fune: step agri.spray-mix after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| 24 ha at 200 L/ha and 2.5 L/ha with a 1000 L tank: four full tanks and 800 L | 24, 200, 2.5, litres, 1,000 | → | tank fills 5, full tanks 4, full tank …, partial tank …, total spray litres 4,800, total product 60 |
| a job of exactly two tanks has no part tank | 10, 100, 1, litres, 500 | → | tank fills 2, full tanks 2, full tank …, partial tank —, total spray litres 1,000, total product 10 |
| less than one tank: the full-tank recipe is still given | 2, 200, 1.5, litres, 1,000 | → | tank fills 1, full tanks 0, full tank …, partial tank …, total spray litres 400, total product 3 |
| a solid product in kg does not displace water | 12, 150, 0.25, kilograms, 600 | → | tank fills 3, full tanks 3, full tank …, partial tank —, total spray litres 1,800, total product 3 |
| awkward numbers: the full and part tank products add up to the job's | 7.3, 120, 0.67, litres, 800 | → | tank fills 2, full tanks 1, full tank …, partial tank …, total spray litres 876, total product 4.891 |
| 129.2 ha at 250 L/ha is exactly 19 tanks of 1700 L, not 18 and a sliver | 129.2, 250, 1, litres, 1,700 | → | tank fills 19, full tanks 19, full tank …, partial tank —, total spray litres 32,300, total product 129.2 |
| water only (no product) | 5, 100, 0, litres, 300 | → | tank fills 2, full tanks 1, full tank …, partial tank …, total spray litres 500, total product 0 |
| the smallest area, one square metre | 0, 200, 1, litres, 1,000 | → | tank fills 1, full tanks 0, full tank …, partial tank …, total spray litres 0.02, total product 0 |
| zero area is an error | 0, 200, 1, litres, 1,000 | → | error: areaHectares must be at least 0.0001 hectares |
| a negative dose is an error | 10, 200, -1, litres, 1,000 | → | error: productDosePerHectare must be a finite number from 0 to 100 |
Show the other 4 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| zero water volume is an error | 10, 0, 1, litres, 1,000 | → | error: waterVolumeLitresPerHectare must be at least 0.1 litres |
| zero tank is an error | 10, 200, 1, litres, 0 | → | error: tankLitres must be at least 0.1 litres |
| a liquid dose as large as the water volume is an error | 10, 50, 50, litres, 1,000 | → | error: productDosePerHectare must be less than waterVolumeLitresPerHectare for a liquid product |
| an unknown dose unit is an error | 10, 200, 1, gallons, 1,000 | → | error: unknown doseUnit "gallons" |
More from the author
## Terms
- **Water volume** (the application rate) is the total spray liquid applied per hectare, product included, as sprayer calibration sets it. - **Dose** is the label rate of product per hectare, in litres for a liquid or kilograms for a solid (`doseUnit`). - **Water** in a tank is the spray volume less the product for a liquid, since the product takes up room; for a solid it is the whole spray volume (the powder or granules dissolve or disperse into it).
## Exactness
Inputs are taken to 0.0001 ha, 0.1 L/ha, 0.001 L or kg per ha and 0.1 L (`math.round-float`) and held as integers, so the job's total spray volume is exact and the full-tank count is a whole-number division. In floating point, 129.2 ha x 250 L/ha is 32299.999999999996, which makes 18 full 1700 L tanks and a sliver; this gives exactly 19.
Each load's area, spray, product and water are worked out from its own spray volume and rounded half up (area to 0.0001 ha, liquids and product to 0.001). Water is the rounded spray less the rounded product, so each load adds up.
## What it does not do
It does not check the dose, water volume, number of applications or buffer zones against the label, account for rinsate, the tank's unusable residue (give the usable capacity) or a tank-mix order, or convert units.
## Limits
Area more than 0 and at most 100,000 ha; water volume 0.1 to 10,000 L/ha; dose 0 to 100 per ha, and for a liquid less than the water volume; tank 0.1 to 100,000 L.
## Before you rely on this
**Not professional advice.** This capability calculates pesticide figures from published rules. It is a software component for developers, not agronomic advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed above, and have a BASIS-qualified agronomist review how you use it, before anyone relies on the output. Provided "as is" under its licence, without warranty.
**Unreviewed.** This capability's implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified BASIS-qualified agronomist has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.
1.0.1 marks it unreviewed. The code and the tests are unchanged.
Files
| Path | Bytes |
|---|---|
| README.md | 3,110 |
| impl/python.py | 3,177 |
| impl/rust.rs | 4,283 |
| impl/typescript.ts | 3,110 |
| vectors.json | 5,364 |