Season Planning & Crop Rotation Optimization

A crop automation platform spends most of the year executing decisions, but the quality of every one of those decisions is fixed months earlier, when the season plan is written. Season planning is the layer that takes three inputs that already exist elsewhere on the platform — the agronomic rules the Crop Application Timing & Agronomic Validation engine enforces, the boundary-anchored field state produced by Farm Data Ingestion & Field Boundary Synchronization, and the regulatory constraints encoded by Agricultural Automation System Architecture & Compliance — and turns them into an executable artifact: a crop rotation, a planting schedule, and a set of variable-rate input prescriptions that field controllers can run. This guide is the reference architecture for that planning layer. It treats a season plan not as a spreadsheet but as a constraint-satisfaction problem whose output is either a validated plan or an explicit flag that no feasible plan exists.

Operational Context: What Season Planning Solves

For a farm operations team, a bad season plan is not discovered in a design review — it is discovered in June, when the window to correct it has closed. A rotation that ignores a herbicide label restriction plants a crop that will not emerge; a nutrient budget that looks fine field-by-field violates a whole-farm limit across the rotation; a planting schedule that fit on paper turns out to demand more machine-days than the weather ever allowed. The engineering challenge is that each of these failures is a constraint that was knowable at planning time and simply was not checked. The planning layer exists to make every such constraint executable, so an infeasible plan fails loudly at the desk instead of silently in the field.

The architecture in this guide is built to prevent the following recurring failures:

  • Herbicide carryover / replant injury. A previous season’s residual herbicide carries a labeled rotational-crop restriction — a plant-back interval — that the new rotation ignores, so the following crop suffers root injury or fails to establish. The planner encodes each label’s plant-back interval as a hard rotation constraint and rejects any sequence that violates it.
  • Nutrient-management plan violation. Nitrogen and phosphorus applied across a multi-year rotation exceed the limits of an NRCS 590 nutrient-management plan, even though no single application looks excessive. The planner sums modeled nutrient load across the whole rotation, not per pass, so a whole-farm limit is checked at the level it is written.
  • Missed growing-degree-day window. A planting date is chosen so that the crop cannot accumulate enough growing-degree-days to reach maturity before the first frost, and the crop fails late in the season when nothing can be done. The planting-schedule optimizer constrains every planting date to a thermal-time window derived from the crop’s maturity requirement and the field’s climate normals.
  • Infeasible operation schedule. A plan assigns more field operations to a planting window than the available field-days can physically hold, and the shortfall is discovered mid-season when machines cannot catch up. The planner models equipment capacity and workable-day probability as a scheduling constraint, so an over-committed calendar is flagged before the season starts.
  • Over-limit prescription rate. A variable-rate map sets a rate in one management zone that exceeds a regulated or label maximum, which a task controller will happily apply. Every prescription map is validated zone-by-zone against the applicable maximum rate before it is exported, and any over-limit zone blocks the export.

The rest of this document walks the planning architecture top to bottom: the constraint-solver data flow, the plan and constraint models, the production season-planner service, the regulatory frameworks that bound it, the resilience behavior when new field data arrives, the security boundary around a plan of record, and the operational runbook.

Architectural Overview: The Constraint-Driven Planning Pipeline

The planning layer is organized as a pipeline that converges field state and a constraint set into a solver, validates the solver’s output against the same constraints a second time, and only then materializes prescriptions. This separation matters: a solver optimizes toward an objective, but an optimizer that is handed a slightly stale constraint can return a plausible plan that violates a rule. Re-validating the solved plan against the authoritative constraint set — independently of the solver — is what turns an optimization result into a plan you can execute.

Five stages compose the pipeline:

  1. Field-state assembly — gathers each field’s boundary-anchored history from the ingestion platform: the rotation history that determines carryover restrictions, the residual-nutrient balance, and the current soil and climate state. Nothing enters the planner that is not tied to a verified boundary revision.
  2. Constraint compilation — resolves the applicable constraint set for the season: plant-back intervals from pesticide labels, nutrient limits from the field’s conservation plan, thermal-time windows from crop maturity requirements, and equipment-capacity limits. Regulatory constraints are pulled from the EPA/USDA Rule Mapping engine so the planner and the runtime validator share one source of truth.
  3. Solve / validate — a constraint solver searches for a crop assignment and operation schedule that satisfies every hard constraint and optimizes the soft objective (yield potential, input cost, agronomic diversity). The solved plan is then re-checked against the constraint set; a plan that fails re-validation is treated as infeasible.
  4. Plan materialization — expands the validated assignment into a dated planting schedule and a set of per-zone variable-rate prescriptions, each carrying the constraint that bounds it.
  5. Prescription export — serializes prescriptions to the formats field controllers consume and hands the executable plan to the application-timing engine, which runs the agronomic and buffer checks at the moment of application.

