Functional Weave
Code in Rust

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 L
  • spray_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 tank
  • spray_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.

pub fn spray_mix(area_hectares: f64, water_volume_litres_per_hectare: f64, product_dose_per_hectare: f64, dose_unit: &str, tank_litres: f64) -> SprayMix
area_hectaresfloatarea to treat, taken to 0.0001 ha
water_volume_litres_per_hectarefloatthe application rate: total spray liquid per hectare, taken to 0.1 L
product_dose_per_hectarefloatproduct per hectare from the label, taken to 0.001 (1 mL or 1 g); 0 to 100
dose_unitDoseUnitlitres (a liquid, which displaces water) or kilograms (a solid, which does not)
tank_litresfloatusable tank capacity, taken to 0.1 L
returnsSprayMix

The types it declares, generated into your project

// DoseUnit is a string in Rust, one of: "litres", "kilograms".
// Parameters take it as &str and results hold it as String.

/// What goes into one tank and what it covers.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TankLoad {
    /// area it covers, to 0.0001 ha
    pub area_hectares: f64,
    /// total liquid, to 0.001 L
    pub spray_litres: f64,
    /// litres or kilograms of product, to 0.001
    pub product: f64,
    /// spray less product for a liquid, all of it for a solid
    pub water_litres: f64,
}

/// The whole job.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SprayMix {
    /// full tanks plus a part tank if there is one
    pub tank_fills: i64,
    pub full_tanks: i64,
    /// the recipe for a full tank, even when the job needs less than one
    pub full_tank: TankLoad,
    /// the last, part-filled tank; null when the job is whole tanks
    pub partial_tank: Option<TankLoad>,
    /// to 0.001 L
    pub total_spray_litres: f64,
    /// to 0.001
    pub total_product: f64,
}

Your code names it in one line, in the file that uses it

fune!(agri.spray-mix@^1);  // then call spray_mix(…)
impl/rust.rs · 106 lines · open · raw

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

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::math_round_float::round_float;  ← from math.round-float ^1.0.0 · built alongside by fune

fn scaled(name: &str, value: f64, decimals: i64, low: i64, high: i64) -> i64 {
    if !value.is_finite() || value < low as f64 || value > high as f64 {
        panic!("{} must be a finite number from {} to {}, received {}", name, low, high, value);
    }
    (round_float(value, decimals) * 10f64.powi(decimals as i32)).round() as i64
}

/// 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.
///
/// # Panics
/// Panics on an argument out of range, an unknown dose unit, or a liquid dose
/// that is not less than the water volume.
pub fn spray_mix(
    area_hectares: f64,
    water_volume_litres_per_hectare: f64,
    product_dose_per_hectare: f64,
    dose_unit: &str,
    tank_litres: f64,
) -> SprayMix {
    let area = scaled("areaHectares", area_hectares, 4, 0, 100000);
    let rate = scaled("waterVolumeLitresPerHectare", water_volume_litres_per_hectare, 1, 0, 10000);
    let dose = scaled("productDosePerHectare", product_dose_per_hectare, 3, 0, 100);
    let tank = scaled("tankLitres", tank_litres, 1, 0, 100000);
    if area < 1 {
        panic!("areaHectares must be at least 0.0001 hectares");
    }
    if rate < 1 {
        panic!("waterVolumeLitresPerHectare must be at least 0.1 litres");
    }
    if tank < 1 {
        panic!("tankLitres must be at least 0.1 litres");
    }
    if dose_unit != "litres" && dose_unit != "kilograms" {
        panic!("unknown doseUnit \"{}\": use litres or kilograms", dose_unit);
    }
    // A litre of product per litre of spray would leave no room for water.
    if dose_unit == "litres" && dose >= rate * 100 {
        panic!("productDosePerHectare must be less than waterVolumeLitresPerHectare for a liquid product");
    }

    let spray = area * rate; // litres x 10^5
    let tank_spray = tank * 10000; // litres x 10^5
    let full_tanks = spray / tank_spray;
    let 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.
    let load = |s: i64| -> TankLoad {
        let spray_ml = round_div(s, 100, "half-up");
        let product = round_div(s * dose, rate * 10000, "half-up");
        TankLoad {
            area_hectares: round_div(s, rate, "half-up") as f64 / 10000.0,
            spray_litres: spray_ml as f64 / 1000.0,
            product: product as f64 / 1000.0,
            water_litres: (if dose_unit == "litres" { spray_ml - product } else { spray_ml }) as f64 / 1000.0,
        }
    };
    SprayMix {
        tank_fills: full_tanks + if remainder > 0 { 1 } else { 0 },
        full_tanks,
        full_tank: load(tank_spray),
        partial_tank: if remainder > 0 { Some(load(remainder)) } else { None },
        total_spray_litres: round_div(spray, 100, "half-up") as f64 / 1000.0,
        total_product: round_div(area * dose, 10000, "half-up") as f64 / 1000.0,
    }
}

pub fn tank_load_to_value(t: &TankLoad) -> Value {
    Value::obj(vec![
        ("areaHectares", Value::Float(t.area_hectares)),
        ("sprayLitres", Value::Float(t.spray_litres)),
        ("product", Value::Float(t.product)),
        ("waterLitres", Value::Float(t.water_litres)),
    ])
}

pub fn spray_mix_to_value(m: &SprayMix) -> Value {
    Value::obj(vec![
        ("tankFills", Value::Int(m.tank_fills)),
        ("fullTanks", Value::Int(m.full_tanks)),
        ("fullTank", tank_load_to_value(&m.full_tank)),
        ("partialTank", m.partial_tank.as_ref().map_or(Value::Null, tank_load_to_value)),
        ("totalSprayLitres", Value::Float(m.total_spray_litres)),
        ("totalProduct", Value::Float(m.total_product)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    spray_mix_to_value(&spray_mix(
        args[0].as_f64(),
        args[1].as_f64(),
        args[2].as_f64(),
        args[3].as_str(),
        args[4].as_f64(),
    ))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add agri.spray-mix
Download for Rust agri.spray-mix-1.0.1-rust.fune · 17,433 bytes sha256 04cea010f98e3813431539227f8cd700eedbc8c5b07f6327ac88ad4a8eb94bdb

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./agri.spray-mix-1.0.1-rust.fune, or fetch it from a terminal with fune pull agri.spray-mix@1.0.1:rust.

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.

CaseArgumentsExpected
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
CaseArgumentsExpected
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

PathBytes
README.md3,110
impl/python.py3,177
impl/rust.rs4,283
impl/typescript.ts3,110
vectors.json5,364