A prescription map is rejected by the machine controller

Problem statement

The file is on the stick, the terminal sees it, and one of four things happens: it refuses to import, it imports and shows an empty field, it imports and applies a constant rate, or it applies rates that bear no relation to the map.

All four are mechanical rather than agronomic, and they are diagnosed by inspecting the file rather than by reloading it. The usual causes are a coordinate system the terminal does not accept, a rate column whose name or type the terminal does not recognise, geometry that is invalid or multipart, and a format that the terminal supports in a different version from the one you wrote.

Quick answer

Check the file the way the terminal will read it:

import geopandas as gpd, numpy as np

rx = gpd.read_file("prescription.shp")
print("CRS:", rx.crs)
print("columns:", rx.columns.tolist())
print("dtypes:", rx.dtypes.astype(str).to_dict())
print("geometry types:", rx.geom_type.value_counts().to_dict())
print("invalid:", int((~rx.geometry.is_valid).sum()))
print("empty:", int(rx.geometry.is_empty.sum()))
print("null rates:", int(rx["rate"].isna().sum()) if "rate" in rx else "no rate column")
print("overlaps:", len(gpd.sjoin(rx, rx, predicate="overlaps")) // 2)

A rate column stored as text is the single commonest cause of "it applied a constant rate": the terminal cannot parse it, falls back to its default, and reports no error.

Triage of six prescription file failures and the check that identifies each.
Six failures; every one is visible in the file before it reaches the terminal.

Step-by-step solution

1. Match the symptom to the cause

symptom usual cause
will not import at all format or version the terminal does not read
imports, field is empty wrong CRS, or geometry far from the machine's position
imports, constant rate applied rate column missing, misnamed, or stored as text
rates look scrambled overlapping polygons, or multipart geometry
rate applied only in part of the field gaps falling through to the default
machine oscillates polygons finer than the controller's response

2. Check the coordinate system

Most terminals expect WGS84 geographic coordinates. Some accept a projected national grid. A shapefile with no .prj is read as whatever the terminal assumes, which puts the field somewhere else entirely.

3. Check the rate column's name and type

Shapefile truncates field names to ten characters, so prescription_rate becomes prescripti. Some terminals look for a specific name. And a rate written as a string โ€” because the column contained a None and pandas made it object dtype โ€” is unparseable.

rx["rate"] = gpd.pd.to_numeric(rx["rate"], errors="coerce")
assert rx["rate"].notna().all(), "non-numeric rates"

4. Check the geometry

Valid, single-part, non-overlapping polygons with no holes where you did not intend them. Multipart polygons are handled inconsistently, and self-intersections cause a terminal to skip a feature silently.

5. Check for gaps

Any part of the field with no polygon gets the controller's default rate โ€” often zero, sometimes the last value used. Fill the gaps explicitly rather than relying on it.

6. Check the resolution against the machine

A polygon narrower than the distance the machine travels during the actuator's response โ€” speed times lag, typically 5โ€“15 m โ€” causes the rate to change and change back before it takes effect.

7. Test with a small file first

One field, three zones, on the actual terminal, before a whole farm. The failure modes are terminal-specific and no amount of specification reading substitutes for a test.

Checklist of the file-level checks to run before handing a prescription to a terminal.
Eight checks, all runnable in Python, none of which the terminal will report.

Code examples

Example 1 โ€” the pre-flight check

import numpy as np, geopandas as gpd

def preflight(path, rate_col="rate", expect_crs=4326, working_width_m=24,
              min_area_ha=0.1, area_crs=28992):
    rx = gpd.read_file(path)
    problems = []

    if rx.crs is None:
        problems.append("no CRS โ€” a shapefile with no .prj is read as an assumption")
    elif expect_crs and rx.crs.to_epsg() != expect_crs:
        problems.append(f"CRS is EPSG:{rx.crs.to_epsg()}, expected EPSG:{expect_crs}")

    if rate_col not in rx.columns:
        near = [c for c in rx.columns if c.lower().startswith(rate_col[:6].lower())]
        problems.append(f"no '{rate_col}' column; closest are {near or rx.columns.tolist()}")
    else:
        if not np.issubdtype(rx[rate_col].dtype, np.number):
            problems.append(f"'{rate_col}' is {rx[rate_col].dtype}, not numeric")
        if rx[rate_col].isna().any():
            problems.append(f"{int(rx[rate_col].isna().sum())} null rates")

    if (~rx.geometry.is_valid).any():
        problems.append(f"{int((~rx.geometry.is_valid).sum())} invalid geometries")
    if rx.geometry.is_empty.any():
        problems.append(f"{int(rx.geometry.is_empty.sum())} empty geometries")
    if (rx.geom_type == "MultiPolygon").any():
        problems.append(f"{int((rx.geom_type == 'MultiPolygon').sum())} multipart polygons")

    overlaps = len(gpd.sjoin(rx, rx, predicate="overlaps")) // 2
    if overlaps:
        problems.append(f"{overlaps} overlapping polygon pairs")

    m = rx.to_crs(area_crs)
    tiny = int((m.area < min_area_ha * 1e4).sum())
    if tiny:
        problems.append(f"{tiny} polygons under {min_area_ha} ha")
    narrow = int((m.area / m.length < working_width_m / 4).sum())
    if narrow:
        problems.append(f"{narrow} polygons narrower than the implement can follow")

    for p in problems:
        print("!", p)
    if not problems:
        print("preflight passed")
    return problems

area / length is a crude width proxy and it catches the ribbons and slivers that make a controller oscillate.

Example 2 โ€” repair the common failures

import numpy as np, geopandas as gpd

def repair_prescription(rx, rate_col="rate", crs_out=4326, working_width_m=24,
                        min_area_ha=0.1, area_crs=28992, rate_step=None):
    rx = rx.copy()
    rx[rate_col] = gpd.pd.to_numeric(rx[rate_col], errors="coerce")
    if rate_step:
        rx[rate_col] = (rx[rate_col] / rate_step).round() * rate_step

    rx["geometry"] = rx.geometry.buffer(0)
    rx = rx[rx.geometry.notna() & ~rx.geometry.is_empty]
    rx = rx.explode(index_parts=False)

    m = rx.to_crs(area_crs)
    m["geometry"] = m.geometry.simplify(working_width_m / 2).buffer(0)
    m = m[m.area >= min_area_ha * 1e4]

    # dissolve by rate so adjacent equal-rate polygons become one
    m = m.dissolve(by=rate_col, as_index=False).explode(index_parts=False)
    m = m[m.area >= min_area_ha * 1e4]

    out = m.to_crs(crs_out)
    out = out[[rate_col, "geometry"]]
    print(f"{len(rx)} โ†’ {len(out)} polygons; rates "
          f"{sorted(out[rate_col].unique())}")
    return out

Dissolving by rate is worth doing: adjacent polygons with the same rate are a boundary the controller has to cross for no reason.

Example 3 โ€” write in more than one format

import geopandas as gpd, pathlib

def export_prescription(rx, stem="prescription", rate_col="rate"):
    out = pathlib.Path(stem).parent
    out.mkdir(parents=True, exist_ok=True)

    # shapefile: the widest-supported interchange, ten-character field names
    short = rx.rename(columns={rate_col: rate_col[:10]})
    short.to_file(f"{stem}.shp")

    # GeoJSON for inspection and for web tools
    rx.to_file(f"{stem}.geojson", driver="GeoJSON", COORDINATE_PRECISION=7)

    for p in sorted(pathlib.Path(".").glob(f"{stem}.*")):
        print(f"{p.name:24} {p.stat().st_size / 1024:8.1f} KB")
    print("check the terminal's required format before relying on any of these")

Shipping a shapefile and a GeoJSON costs nothing and means the operator has an alternative when the first one fails at the gate.

Explanation

Why a text rate column produces a constant rate

A terminal reads the attribute table and looks for a numeric field. A column that pandas typed as object โ€” because it contained a None, a blank or a formatted string โ€” is written to the shapefile as text. The terminal finds no numeric rate, falls back to the default configured on the machine, and applies it everywhere. Nothing errors, and the map looks fine on the screen.

Why missing CRS information puts the field in the wrong place

A shapefile's coordinate system lives in the .prj sidecar. Without it, a terminal assumes something โ€” usually WGS84 โ€” and a prescription written in a national grid with easting values in the hundreds of thousands is then interpreted as degrees, placing it far outside any valid range. The field appears empty because the machine is nowhere near it.

Why overlaps scramble the rates

When two polygons cover the same ground, the controller has to choose. Different implementations choose the first, the last, the largest or the highest rate, and nothing in the file says which. The applied pattern then depends on the order features happen to be stored in, which is why an overlap can behave differently on two terminals reading the same file.

Why polygons finer than the response distance cause oscillation

A rate change is a mechanical action with a lag โ€” a valve, a gate, a motor. During that lag the machine travels several metres. A polygon narrower than that distance is entered and left before the rate has changed, so the controller is permanently chasing a target it never reaches, and the applied rate is an average with no relation to the map.

Two panels contrasting a numeric prescription rate column with one stored as text because it contained a null.
A single null turns the column to object dtype and the rate to a string.

Edge cases or notes

  • Ask for the terminal's format before exporting anything.
  • Shapefile truncates names to ten characters, silently.
  • Test with one small file on the actual machine.
  • Units are not in the file. kg/ha and l/ha look identical.
  • Some terminals cap the number of polygons. Dissolve by rate.
  • A .prj is not optional. Ship the whole shapefile set.
  • ISOXML is the standard and is what most modern terminals prefer.
  • Keep the as-applied data. It is the only evidence of what actually happened.

FAQ

Why does my prescription apply a constant rate?

Usually the rate column is stored as text, or its name was truncated to ten characters by the shapefile driver. The terminal finds no numeric rate and uses its default.

Why does the field appear empty on the terminal?

A missing or wrong CRS. Without a .prj, a national-grid prescription is read as degrees and lands outside any valid range.

Why are the applied rates scrambled?

Overlapping polygons. Different controllers resolve an overlap differently, and nothing in the file says which one wins.

Why is the machine oscillating between rates?

Polygons narrower than the distance travelled during the actuator's response โ€” typically 5โ€“15 m. Simplify to about half the working width.

What format should I use?

Whatever the terminal wants. Shapefile is the most widely accepted interchange; ISOXML is the standard modern terminals prefer.

How do I avoid all of this?

Run a file-level pre-flight โ€” CRS, column name and type, validity, single-part, no overlaps, no gaps, minimum size โ€” and test one small file on the actual machine first.