Every plan carries an explicit status: DRAFT → SOLVED → VALIDATED → EXPORTED, and any stage that cannot produce a feasible result diverts the plan to INFEASIBLE, holding it for agronomist review rather than emitting a plan that breaks a rule. A plan never reaches export without passing re-validation, and the constraint-set revision in force at solve time is stamped onto the plan.

Constraint-driven season-planning pipeline and the plan state machine The planning path moves a plan left to right through Field-State Assembly (boundary-anchored history), Constraint Compilation (resolve the season's rules), Solve and Validate (search for a feasible plan, then re-check it independently), Plan Materialization (expand to dated schedule and per-zone prescriptions), and Prescription Export (serialize for field controllers). Two inputs feed the pipeline from below: boundary-anchored field state enters Field-State Assembly, and one compiled constraint set feeds both Constraint Compilation and the independent validator inside Solve and Validate. The exported plan hands off to the downstream Application Timing engine. Below the track, the plan lifecycle state machine runs DRAFT, SOLVED, VALIDATED, EXPORTED in sequence; from each pre-export state a dashed branch drops to a single INFEASIBLE store, so any stage that cannot produce a feasible plan diverts it for agronomist review instead of exporting a plan that breaks a rule. Constraint-driven planning — field state and one constraint set converge on a solver, then an independent validator gates every plan Field-State Assembly Constraint Compilation Solve & Validate Plan Materialization Prescription Export Application Timing runs the field checks Field State Compiled Constraint Set DRAFT SOLVED VALIDATED EXPORTED INFEASIBLE boundary-anchored resolve the rules search + re-check schedule + zones for controllers rotation + nutrients plant-back · N/P · GDD · capacity held for agronomist review any stage infeasible → hold Plan lifecycle — advance one gate at a time, or divert to review

Data Model and Schema Constraints

A season plan is only as trustworthy as the entities it is built from, and the entities that matter carry regulated quantities: nutrient rates, thermal-time thresholds, plant-back intervals. Modeling them with Pydantic v2 forces every such quantity to be validated at construction, and — critically — forces every rate to be a Decimal, because a floating-point nitrogen rate that rounds the wrong way is the difference between a compliant plan and a nutrient-management-plan violation. The four anchors are the crop season plan (the executable artifact), the rotation history (what determines carryover restrictions), the plan constraint (a single rule the plan must satisfy), and the planting window (the thermal-time envelope for a planting date).

python
from __future__ import annotations

import datetime as dt
from decimal import Decimal
from enum import Enum
from typing import Literal
from uuid import UUID

from pydantic import BaseModel, Field, field_validator, model_validator


class ConstraintKind(str, Enum):
    PLANT_BACK = "plant_back"          # herbicide carryover / replant interval
    NUTRIENT_LIMIT = "nutrient_limit"  # NRCS 590 N/P budget across rotation
    GDD_WINDOW = "gdd_window"          # growing-degree-day maturity envelope
    FIELD_CAPACITY = "field_capacity"  # equipment field-days per window
    MAX_RATE = "max_rate"              # regulated / label maximum rate


class PlanConstraint(BaseModel):
    """One hard rule the solved plan must satisfy, with its provenance."""

    constraint_id: UUID
    kind: ConstraintKind
    # The regulated bound. Decimal so a rate comparison is never a float artifact.
    limit_value: Decimal = Field(gt=Decimal("0"))
    unit: str = Field(min_length=1)          # e.g. "lb_N/ac", "days", "GDD"
    # Where the number came from — a label, a 590 plan, a climate normal.
    authority: str = Field(min_length=3)
    hard: bool = True                        # hard blocks; soft is advisory

    @field_validator("limit_value")
    @classmethod
    def quantize_limit(cls, value: Decimal) -> Decimal:
        # Regulated rates are meaningful to three decimals; pin the scale so
        # two constraints with the same intent compare equal.
        return value.quantize(Decimal("0.001"))


class RotationHistory(BaseModel):
    """Prior crop_cycle record for one field zone — the carryover source."""

    zone_id: UUID
    # crop_cycle key: (year, season) uniquely names a slot in the rotation.
    crop_cycle: tuple[int, Literal["spring", "summer", "fall", "winter"]]
    crop: str = Field(min_length=2)
    # Products applied that may impose a plant-back interval next cycle.
    residual_products: list[str] = Field(default_factory=list)
    applied_n: Decimal = Field(default=Decimal("0"), ge=Decimal("0"))
    applied_p: Decimal = Field(default=Decimal("0"), ge=Decimal("0"))


class PlantingWindow(BaseModel):
    """The thermal-time envelope a planting date must fall within."""

    zone_id: UUID
    crop: str = Field(min_length=2)
    earliest: dt.datetime          # tz-aware UTC: soil-temperature open
    latest: dt.datetime            # tz-aware UTC: last date to reach maturity
    gdd_to_maturity: Decimal = Field(gt=Decimal("0"))
    gdd_available: Decimal = Field(gt=Decimal("0"))

    @field_validator("earliest", "latest")
    @classmethod
    def must_be_utc(cls, value: dt.datetime) -> dt.datetime:
        if value.utcoffset() != dt.timedelta(0):
            raise ValueError("planting window bounds must be tz-aware UTC")
        return value

    @model_validator(mode="after")
    def window_must_be_feasible(self) -> "PlantingWindow":
        if self.latest <= self.earliest:
            raise ValueError("latest must be after earliest")
        if self.gdd_available < self.gdd_to_maturity:
            # No date in this window lets the crop finish; fail at construction.
            raise ValueError("window cannot accumulate GDD to maturity")
        return self


class CropSeasonPlan(BaseModel):
    """The executable seasonal artifact: assignment, schedule, provenance."""

    plan_id: UUID
    crop_cycle: tuple[int, Literal["spring", "summer", "fall", "winter"]]
    zone_id: UUID
    crop: str = Field(min_length=2)
    planting_date: dt.datetime               # tz-aware UTC
    planned_n: Decimal = Field(ge=Decimal("0"))
    planned_p: Decimal = Field(ge=Decimal("0"))
    constraint_set_revision: int = Field(ge=1)  # which rules bound this plan
    status: Literal["draft", "solved", "validated", "exported", "infeasible"]
    solved_at: dt.datetime

    @field_validator("planting_date", "solved_at")
    @classmethod
    def plan_times_utc(cls, value: dt.datetime) -> dt.datetime:
        if value.utcoffset() != dt.timedelta(0):
            raise ValueError("plan datetimes must be tz-aware UTC")
        return value

Three modeling decisions carry most of the compliance weight:

  • Decimal on every rate, quantized to a fixed scale. Nutrient and application rates are compared against regulated limits, and a float representation makes 4.4 lb occasionally not equal 4.4 lb. Pinning the scale at construction means two constraints expressing the same limit compare equal, and a rate that sits exactly on a bound is decided deterministically rather than by rounding.
  • crop_cycle as a (year, season) key. A rotation is a sequence of named slots, and carryover restrictions are expressed relative to those slots (“no sensitive crop in the cycle following application”). Making the cycle an explicit composite key — rather than a loose date — lets the planner reason about “next cycle” without ambiguity across a winter that straddles a calendar year.
  • Feasibility checks in the model, not just the solver. PlantingWindow.window_must_be_feasible rejects a window that cannot accumulate the growing-degree-days a crop needs before the solver ever sees it. Pushing physical infeasibility to construction means the solver only ever reasons about windows that are at least individually possible.

Core Implementation: The Constraint-Driven Season Planner

The central runtime component is a service that takes a field’s assembled state and a compiled constraint set and returns either a validated CropSeasonPlan or an explicit infeasibility flag. Its defining property is conservatism: when the planner cannot produce a plan that satisfies every hard constraint, it does not relax a constraint and ship a “best effort” plan — it flags the case for agronomist review. That is the correct default for a system whose output drives regulated field operations, where a wrong plan is far more expensive than a deferred one. The service is typed end to end, retries only genuinely transient faults, logs a structured record at every decision, and calls an audit hook on every terminal outcome.

python
import datetime as dt
import logging
from dataclasses import dataclass
from decimal import Decimal
from typing import Callable, Sequence

from tenacity import (
    retry,
    retry_if_exception_type,
    stop_after_attempt,
    wait_exponential,
)

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(name)s | %(message)s",
    handlers=[logging.StreamHandler()],
)
logger = logging.getLogger("agtech.planning")


