Encoding Herbicide Carryover Restrictions as Constraints
Problem statement
Herbicide carryover is the quiet way a rotation plan turns into a failed stand. A residual herbicide applied to this year’s crop persists in the soil, and if next year’s crop is sensitive to it and the labeled plant-back interval has not fully elapsed, the replant emerges stunted, chlorotic, or not at all — and the loss is only visible weeks after the seed is in the ground. The rule that would have caught it is printed on the EPA-approved product label as a rotational-crop restriction, and it is legally binding. This guide encodes that restriction as a machine-readable constraint so a proposed next crop is checked against every prior application before the plan is committed, inside the broader rotation constraint modeling subsystem.
The check is more than “has enough time passed.” A plant-back interval is conditional: atrazine breaks down more slowly in high-pH soils and in a dry season, so its label extends the interval when soil pH exceeds a threshold or cumulative rainfall since application falls short. Encoding only the nominal interval passes a plan that a wet-year assumption would fail. The module below models three common residuals — atrazine, imazethapyr, and clopyralid — with their conditional extensions, and returns a typed replant verdict of SAFE, FLAG, or BLOCK rather than a bare boolean, so an agronomist can distinguish a hard label breach from a conditional window that needs review.
Parameter reference table
Every value below changes whether a replant risk is caught or missed. Recommended values follow common US label language; always confirm the exact interval against the specific product label for the rate applied.
| Parameter | Type | Recommended value | Effect on behavior |
|---|---|---|---|
min_months |
int |
per label | The labeled minimum plant-back interval; below it the verdict is always BLOCK, regardless of conditions. |
ph_threshold |
Decimal |
7.5 |
Soil pH above which residual breakdown slows and the interval extends (atrazine, clopyralid). |
ph_extends_months |
int |
6 |
Extra months added to the interval when the pH or rainfall condition is met. |
min_rain_mm |
Decimal |
150–300 |
Cumulative rainfall the label assumes for breakdown; below it the extended interval applies. |
rate_g_ai_ha |
Decimal |
applied rate | Grams of active ingredient per hectare; a higher rate lengthens real persistence and should widen the interval. |
plant_date |
date |
planned | The proposed planting date of the next crop; elapsed months are measured to it, not to a calendar year. |
next_crop |
str |
— | Canonical crop code; the restriction table is keyed on the active-ingredient/next-crop pair. |
Keep the plant-back table in a version-stamped registry rather than inline literals, so a label revision re-checks every historical plan the same way — the same versioning discipline the parent rotation constraint modeling subsystem applies to its whole rule set.
Runnable implementation
The module below is fully typed, targets Python 3.10+, and depends only on the standard library. It measures elapsed months to the actual planting date, applies the conditional extension when soil pH or rainfall triggers it, and returns a typed verdict. Rates use Decimal so a regulated value never carries a binary-float rounding error.
from __future__ import annotations
import json
import logging
from dataclasses import dataclass
from datetime import date
from decimal import Decimal
from enum import Enum
logger = logging.getLogger("carryover.check")
class Risk(str, Enum):
SAFE = "safe"
FLAG = "flag" # inside a pH/rainfall-extended window; needs agronomist review
BLOCK = "block" # inside the labeled minimum interval; not admissible
@dataclass(frozen=True)
class Application:
field_id: str
active_ingredient: str
applied_on: date
rate_g_ai_ha: Decimal
soil_ph: Decimal
rainfall_since_mm: Decimal
@dataclass(frozen=True)
class Restriction:
min_months: int # labeled minimum plant-back interval
ph_extends_months: int # extra months when the pH or rainfall condition holds
ph_threshold: Decimal # soil pH above which breakdown slows
min_rain_mm: Decimal # cumulative rainfall the label assumes for breakdown
# active ingredient -> next crop -> restriction (representative US label values)
PLANT_BACK: dict[str, dict[str, Restriction]] = {
"atrazine": {
"soybean": Restriction(12, 6, Decimal("7.5"), Decimal("150")),
"sugarbeet": Restriction(24, 0, Decimal("7.5"), Decimal("300")),
},
"imazethapyr": {
"corn": Restriction(9, 0, Decimal("7.8"), Decimal("0")),
"sugarbeet": Restriction(40, 0, Decimal("7.8"), Decimal("0")),
},
"clopyralid": {
"sugarbeet": Restriction(18, 0, Decimal("7.5"), Decimal("0")),
"canola": Restriction(18, 0, Decimal("7.5"), Decimal("0")),
},
}
@dataclass(frozen=True)
class ReplantVerdict:
risk: Risk
field_id: str
active_ingredient: str
next_crop: str
months_elapsed: int
required_months: int
reason: str
def _months_between(start: date, end: date) -> int:
return (end.year - start.year) * 12 + (end.month - start.month)
def assess_replant(app: Application, next_crop: str, plant_date: date) -> ReplantVerdict:
"""Return the replant risk of next_crop given one prior herbicide application."""
restriction = PLANT_BACK.get(app.active_ingredient, {}).get(next_crop)
elapsed = _months_between(app.applied_on, plant_date)
if restriction is None:
# No labeled restriction for this AI/crop pair: nothing to enforce here.
return _finish(Risk.SAFE, app, next_crop, elapsed, 0,
"no rotational restriction on record for this pair")
# High pH or a dry season slows residual breakdown, so the label extends the interval.
conditional = (app.soil_ph > restriction.ph_threshold
or app.rainfall_since_mm < restriction.min_rain_mm)
required = restriction.min_months + (restriction.ph_extends_months if conditional else 0)
if elapsed < restriction.min_months:
risk, reason = Risk.BLOCK, "inside labeled minimum plant-back interval"
elif elapsed < required:
risk, reason = Risk.FLAG, "inside pH/rainfall-extended interval; review required"
else:
risk, reason = Risk.SAFE, "plant-back interval satisfied"
return _finish(risk, app, next_crop, elapsed, required, reason)
def _finish(risk: Risk, app: Application, next_crop: str, elapsed: int,
required: int, reason: str) -> ReplantVerdict:
verdict = ReplantVerdict(risk, app.field_id, app.active_ingredient,
next_crop, elapsed, required, reason)
logger.info(json.dumps({
"event": "replant_assessed",
"field_id": verdict.field_id,
"active_ingredient": verdict.active_ingredient,
"next_crop": verdict.next_crop,
"months_elapsed": verdict.months_elapsed,
"required_months": verdict.required_months,
"risk": verdict.risk.value,
"reason": verdict.reason,
}))
return verdict
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(message)s")
prior = Application(
field_id="S-07",
active_ingredient="atrazine",
applied_on=date(2026, 5, 12),
rate_g_ai_ha=Decimal("1120"),
soil_ph=Decimal("7.8"), # above 7.5 -> breakdown slows
rainfall_since_mm=Decimal("120"), # below 150 -> interval extends
)
result = assess_replant(prior, "soybean", date(2027, 5, 1))
print(f"{result.risk.value}: elapsed={result.months_elapsed} required={result.required_months}")
In the example, twelve months have elapsed — enough to clear atrazine’s twelve-month minimum for soybean — but the high soil pH and low rainfall extend the interval to eighteen months, so the verdict is FLAG rather than SAFE. That is the whole point of encoding the condition: the nominal interval alone would have waved the plan through.
Log patterns and observable signals
Every assessment emits one structured line, so any planting decision can be traced back to the application and interval that judged it.
Safe path (interval fully satisfied):
{"event": "replant_assessed", "field_id": "S-07", "active_ingredient": "atrazine", "next_crop": "soybean", "months_elapsed": 24, "required_months": 18, "risk": "safe", "reason": "plant-back interval satisfied"}
Conditional flag (nominal interval met, extended interval not):
{"event": "replant_assessed", "field_id": "S-07", "active_ingredient": "atrazine", "next_crop": "soybean", "months_elapsed": 12, "required_months": 18, "risk": "flag", "reason": "inside pH/rainfall-extended interval; review required"}
Hard block (inside the labeled minimum):
{"event": "replant_assessed", "field_id": "N-03", "active_ingredient": "imazethapyr", "next_crop": "sugarbeet", "months_elapsed": 12, "required_months": 40, "risk": "block", "reason": "inside labeled minimum plant-back interval"}
When triaging a plan, filter on risk of block first — those are legal label breaches that must change the crop or the date. A run of flag results on one active ingredient usually points at a dry season or a high-pH block of fields; correlate the field_id set against soil-test records before overriding any of them.
Safe override protocol
Occasionally a FLAG verdict must be admitted — a field whose soil test post-dates the application shows a pH that no longer slows breakdown, or a localized rainfall gauge recorded more than the regional figure. The override supplies better evidence to the same check; it never bypasses assess_replant, and a BLOCK is never overridable because the labeled minimum is not advisory.
Guard conditions, all mandatory:
- Never override a BLOCK. Only a
FLAG— a conditional extension — is eligible. A verdict inside the labeled minimum interval requires a different crop or a later date, full stop. - Evidence, not judgment. The override must supply a concrete measurement (a dated soil-test pH, a field-level rainfall total) that re-runs the check, not a free-text waiver.
- Dry-run and re-assess. Re-run
assess_replantwith the corrected input; the override is valid only if the new verdict isSAFE. - Immutable audit trail. The original verdict, the corrected input, the approver identity, and the citation to the governing pesticide label are written to an append-only ledger for compliance review.
Troubleshooting
- A plan passes that a wet-year assumption should fail. Root cause: only
min_monthsis encoded and the pH/rainfall extension is missing. Remediation: populateph_extends_months,ph_threshold, andmin_rain_mmfrom the label; aFLAGon a high-pH field is the intended behavior, not a bug. - Interval looks satisfied but the replant is stunted. Root cause: elapsed time was measured to a calendar year boundary rather than to the real planting date. Remediation: measure months to the actual
plant_date; a May application to a May planting is twelve months, but to an April planting it is only eleven. - A high application rate carried over past the nominal interval. Root cause: the table encodes a single interval independent of rate. Remediation: key the restriction on a rate band, or widen the interval for the top label rate; carry
rate_g_ai_hainto the decision rather than logging it only. - An unmodeled active ingredient returns SAFE. Root cause: the AI/crop pair is absent from
PLANT_BACK, so no restriction applies. Remediation: treat an unknown residual as unmodeled, not clean — surface aFLAGfrom a catch-all entry until the label is encoded, the same way the parent rotation constraint modeling subsystem flags unmodeled crops.
Frequently asked questions
Why extend a plant-back interval for soil pH and rainfall? Because residuals like atrazine break down more slowly in high-pH, dry conditions, and the label says so. Encoding only the nominal interval passes a plan a dry year would fail.
What is the difference between a FLAG and a BLOCK? A BLOCK is inside the legally binding minimum interval; a FLAG means the nominal interval is met but a pH or rainfall condition extends it and the field needs review.
Can a carryover BLOCK ever be overridden? No — the labeled minimum is legal, not advisory. Only a conditional FLAG is eligible, and only when new measured evidence re-runs the check to SAFE.
Related
- Rotation Constraint Modeling — the subsystem this carryover check plugs into as one versioned rule.
- Modeling Cover-Crop Rotations with Constraint Solvers — the sibling guide that selects cover species under the same conservation constraints.
- EPA/USDA Rule Mapping — the runtime label-restriction registry this planning check mirrors.
Up: Rotation Constraint Modeling · Season Planning & Crop Rotation Optimization