grainPlan is the grain-specific decision orchestrator of the ORCHESTRA agricultural analytics stack -- the practitioner last-mile that turns an upstream member's inference result into an actionable grain-production plan.
It is a thin grain-semantics layer on top of decideR: it reuses decideR's decision engine, its manifest-native pipeline tail, its grade-band loss library, and its grounding firewall, and adds the grain economics those decisions kept re-deriving. grainPlan never reinvents decision theory, manifest reading, or the Independent Oracle Principle firewall; it composes them with grain meaning.
plan_nitrogen_rate() (a profit-priced
nitrogen rate), plan_variety() (a downside-aware variety ranking
from a multi-environment trial or a genomic posterior), and
plan_grade_target() (a quality-band decision against a delivery
standard). Each returns a grain_decision.wheat_protein_bands(),
barley_malting_premium(), and n_rate_economics(): the economic
schedules the orchestra runs kept hand-building, consolidated into
reusable, documented primitives built from published grade
conventions.plan_season() composes several grain
decisions into one grain_plan, combining their grounding worst-case
so a plan resting on any unverified input abstains as a whole.grainPlan starts where these leave off. It takes draws from a fitted model, such as yield at each nitrogen rate or variety merit across trials, and prices them with grain prices, protein bands and malting premiums to give a recommendation. When the evidence is marked unverified, it declines and keeps the status quo instead of recommending.
# decideR is grainPlan's base; install it first (it is not on CRAN).
# install.packages("remotes")
remotes::install_github("max578/decideR")
# remotes::install_github("max578/grainPlan")
install.packages("grainPlan", repos = c('https://max578.r-universe.dev', 'https://cloud.r-project.org'))
A nitrogen-rate decision on grounded yield evidence decides; the same decision on unverified evidence abstains to the status quo. This is the inherited Independent Oracle Principle firewall -- a grain decision is never minted on a fact the upstream producer did not ground.
library(grainPlan)
set.seed(1L)
rates <- seq(0, 200, by = 25)
mu <- 3 + 2.5 * (1 - exp(-rates / 80)) # diminishing returns
yld <- vapply(mu, function(m) rnorm(1500L, m, 0.3), numeric(1500L))
# grounded evidence -> a decision
plan_nitrogen_rate(yld, rates, price_grain = 350, price_n = 1.3,
crop = "wheat",
grounding = decideR::grounding_grounded())
#> <grain_decision> RECOMMENDED [grounded]
#> crop : wheat
#> kind : nitrogen_rate
#> action : 175 kg N/ha
#> why : Apply 175 kg N/ha to the wheat crop: it maximises expected profit ($1596.9/unit area) and clearly beats the status quo on the posterior.
# the SAME draws, unverified -> abstain to the status quo
plan_nitrogen_rate(yld, rates, price_grain = 350, price_n = 1.3,
crop = "wheat",
grounding = decideR::grounding_unverified())
#> <grain_decision> ABSTAINED [ungrounded]
#> crop : wheat
#> kind : nitrogen_rate
#> action : 0 kg N/ha
#> why : Held at the status-quo rate: the yield evidence is unverified, so the Independent Oracle Principle firewall declines to act on it.
grainPlan is a member of the ORCHESTRA federation: it consumes the
orchestra_manifest contract through decideR's duck-typed tail and
depends only on decideR (the orchestra DAG stays acyclic, with grainPlan
a leaf at the decision tail). See CONSTELLATION_MEMBER.md for the full
roster.
grainPlan is a small but consequential API: the meaning of a
recommendation, an abstention, and a grounding token can drive a paddock
decision. While the package is on a 0.x series it follows a
conservative stability rule.
grain_decision, grain_plan) do not change silently.context / metadata.NEWS.md (as the v0.1.1 point-GEBV uncertainty change was).Bug reports and suggestions are welcome through the issue tracker.
citation("grainPlan")
MIT (c) 2026 Max Moldovan.