class PlannerTransientError(Exception):
    """Retryable fault — a constraint store or solver-backend timeout."""


class PlanInfeasible(Exception):
    """Terminal: no plan satisfies every hard constraint. Route to review."""

    def __init__(self, reasons: Sequence[str]):
        self.reasons = list(reasons)
        super().__init__("; ".join(reasons))


@dataclass(frozen=True)
class PlanCandidate:
    """A solver proposal, prior to independent re-validation."""

    zone_id: "UUID"
    crop: str
    planting_date: dt.datetime
    planned_n: Decimal
    planned_p: Decimal


class SeasonPlanner:
    def __init__(
        self,
        solve: Callable[["FieldState", Sequence[PlanConstraint]], PlanCandidate | None],
        audit_hook: Callable[[str, str, list[str]], None] | None = None,
    ):
        # `solve` is the pluggable optimizer (OR-Tools CP-SAT in production).
        self._solve = solve
        self._audit_hook = audit_hook

    def _emit_audit(self, zone_id: str, outcome: str, notes: list[str]) -> None:
        # Every terminal outcome is recorded, including the infeasible path.
        if self._audit_hook is not None:
            self._audit_hook(zone_id, outcome, notes)

    @staticmethod
    def _check_hard_constraints(
        candidate: PlanCandidate,
        state: "FieldState",
        constraints: Sequence[PlanConstraint],
    ) -> list[str]:
        """Re-validate a solved candidate independently of the solver."""
        violations: list[str] = []
        for c in constraints:
            if not c.hard:
                continue
            match c.kind:
                case ConstraintKind.MAX_RATE:
                    # A single zone rate above the regulated ceiling blocks export.
                    if candidate.planned_n > c.limit_value:
                        violations.append(
                            f"N rate {candidate.planned_n} exceeds max "
                            f"{c.limit_value} {c.unit} ({c.authority})"
                        )
                case ConstraintKind.NUTRIENT_LIMIT:
                    # Whole-rotation load, not this pass alone.
                    rotation_n = state.residual_n + candidate.planned_n
                    if rotation_n > c.limit_value:
                        violations.append(
                            f"rotation N {rotation_n} exceeds 590 budget "
                            f"{c.limit_value} {c.unit} ({c.authority})"
                        )
                case ConstraintKind.PLANT_BACK:
                    if candidate.crop in state.restricted_crops:
                        violations.append(
                            f"{candidate.crop} within plant-back interval "
                            f"({c.authority})"
                        )
                case ConstraintKind.GDD_WINDOW:
                    if not state.window.earliest <= candidate.planting_date <= state.window.latest:
                        violations.append(
                            f"planting date outside GDD window ({c.authority})"
                        )
                case ConstraintKind.FIELD_CAPACITY:
                    if state.required_field_days > c.limit_value:
                        violations.append(
                            f"schedule needs {state.required_field_days} field-days, "
                            f"window holds {c.limit_value} ({c.authority})"
                        )
        return violations

    @retry(
        retry=retry_if_exception_type(PlannerTransientError),
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, min=2, max=10),
        reraise=True,
    )
    def build_plan(
        self,
        state: "FieldState",
        constraints: Sequence[PlanConstraint],
        constraint_set_revision: int,
    ) -> CropSeasonPlan:
        """Solve, independently re-validate, and materialize one season plan."""
        candidate = self._solve(state, constraints)
        if candidate is None:
            # The solver proved no assignment satisfies the hard constraints.
            reasons = ["solver returned no feasible assignment"]
            logger.warning("Infeasible zone=%s: %s", state.zone_id, reasons)
            self._emit_audit(str(state.zone_id), "infeasible_no_solution", reasons)
            raise PlanInfeasible(reasons)

        # Independent re-validation: never trust the solver's own feasibility.
        violations = self._check_hard_constraints(candidate, state, constraints)
        if violations:
            logger.error("Solved plan failed re-validation zone=%s: %s",
                         state.zone_id, violations)
            self._emit_audit(str(state.zone_id), "infeasible_revalidation", violations)
            raise PlanInfeasible(violations)

        plan = CropSeasonPlan(
            plan_id=state.new_plan_id(),
            crop_cycle=state.crop_cycle,
            zone_id=state.zone_id,
            crop=candidate.crop,
            planting_date=candidate.planting_date,
            planned_n=candidate.planned_n,
            planned_p=candidate.planned_p,
            constraint_set_revision=constraint_set_revision,
            status="validated",
            solved_at=dt.datetime.now(dt.timezone.utc),
        )
        logger.info("Validated plan zone=%s crop=%s date=%s rev=%s",
                    plan.zone_id, plan.crop, plan.planting_date.date(),
                    constraint_set_revision)
        self._emit_audit(str(plan.zone_id), "validated", [])
        return plan

