Debugging Prescription Export Errors
A variable-rate prescription that renders perfectly in Python can still be rejected the instant it reaches the display in the cab. The zone map looks right, the rates are agronomically sound, and yet the John Deere GreenStar monitor throws “invalid boundary,” the Trimble display silently drops half the zones, or the ISOXML import bar hangs at 0%. This page is a diagnosis-to-fix reference for those failures: the invalid shapely geometries a controller’s topology engine refuses, the sliver polygons and zero-area zones that survive a dissolve, non-closed rings, multipart-versus-singlepart mismatches, wrong CRS and units, shapefile attribute-schema traps, and ISOXML (ISO 11783-10) schema validation errors. Each class of failure gets a reproducible symptom, a shapely/geopandas/lxml check, and a concrete repair that leaves the file loadable on real equipment.
This page is part of the Yield Mapping & Variable Rate Prescription Generation section. See that overview for the full pipeline from yield ingest through zone delineation to controller export; here we focus purely on what to do when the export step fails. Two focused companion guides go deeper on the two most common blockers — fixing shapely invalid geometry errors and resolving ISOXML schema validation failures.
Prerequisites
Python packages (exact versions tested):
shapely>=2.0,<2.1geopandas==0.14.4pyproj==3.6.1fiona==1.9.6lxml==5.2.2xmlschema==3.3.1libxml2command-line tools (xmllint) for a fast first-pass XSD check
Install with:
pip install "shapely>=2.0,<2.1" geopandas==0.14.4 pyproj==3.6.1 fiona==1.9.6 lxml==5.2.2 xmlschema==3.3.1
Input requirements:
- A prescription as a
GeoDataFrameofPolygon/MultiPolygonzones with a numeric rate column, built in a projected metric CRS (for example EPSG:32615, UTM zone 15N) so that area and buffer math is in metres. Building zones in a geographic CRS silently corrupts min-area filters — see understanding CRS in precision agriculture. - The target controller and format: John Deere GreenStar / Operations Center, Trimble, Raven, or CNH, delivered as either a WGS84 shapefile or an ISO 11783-10 ISOXML
TASKDATA.XML. - For ISOXML validation, a copy of the ISO 11783-10 XSD set (the
ISO11783_TaskFile_V4.xsdand its imported schemas). Vendor SDKs ship these; keep them in a known local directory.
1. The Failure Landscape
Prescription export sits at the boundary between a permissive GIS world and an unforgiving embedded one. In Python, geopandas and QGIS will happily hold, render, and even reproject geometries that violate the OGC simple-features rules. Equipment controllers will not. The John Deere, Trimble, Raven, and CNH import paths each run their own topology and schema validation, and they fail closed: a single bad zone can reject the whole file, and the on-screen error (“invalid boundary,” “no data in field,” a stalled progress bar) rarely names the offending feature.
Six failure classes cover almost every rejected export:
Invalid geometry. Self-intersecting rings (bow-ties), ring self-tangency, and duplicate consecutive vertices make shapely’s is_valid return False. Controllers reject these outright because their point-in-polygon rate lookup is undefined on a self-intersecting boundary. This is the single most common blocker and has its own deep-dive in fixing shapely invalid geometry errors.
Slivers and zero-area zones. A dissolve or overlay operation across near-coincident boundaries leaves hairline sliver polygons and, occasionally, zones with effectively zero area. These pass is_valid but represent no treatable ground; a controller may reject a zero-area ring or, worse, assign it a rate and thrash the applicator on and off as the machine clips its edge.
Non-closed rings. A LinearRing whose first and last coordinate differ by a floating-point epsilon is invalid per the shapefile and ISOXML specs. shapely auto-closes on construction, but rings assembled by hand or round-tripped through a lossy intermediate can arrive open.
Multipart versus singlepart. Some controllers accept MultiPolygon zone features; others require every feature to be a singlepart Polygon and silently drop the extra parts of a multipart record, so a two-island zone loses one island’s rate. Knowing your controller’s expectation and exploding accordingly is mandatory.
Wrong CRS or units. Zones authored in UTM but exported without reprojection land on the far side of the planet in a WGS84-expecting importer. Even with the right CRS, a rate expressed in the wrong unit (L/ha versus gal/ac, seeds/m² versus seeds/ac) produces a valid file that applies a catastrophically wrong rate.
Attribute-schema failures. The shapefile DBF format truncates field names to 10 characters and coerces dtypes; a rate stored as a string, or a rate_lbs_per_acre column collapsing to rate_lbs_p, breaks the controller’s column mapping. ISOXML has the analogous problem in structured form: missing or dangling references between the process-data, product, and treatment-zone elements.
ISOXML schema failures. An ISO 11783-10 TASKDATA.XML that omits a referenced ProcessDataVariable (PDT/PGP), points a TreatmentZone (TZN) at a non-existent process-data or device element, or declares a unit that does not match the value definition will fail XSD validation and IDREF resolution. These are covered end-to-end in resolving ISOXML schema validation failures.
The rest of this page is the ordered gate that catches all six before the file leaves your machine.
2. Step-by-Step Triage
Step 1 — Reproduce the failure and read the raw geometry
Before touching the exporter, confirm which features are actually invalid. shapely 2.x vectorises the validity check, and explain_validity names the failure and its coordinate, which is far more actionable than the controller’s generic error.
import geopandas as gpd
from shapely.validation import explain_validity
# Prescription built in a projected metric CRS (UTM 15N)
rx = gpd.read_file("prescription_zones.gpkg")
assert rx.crs is not None, "Prescription has no CRS — cannot proceed"
assert rx.crs.is_projected, (
f"Expected a projected CRS for area math, got EPSG:{rx.crs.to_epsg()}"
)
invalid = rx[~rx.geometry.is_valid]
print(f"{len(invalid)} of {len(rx)} zones are invalid")
for idx, geom in invalid.geometry.items():
print(f" zone {idx}: {explain_validity(geom)}")
A typical line reads Self-intersection[500123.4 4213880.7], pinpointing the bow-tie vertex. Record the invalid count; you will assert it drops to zero after repair.
Step 2 — Repair geometry and drop slivers
Use make_valid for a topology-preserving repair and reserve buffer(0) for the specific case of a self-intersecting outer ring. After repair, filter slivers by a minimum-area threshold expressed in the projected CRS’s square metres, and drop any zone that has collapsed to near-zero area.
from shapely import make_valid
from shapely.geometry import Polygon, MultiPolygon
MIN_ZONE_AREA_M2 = 200.0 # ~0.02 ha; smaller zones are not treatable
def repair_geom(geom):
"""Make a single geometry valid and keep only polygonal parts."""
if geom is None or geom.is_empty:
return None
fixed = make_valid(geom) if not geom.is_valid else geom
# make_valid can emit GeometryCollections; keep polygonal parts only
if fixed.geom_type == "GeometryCollection":
polys = [g for g in fixed.geoms if g.geom_type in ("Polygon", "MultiPolygon")]
if not polys:
return None
fixed = MultiPolygon(
[p for g in polys for p in (g.geoms if g.geom_type == "MultiPolygon" else [g])]
)
return fixed
rx["geometry"] = rx.geometry.apply(repair_geom)
rx = rx[rx.geometry.notna()].copy()
# Drop slivers and zero-area zones (area is metres² in a projected CRS)
rx = rx[rx.geometry.area >= MIN_ZONE_AREA_M2].copy()
assert rx.geometry.is_valid.all(), "Invalid geometry survived repair"
assert (rx.geometry.area >= MIN_ZONE_AREA_M2).all(), "Sliver survived filter"
print(f"{len(rx)} valid zones remain after repair and sliver removal")
The make_valid versus buffer(0) trade-off, ring self-tangency, and snapping duplicate vertices are worked in full detail in the companion geometry-repair guide.
Step 3 — Fix CRS, units, and multipart structure
Do all area and buffer math in the projected CRS, then reproject to the controller’s expected CRS — usually WGS84 (EPSG:4326) — as the final geometric step. Decide singlepart versus multipart per your controller and explode if needed.
import numpy as np
EXPORT_EPSG = 4326 # most controllers expect geographic WGS84 on import
SINGLEPART_ONLY = True # set False if the controller accepts MultiPolygon zones
# Round rates to the controller's resolution BEFORE reprojection so the
# numeric attribute is stable regardless of geometry precision
rx["rate"] = np.round(rx["rate"].astype("float64"), 1)
rx_out = rx.to_crs(f"EPSG:{EXPORT_EPSG}")
if SINGLEPART_ONLY:
rx_out = rx_out.explode(index_parts=False, ignore_index=True)
assert rx_out.crs.to_epsg() == EXPORT_EPSG
if SINGLEPART_ONLY:
assert (rx_out.geometry.geom_type == "Polygon").all(), "Multipart survived explode"
print(f"Reprojected to EPSG:{EXPORT_EPSG}; {len(rx_out)} export features")
Units are a data problem, not a geometry one: confirm the rate column is in the unit the controller’s product definition expects, and record that unit explicitly (an attribute tag or the ISOXML ProcessDataVariable unit) rather than relying on a filename convention.
Step 4 — Enforce the attribute schema, then write
Shapefile field names cap at 10 characters. Rename to short, explicit names and confirm the rate is a numeric double before writing, so the DBF does not silently truncate or coerce it.
# Short, DBF-safe field names (<= 10 chars) mapped to controller columns
rx_out = rx_out.rename(columns={"rate": "RATE"})
KEEP = ["RATE", "geometry"]
rx_out = rx_out[KEEP]
for name in rx_out.columns:
if name != "geometry":
assert len(name) <= 10, f"Field '{name}' exceeds the 10-char DBF limit"
assert rx_out["RATE"].dtype.kind == "f", "RATE must be a numeric double, not a string"
rx_out.to_file("prescription_wgs84.shp", driver="ESRI Shapefile")
print("Wrote prescription_wgs84.shp")
For a fuller battery of shapefile-specific checks — encoding, .prj presence, geometry-type consistency — route the file through shapefile validation for farm equipment and, when zones still misbehave in the field, debugging shapefile geometry errors in QGIS and Python.
Step 5 — Validate the ISOXML task file against the ISO 11783-10 XSD
If the target is ISOXML rather than a shapefile, the generated TASKDATA.XML must validate against the ISO 11783-10 schema and resolve every internal reference. A fast first pass with xmllint catches structural errors before you invest in Python-side diagnostics.
xmllint --noout --schema ISO11783_TaskFile_V4.xsd TASKDATA/TASKDATA.XML
import xmlschema
schema = xmlschema.XMLSchema("ISO11783_TaskFile_V4.xsd")
try:
schema.validate("TASKDATA/TASKDATA.XML")
print("ISOXML valid against ISO 11783-10")
except xmlschema.XMLSchemaValidationError as exc:
# exc.reason and exc.path localise the failing element/attribute
print(f"Schema error at {exc.path}: {exc.reason}")
Interpreting the specific errors — a TreatmentZone (TZN) pointing at a missing ProcessDataVariable (PDV), an unresolved device-element IDREF, or a unit mismatch between the treatment zone and its process data — plus the generator-side fixes, is the whole subject of resolving ISOXML schema validation failures and the broader variable rate export to ISOXML guide.
3. Key Parameters & Tuning
| Parameter | Type | Default | Agronomic Effect |
|---|---|---|---|
make_valid vs buffer(0) |
strategy | make_valid |
make_valid preserves topology and rate boundaries; buffer(0) is faster but can shave slivers off zone edges, shifting a rate boundary by centimetres. Prefer make_valid for prescriptions where the zone line marks a real product change. |
MIN_ZONE_AREA_M2 |
float | 200.0 | Minimum treatable zone (~0.02 ha). Raise to 500–1000 for wide-boom applicators that cannot resolve small zones; too low leaves slivers that toggle the applicator on and off along a boundary. |
snap_tolerance |
float (m) | 0.05 | Vertex-snap distance used to close near-coincident rings and remove duplicate points before validation. Above ~0.5 m it starts to move genuine zone boundaries; keep it below the controller’s positioning resolution. |
rate_decimals |
int | 1 | Decimal places the rate is rounded to before export. Match the controller’s rate resolution; over-precise rates bloat the file and can round-trip differently than displayed. |
EXPORT_EPSG |
int | 4326 | Delivery CRS. Almost all controllers ingest WGS84; exporting in UTM to a WGS84-expecting importer places zones off-planet. Reproject only after all metric math is done. |
SINGLEPART_ONLY |
bool | True | Explode MultiPolygon zones to singlepart when the controller drops extra parts of a multipart feature. Leaving it False on such a controller silently loses a zone’s outlying islands. |
field_name_len |
int | 10 | Hard DBF limit for shapefile attribute names. Names above 10 chars truncate silently and break the controller’s column mapping; rename before writing. |
4. Edge Cases & Failure Modes
A dissolve produces valid-but-untreatable slivers. Dissolving adjacent same-rate zones across boundaries that were digitised twice leaves hairline polygons a few centimetres wide. They pass is_valid, so the geometry gate misses them; only the MIN_ZONE_AREA_M2 filter removes them. Always run the area filter after any dissolve or overlay, never before.
make_valid returns a GeometryCollection. Repairing a bow-tie can split one polygon into a polygon plus a stray LineString. Writing a GeometryCollection to a shapefile fails or drops the line silently. The repair_geom function above keeps only polygonal parts; never write a raw make_valid result without that filter.
CRS is present but wrong. A file tagged EPSG:4326 whose coordinates are actually UTM metres will “validate” as having a CRS while plotting near latitude 4,000,000. Sanity-check that WGS84 coordinates fall within plausible longitude/latitude bounds before delivery, not just that a CRS is set. The validating coordinate systems for variable-rate maps guide covers these bound checks.
Rate stored as a string. Reading a prescription from CSV or GeoJSON often yields an object-dtype rate column. Written to a DBF it becomes a text field, and the controller — expecting a numeric product rate — either ignores it or applies zero. The dtype.kind == "f" assertion in Step 4 catches this.
ISOXML validates but the reference graph is broken. XSD validation confirms the document’s shape, not that every IDREF resolves. A TreatmentZone can reference a ProcessDataVariable DDI that no device on the machine reports, so the file loads but no rate is applied. Cross-check IDREFs explicitly, as shown in the ISOXML validation guide.
Multipart headland zone. A single “headland” zone that wraps a field as two disconnected strips is a MultiPolygon. On a singlepart-only controller, exploding it is correct — but each exploded part inherits the same rate, which is usually what you want. Verify the rate copied to every part after explode.
5. Verification & Output Validation
Before shipping, run one consolidated gate that reproduces every check above and fails loudly on any regression. This is the function to wire into CI or a pre-export hook.
import geopandas as gpd
def validate_prescription_export(path: str, export_epsg: int = 4326,
min_area_m2: float = 200.0,
singlepart_only: bool = True,
max_field_len: int = 10) -> dict:
"""Fail-closed validation of a prescription file before it reaches a controller."""
gdf = gpd.read_file(path)
assert gdf.crs is not None, "No CRS on export file"
assert gdf.crs.to_epsg() == export_epsg, (
f"Export CRS EPSG:{gdf.crs.to_epsg()} != expected EPSG:{export_epsg}"
)
assert gdf.geometry.is_valid.all(), "Invalid geometry in export file"
assert not gdf.geometry.is_empty.any(), "Empty geometry in export file"
if singlepart_only:
assert (gdf.geometry.geom_type == "Polygon").all(), "Multipart feature present"
for name in gdf.columns:
if name != "geometry":
assert len(name) <= max_field_len, f"Field '{name}' exceeds {max_field_len} chars"
rate_cols = [c for c in gdf.columns if c.upper().startswith("RATE")]
assert rate_cols, "No RATE attribute found"
assert gdf[rate_cols[0]].dtype.kind in ("f", "i"), "Rate column is not numeric"
return {
"features": len(gdf),
"crs": f"EPSG:{gdf.crs.to_epsg()}",
"all_valid": True,
"rate_field": rate_cols[0],
}
print(validate_prescription_export("prescription_wgs84.shp"))
For ISOXML deliverables, the equivalent gate is the xmlschema validation from Step 5 combined with an explicit IDREF resolution pass. A file that clears both the geometry/attribute gate here and the ISOXML gate has cleared every failure class that commonly stops a load in the cab.
6. Integration with the Pipeline
This troubleshooting gate sits at the very end of the prescription pipeline, immediately before delivery.
Upstream dependency. The zones arriving here come from management zone classification algorithms run on interpolated yield surfaces. Repairing geometry after zone delineation is cheaper than re-clustering, but a persistently invalid zone often signals a dissolve or overlay bug upstream worth fixing at the source.
Shapefile delivery path. When the target is a shapefile for Trimble, Raven, or CNH, hand off to shapefile validation for farm equipment for the full write-side battery, and to exporting prescription maps to John Deere GreenStar format for the GreenStar-specific field layout.
ISOXML delivery path. When the target is ISO 11783-10, variable rate export to ISOXML builds the TASKDATA.XML, and resolving ISOXML schema validation failures closes the loop when that build fails validation.
Geometry-first delivery path. The most common single blocker — an invalid shapely geometry — has its own end-to-end repair script in fixing shapely invalid geometry errors; start there when the controller error mentions “invalid boundary.”
Frequently Asked Questions
Why does the controller reject a prescription that opens fine in QGIS?
QGIS renders many technically invalid geometries without complaint, but equipment controllers run a strict topology check and refuse self-intersecting rings, zero-area slivers, and non-closed polygons. Run shapely is_valid and explain_validity on every zone before export and repair anything that fails, because the on-screen preview is not a validity guarantee.
Which CRS should a variable-rate prescription use for export?
Most controllers expect geographic WGS84 (EPSG:4326) in the delivered shapefile or ISOXML, even though you build zones in a projected metric CRS such as a UTM zone. Do your area and buffer math in the projected CRS, then reproject the final geometry to EPSG:4326 for the export step, and confirm the rate units match what the controller expects.
Why is my rate column truncated or missing after writing a shapefile?
The shapefile DBF format caps field names at 10 characters and silently truncates longer ones, so a column like prescription_rate becomes prescripti and downstream lookups fail. Rename attributes to short explicit names before writing and store the rate as a numeric double, not a string, so the controller can parse it.
Related
- Fixing Shapely Invalid Geometry Errors — end-to-end repair of self-intersections, ring tangency, and duplicate points with make_valid and buffer(0)
- Resolving ISOXML Schema Validation Failures — validating TASKDATA.XML against the ISO 11783-10 XSD and fixing dangling PDV/TZN references
- Shapefile Validation for Farm Equipment — the full write-side validation battery for Trimble, Raven, and CNH shapefile deliverables
- Debugging Shapefile Geometry Errors in QGIS and Python — QGIS-side diagnosis of invalid boundary polygons that fail on import
- Exporting Prescription Maps to John Deere GreenStar Format — the GreenStar and Operations Center field layout and delivery structure