agri.seed-rate
Seed rate in kg/ha from target plants per m², thousand-grain weight and expected establishment.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 17 tests, run in TypeScript, Python and Rust.
What it does
How much seed to drill, in kg/ha, to end up with a target number of established plants per square metre:
seed rate (kg/ha) = target plants/m² x thousand-grain weight (g) / expected establishment (%)
For example
seed_rate(300, 50, 80%)→ kg per hectare 187.5, seeds per square metre 375 300 plants/m² at TGW 50 g and 80% establishment is 187.5 kg/haseed_rate(225, 40, 76.5%)→ kg per hectare 117.6, seeds per square metre 294.1 FAR spring barley example: 225 plants, TGW 40 g, 90% germination x 85% emergence (76.5%) is about 118 kg/haseed_rate(260, 48, 70%)→ kg per hectare 178.3, seeds per square metre 371.4 AHDB wheat benchmark: 260 plants/m² at 70% establishment, TGW 48 g
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 seed_rate(target_plants_per_square_metre: i64, thousand_grain_weight_grams: f64, establishment_basis_points: i64) -> SeedRate
| target_plants_per_square_metre | int | established plants wanted, 1 to 100000 |
| thousand_grain_weight_grams | float | weight of 1000 seeds in grams, taken to 0.01 g; from the seed bag |
| establishment_basis_points | int | share of seeds sown expected to become plants, 7000 = 70%; 1 to 10000 |
| returns | SeedRate |
The type it declares, generated into your project
/// What to set the drill to.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct SeedRate {
/// plants x TGW / establishment %, to 0.1 kg, half up
pub kg_per_hectare: f64,
/// seeds to sow per m², to 0.1, half up
pub seeds_per_square_metre: f64,
}
Your code names it in one line, in the file that uses it
fune!(agri.seed-rate@^1); // then call seed_rate(…)
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
/// Seed rate (kg/ha) = target plants/m² x TGW (g) / expected establishment (%).
///
/// The TGW is taken to the centigram and the establishment is in basis points,
/// so kg/ha is exactly plants x centigrams / basis points and is rounded once.
/// Doing it in floats gets 100 x 1.15 / 100 = 1.1499999999999999, which rounds
/// to 1.1 kg/ha instead of 1.2.
///
/// # Panics
/// Panics when an argument is out of range.
pub fn seed_rate(target_plants_per_square_metre: i64, thousand_grain_weight_grams: f64, establishment_basis_points: i64) -> SeedRate {
let plants = target_plants_per_square_metre;
if !(1..=100000).contains(&plants) {
panic!("targetPlantsPerSquareMetre must be a whole number from 1 to 100000, received {}", plants);
}
let tgw = thousand_grain_weight_grams;
if !tgw.is_finite() || tgw <= 0.0 || tgw > 1000.0 {
panic!("thousandGrainWeightGrams must be a finite number greater than 0 and at most 1000, received {}", tgw);
}
let centigrams = (round_float(tgw, 2) * 100.0).round() as i64;
if centigrams < 1 {
panic!("thousandGrainWeightGrams must be at least 0.01 grams, received {}", tgw);
}
let bp = establishment_basis_points;
if !(1..=10000).contains(&bp) {
panic!("establishmentBasisPoints must be a whole number from 1 to 10000, received {}", bp);
}
SeedRate {
kg_per_hectare: round_div(plants * centigrams * 10, bp, "half-up") as f64 / 10.0,
seeds_per_square_metre: round_div(plants * 100000, bp, "half-up") as f64 / 10.0,
}
}
pub fn seed_rate_to_value(s: &SeedRate) -> Value {
Value::obj(vec![
("kgPerHectare", Value::Float(s.kg_per_hectare)),
("seedsPerSquareMetre", Value::Float(s.seeds_per_square_metre)),
])
}
fn whole(v: &Value, message: &str) -> i64 {
match v {
Value::Int(i) => *i,
_ => panic!("{}, received {:?}", message, v),
}
}
pub fn fune_vector(args: &[Value]) -> Value {
let plants = whole(&args[0], "targetPlantsPerSquareMetre must be a whole number from 1 to 100000");
let bp = whole(&args[2], "establishmentBasisPoints must be a whole number from 1 to 10000");
seed_rate_to_value(&seed_rate(plants, args[1].as_f64(), bp))
}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.seed-rate
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./agri.seed-rate-1.0.0-rust.fune, or fetch it from a terminal with fune pull agri.seed-rate@1.0.0:rust.
The whole function, every language, is one file too: agri.seed-rate-1.0.0.fune, 13,674 bytes, sha256 0cb3c657ec361c750a3bb5c33e092b800dbfa3b731ee943c781c2f8d4c3f0672. 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.seed-rate
after — your function gets the result and the arguments, and returns the final result.
// fune: after agri.seed-rate
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.seed-rate
// fune: replace math.round-float in agri.seed-rate
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.seed-rate --steps.
// fune: step agri.seed-rate 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 | |
|---|---|---|---|
| 300 plants/m² at TGW 50 g and 80% establishment is 187.5 kg/ha | 300, 50, 80% | → | kg per hectare 187.5, seeds per square metre 375 |
| FAR spring barley example: 225 plants, TGW 40 g, 90% germination x 85% emergence (76.5%) is about 118 kg/ha | 225, 40, 76.5% | → | kg per hectare 117.6, seeds per square metre 294.1 |
| AHDB wheat benchmark: 260 plants/m² at 70% establishment, TGW 48 g | 260, 48, 70% | → | kg per hectare 178.3, seeds per square metre 371.4 |
| oilseed rape: 50 plants, TGW 5.5 g, 60% establishment | 50, 5.5, 60% | → | kg per hectare 4.6, seeds per square metre 83.3 |
| field beans: 30 plants, TGW 550 g, 90% establishment | 30, 550, 90% | → | kg per hectare 183.3, seeds per square metre 33.3 |
| 100% establishment sows exactly the target | 400, 45, 100% | → | kg per hectare 180, seeds per square metre 400 |
| an exact half tenth rounds up: 100 x 45.25 / 100 = 45.25 | 100, 45.25, 100% | → | kg per hectare 45.3, seeds per square metre 100 |
| TGW 1.15 g is 1.15 kg/ha exactly and rounds to 1.2, where a float product says 1.1499999999999999 | 100, 1.15, 100% | → | kg per hectare 1.2, seeds per square metre 100 |
| TGW is taken to the centigram: 48.004 g is 48 g | 260, 48.004, 70% | → | kg per hectare 178.3, seeds per square metre 371.4 |
| one plant at 0.01% establishment | 1, 1, 0.01% | → | kg per hectare 100, seeds per square metre 10,000 |
Show the other 7 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| zero plants is an error | 0, 50, 80% | → | error: targetPlantsPerSquareMetre must be a whole number from 1 to 100000 |
| a fractional plant count is an error | 250.5, 50, 80% | → | error: targetPlantsPerSquareMetre must be a whole number from 1 to 100000 |
| zero TGW is an error | 300, 0, 80% | → | error: thousandGrainWeightGrams must be a finite number greater than 0 and at most 1000 |
| a TGW that rounds to zero centigrams is an error | 300, 0.004, 80% | → | error: thousandGrainWeightGrams must be at least 0.01 grams |
| zero establishment is an error | 300, 50, 0% | → | error: establishmentBasisPoints must be a whole number from 1 to 10000 |
| establishment over 100% is an error | 300, 50, 100.01% | → | error: establishmentBasisPoints must be a whole number from 1 to 10000 |
| establishment given as a percentage fraction is an error | 300, 50, 0.805% | → | error: establishmentBasisPoints must be a whole number from 1 to 10000 |
More from the author
`seedRate(260, 48, 7000)` is 178.3 kg/ha and 371.4 seeds/m²: 260 plants at 70% establishment needs 371.4 seeds/m², and 1000 seeds weigh 48 g.
The formula follows from the units: seeds/m² = plants / establishment; one seed weighs TGW / 1000 g; and 1 g/m² is 10 kg/ha. It is the calculation behind AHDB's seed rate conversion tool (seeds/m² against TGW, read off as kg/ha) and the one UK agronomy guidance quotes.
## Establishment
`establishmentBasisPoints` is the share of seeds sown that become plants: germination and field losses (slugs, seedbed, sowing date) together. If you have a germination figure and a separate field-loss estimate, multiply them: 90% germination and 85% emergence is 76.5%, `7650`. AHDB's benchmarks are around 70% for well-timed winter wheat, falling as drilling gets later.
## Exactness
The thousand-grain weight is taken to the nearest 0.01 g (half away from zero, `math.round-float`), so the rate is exactly plants x centigrams / basis points and is rounded once, half up, to 0.1 kg/ha. A float calculation gets 1.15 g at 100 plants and 100% as 1.1499999999999999 and rounds it to 1.1; this gives 1.2.
It does not choose the target population, adjust for sowing date or soil, or round to what a drill can be set to.
## Limits
Plants 1 to 100,000 per m² (grass and small seeds are sown in thousands); TGW more than 0 and at most 1000 g, and at least 0.01 g after rounding; establishment 1 to 10,000 basis points.
## Sources
- AHDB, "Seed rate calculator – conversion tool", https://ahdb.org.uk/seed-rate-conversion-tool (seeds/m² to kg/ha by TGW). - AHDB, "Establishment in wheat: germination and emergence (GS0–GS2)", https://ahdb.org.uk/knowledge-library/establishment-in-wheat-germination-and-emergence-gs0-gs2 (the 70% establishment benchmark). - Foundation for Arable Research, "Seed rate calculation", https://www.far.org.nz/resources/seed-rate-calculation: sowing rate = target plants x TGW x 100 / (% germination x % emergence); its worked example (225 plants, TGW 40 g, 90% x 85%, about 118 kg/ha) is a vector.
Files
| Path | Bytes |
|---|---|
| README.md | 2,306 |
| impl/python.py | 1,789 |
| impl/rust.rs | 2,341 |
| impl/typescript.ts | 1,763 |
| vectors.json | 2,841 |