The design turns on one deliberate separation. The solve callable is a pluggable optimizer — in production a CP-SAT model from OR-Tools that encodes plant-back intervals, nutrient budgets, thermal-time windows, and equipment capacity as constraints and maximizes a weighted objective. But the planner never trusts the solver’s own report of feasibility: _check_hard_constraints re-validates the returned candidate against the same hard rules, independently, using Decimal comparisons so a rate exactly on a limit is decided without float drift. A candidate that fails re-validation is treated exactly like no candidate at all — it raises PlanInfeasible and routes to review. This guards against the real failure mode where a solver is handed a slightly stale or mis-encoded constraint and returns a plan that looks optimal and breaks a rule. The retry policy is narrow on purpose: only a PlannerTransientError (a constraint-store or solver-backend timeout) is retried; a PlanInfeasible is never retried, because re-running a solver over the same infeasible constraint set only wastes time.

Regulatory and Compliance Constraints

Season planning is where regulated quantities are first committed, so the relevant frameworks are architectural inputs rather than after-the-fact reporting. Three bodies of rule bound the planner directly.

Crop-rotation structure is shaped by the USDA Natural Resources Conservation Service conservation practice standards. The planner treats the NRCS Conservation Crop Rotation standard (Code 328) as the source of rotational-diversity and residue objectives, and the NRCS Nutrient Management standard (Code 590) as the authority for the nitrogen and phosphorus budget that a rotation must not exceed. The critical modeling consequence is that a 590 budget is a whole-rotation limit: the planner must sum modeled nutrient load across the cycle, which is why NUTRIENT_LIMIT re-validation adds state.residual_n to the candidate’s planned nitrogen rather than checking a single pass in isolation.

