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.
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.
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.
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
.prjis 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.
Internal links
- How to write a variable-rate prescription map โ building the file correctly
- Management zones explained โ where the polygons come from
- Shapefile column names are truncated โ the ten-character limit
- How to fix invalid geometries in GeoPandas โ the
buffer(0)repair - How to fix gaps and overlaps in a polygon coverage โ making the coverage clean
- How to simplify geometry in GeoPandas โ simplifying to the implement
- Mixed geometry types cause a save error โ multipart problems
- How to package GIS deliverables in Python โ shipping the whole set
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.