Rotational-crop safety is bounded by pesticide labeling. Under the EPA pesticide worker-safety and labeling framework, a product’s label specifies rotational-crop restrictions — plant-back intervals — that dictate which crops may follow an application and after how long. These are legally binding label directions, and the planner encodes each as a hard PLANT_BACK constraint derived from the field’s RotationHistory.residual_products; the detailed encoding lives in Rotation Constraint Modeling, and the same restrictions constrain the runtime through the EPA/USDA Rule Mapping engine so the plan and the field checks agree. Broader agronomic rotation principles — the rotational logic behind breaking pest cycles and maintaining soil fertility — follow the FAO crop-rotation guidance, which informs the soft objective the solver optimizes rather than a hard bound.

Finally, the planner shares a boundary with two runtime systems it must not contradict. Thermal-time windows and growth-stage logic are consistent with Growth Stage Mapping, and spatial exclusion distances that constrain where a prescription may place a rate come from Buffer Zone Calculations; a prescription zone that overlaps a regulated buffer is treated as an over-limit zone and blocked at export.

Resilience: Deterministic Re-Planning

A season plan is not written once. New telemetry arrives — a boundary revision changes a zone’s acreage, a soil test updates the residual-nutrient balance, an actual planting date lands earlier than planned and shifts the thermal-time budget. The resilience requirement is that re-planning on new data is deterministic: given the same field state and the same constraint-set revision, the planner must produce the same plan, and a plan must only change when an input that genuinely bounds it has changed. Non-deterministic re-planning is worse than none — it erodes trust in the plan of record and makes an audit impossible to reproduce.

Three mechanisms enforce this. First, the planner is a pure function of (FieldState, constraint_set, revision): the solver is seeded deterministically and the objective weights are pinned per revision, so re-solving identical inputs yields an identical plan. Second, every plan stamps the constraint_set_revision it was solved against; when new data arrives, the planner compares the new field state and constraint revision against the stamped plan and re-plans only the zones whose bounding inputs actually moved, leaving untouched plans byte-identical. Third — the conservative fallback that ties the whole layer together — if re-planning on new data produces PlanInfeasible where a valid plan previously existed (a boundary change shrank a window, a soil test raised residual nitrogen past the 590 budget), the planner does not silently discard the old plan; it holds both, flags the regression for agronomist review, and keeps the last validated plan as the plan of record until a human resolves it. The reconnection and de-duplication mechanics this shares with the platform’s decision path live in the compliance architecture’s fallback routing logic.

Security and Access Governance

A season plan is a high-value, high-trust artifact: it commits regulated nutrient rates, encodes proprietary agronomic strategy, and — once exported — drives autonomous field controllers. The security posture protects the plan of record from unauthorized change and preserves a complete account of who changed what.

  • RBAC on the plan lifecycle. An agronomist may author and approve a plan; the automated planner may propose and validate one; but only a narrowly scoped approver role may promote a plan to exported, and no role may edit a validated plan in place — a change creates a new revision. These boundaries are specified under Security & Access Boundaries.
  • Immutable, versioned plans. A validated plan is never mutated; new data produces a new CropSeasonPlan with an incremented constraint-set revision, so the exact plan that drove a field operation is always reproducible.
  • Parameterized access. All reads and writes against the plan and constraint stores use bound parameters, never string interpolation, closing injection paths from constraint values or crop identifiers that may originate in third-party label or soil-test imports.
  • Audit-trail completeness. Every terminal planner outcome — validated, infeasible, or held-for-review — records four facts: which zone and crop_cycle, under which constraint-set revision, with which violations if any, and when the decision was made.

Beyond mutation control, production planning is isolated from development so a test solve can never promote a synthetic plan into a real field’s plan of record.

Operational Runbook

Deploying and operating the season planner safely follows a fixed sequence. Treat the steps below as the release and on-call checklist for the planning-critical path.

  1. Pin and publish the constraint set. Confirm the constraint-set revision — plant-back intervals, 590 nutrient budgets, thermal-time windows, equipment capacities — is published and effective-dated before running the planner. Never publish a constraint change and a planner code change in the same step; stage the constraints first, then roll the code.
  2. Run the schema and code QA gate. Validate that every Pydantic model loads and each Python block passes the syntax-and-indentation check before build. A model change that widens a rate field is a breaking change for downstream controllers and must be reviewed.
  3. Dry-run against a staging constraint set. Solve a fixed corpus of known-feasible, known-infeasible, and on-the-limit field-state fixtures and assert the expected validated / infeasible_no_solution / infeasible_revalidation split. Inject a plan whose nitrogen rate sits one unit above the 590 budget and confirm it routes to review rather than validating.
  4. Verify independent re-validation. Feed the planner a hand-crafted candidate that violates a plant-back interval and confirm _check_hard_constraints catches it even when the mock solver reports the candidate as feasible.
  5. Verify the audit hook end to end. Run one zone and confirm exactly one ledger entry appears with the outcome, constraint-set revision, and any violation reasons stamped on it.
  6. Deploy with deterministic seeding armed. Roll out with the solver seed and objective weights pinned to the published revision so a re-solve of identical inputs is byte-identical.
  7. Watch the observability signals. Monitor the infeasibility rate, the re-validation-failure rate (a solved plan that fails independent re-check signals a stale or mis-encoded constraint), the re-plan churn rate, and the constraint-set-revision skew between the newest published revision and the one bounding live plans.
  8. Escalate on defined triggers. Page on-call when the re-validation-failure rate rises above zero (the solver and validator disagree — a correctness risk that takes priority over throughput), when the infeasibility rate spikes for a region (a constraint compilation error), or when revision skew persists (plans bound to superseded rules).

Frequently Asked Questions

Why re-validate a solved plan against the same constraints the solver already used? Because a solver optimizes against the constraints it was given, and if any of those were stale or mis-encoded it will confidently return a plan that violates the authoritative rule. Independent re-validation with Decimal comparisons catches that disagreement before a plan reaches a field controller, which is why a re-validation failure pages on-call ahead of any throughput concern.

What does the planner do when no feasible plan exists? It flags the case as PlanInfeasible with the specific violated constraints and routes it to agronomist review, keeping the last validated plan as the plan of record. It never relaxes a hard constraint to ship a best-effort plan, because a plan that quietly breaks a plant-back interval or a 590 nutrient budget is far more expensive than a deferred one.

How does the planner keep a whole-rotation nutrient limit from being violated one field at a time? The NUTRIENT_LIMIT check sums modeled load across the rotation — it adds the field’s residual nitrogen to the candidate’s planned rate and compares the total against the NRCS 590 budget — rather than checking each application in isolation, so a sequence of individually reasonable passes that together exceed the budget is caught.

Why is re-planning on new data required to be deterministic? Because the plan is the reproducible record that an audit and every downstream controller trust. Seeding the solver and pinning objective weights per constraint-set revision means identical inputs produce an identical plan, and a plan only changes when an input that actually bounds it moves — so a re-plan is explainable rather than mysterious.

Up: Crop Planning & Input Tracking Automation home