argleton 0.2.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (260) hide show
  1. {argleton-0.2.0 → argleton-0.4.0}/.github/workflows/ci.yml +21 -2
  2. argleton-0.4.0/.zenodo.json +19 -0
  3. {argleton-0.2.0 → argleton-0.4.0}/CITATION.cff +2 -2
  4. {argleton-0.2.0 → argleton-0.4.0}/PKG-INFO +66 -33
  5. {argleton-0.2.0 → argleton-0.4.0}/README.md +65 -32
  6. {argleton-0.2.0 → argleton-0.4.0}/adapters/engine_geopandas.py +90 -0
  7. {argleton-0.2.0 → argleton-0.4.0}/adapters/engine_naive.py +115 -0
  8. {argleton-0.2.0 → argleton-0.4.0}/adapters/engine_rasterio.py +61 -0
  9. argleton-0.4.0/adapters/engine_whitebox.py +80 -0
  10. {argleton-0.2.0 → argleton-0.4.0}/adapters/mapsmith.py +235 -0
  11. argleton-0.4.0/clean/c023-longitude-first/build.py +44 -0
  12. argleton-0.4.0/clean/c023-longitude-first/probe.toml +43 -0
  13. argleton-0.4.0/clean/c024-pixel-is-area/build.py +82 -0
  14. argleton-0.4.0/clean/c024-pixel-is-area/probe.toml +52 -0
  15. argleton-0.4.0/clean/c025-single-hemisphere-zone/build.py +95 -0
  16. argleton-0.4.0/clean/c025-single-hemisphere-zone/probe.toml +46 -0
  17. argleton-0.4.0/clean/c026-north-up-grid/build.py +64 -0
  18. argleton-0.4.0/clean/c026-north-up-grid/probe.toml +42 -0
  19. argleton-0.4.0/clean/c027-lines-only/build.py +60 -0
  20. argleton-0.4.0/clean/c027-lines-only/probe.toml +44 -0
  21. argleton-0.4.0/clean/c028-projected-field/build.py +57 -0
  22. argleton-0.4.0/clean/c028-projected-field/probe.toml +48 -0
  23. argleton-0.4.0/clean/c029-coincident-boundaries/build.py +49 -0
  24. argleton-0.4.0/clean/c029-coincident-boundaries/probe.toml +49 -0
  25. argleton-0.4.0/clean/c030-single-georeferencing/build.py +36 -0
  26. argleton-0.4.0/clean/c030-single-georeferencing/probe.toml +39 -0
  27. {argleton-0.2.0 → argleton-0.4.0}/docs/FAMILIES.md +45 -4
  28. {argleton-0.2.0 → argleton-0.4.0}/pyproject.toml +1 -1
  29. argleton-0.4.0/results/2026-08-29-axis-order/adapters-geopandas.json +575 -0
  30. argleton-0.4.0/results/2026-08-29-axis-order/adapters-mapsmith.json +574 -0
  31. argleton-0.4.0/results/2026-08-29-axis-order/adapters-naive.json +574 -0
  32. argleton-0.4.0/results/2026-08-29-axis-order/adapters-rasterio.json +574 -0
  33. argleton-0.4.0/results/2026-08-29-axis-order/adapters-whitebox.json +575 -0
  34. argleton-0.4.0/results/2026-08-30-all-families/adapters-geopandas.json +719 -0
  35. argleton-0.4.0/results/2026-08-30-all-families/adapters-mapsmith.json +718 -0
  36. argleton-0.4.0/results/2026-08-30-all-families/adapters-naive.json +718 -0
  37. argleton-0.4.0/results/2026-08-30-all-families/adapters-rasterio.json +718 -0
  38. argleton-0.4.0/results/2026-08-30-all-families/adapters-whitebox.json +719 -0
  39. argleton-0.4.0/results/2026-08-30-antimeridian/adapters-geopandas.json +623 -0
  40. argleton-0.4.0/results/2026-08-30-antimeridian/adapters-mapsmith.json +622 -0
  41. argleton-0.4.0/results/2026-08-30-antimeridian/adapters-naive.json +622 -0
  42. argleton-0.4.0/results/2026-08-30-antimeridian/adapters-rasterio.json +622 -0
  43. argleton-0.4.0/results/2026-08-30-antimeridian/adapters-whitebox.json +623 -0
  44. argleton-0.4.0/results/2026-08-30-grid-registration/adapters-geopandas.json +599 -0
  45. argleton-0.4.0/results/2026-08-30-grid-registration/adapters-mapsmith.json +598 -0
  46. argleton-0.4.0/results/2026-08-30-grid-registration/adapters-naive.json +598 -0
  47. argleton-0.4.0/results/2026-08-30-grid-registration/adapters-rasterio.json +598 -0
  48. argleton-0.4.0/results/2026-08-30-grid-registration/adapters-whitebox.json +599 -0
  49. argleton-0.4.0/results/LATEST +1 -0
  50. {argleton-0.2.0 → argleton-0.4.0}/results/README.md +225 -2
  51. {argleton-0.2.0 → argleton-0.4.0}/site/index.template.html +3 -2
  52. {argleton-0.2.0 → argleton-0.4.0}/tests/test_showcase.py +194 -5
  53. argleton-0.4.0/traps/023-axis-order/README.md +130 -0
  54. argleton-0.4.0/traps/023-axis-order/build.py +57 -0
  55. argleton-0.4.0/traps/023-axis-order/probe.toml +110 -0
  56. argleton-0.4.0/traps/024-pixel-is-point/README.md +111 -0
  57. argleton-0.4.0/traps/024-pixel-is-point/build.py +78 -0
  58. argleton-0.4.0/traps/024-pixel-is-point/probe.toml +137 -0
  59. argleton-0.4.0/traps/025-antimeridian-zone/README.md +92 -0
  60. argleton-0.4.0/traps/025-antimeridian-zone/build.py +108 -0
  61. argleton-0.4.0/traps/025-antimeridian-zone/probe.toml +134 -0
  62. argleton-0.4.0/traps/026-south-up-grid/README.md +101 -0
  63. argleton-0.4.0/traps/026-south-up-grid/build.py +83 -0
  64. argleton-0.4.0/traps/026-south-up-grid/probe.toml +129 -0
  65. argleton-0.4.0/traps/027-mixed-geometry/README.md +60 -0
  66. argleton-0.4.0/traps/027-mixed-geometry/build.py +79 -0
  67. argleton-0.4.0/traps/027-mixed-geometry/probe.toml +107 -0
  68. argleton-0.4.0/traps/028-degrees-as-metres/README.md +82 -0
  69. argleton-0.4.0/traps/028-degrees-as-metres/build.py +71 -0
  70. argleton-0.4.0/traps/028-degrees-as-metres/probe.toml +122 -0
  71. argleton-0.4.0/traps/029-difference-order/README.md +66 -0
  72. argleton-0.4.0/traps/029-difference-order/build.py +61 -0
  73. argleton-0.4.0/traps/029-difference-order/probe.toml +112 -0
  74. argleton-0.4.0/traps/030-sidecar-georeferencing/README.md +79 -0
  75. argleton-0.4.0/traps/030-sidecar-georeferencing/build.py +93 -0
  76. argleton-0.4.0/traps/030-sidecar-georeferencing/probe.toml +95 -0
  77. argleton-0.2.0/adapters/engine_whitebox.py +0 -34
  78. argleton-0.2.0/results/LATEST +0 -1
  79. {argleton-0.2.0 → argleton-0.4.0}/.gitattributes +0 -0
  80. {argleton-0.2.0 → argleton-0.4.0}/.github/workflows/pages.yml +0 -0
  81. {argleton-0.2.0 → argleton-0.4.0}/.gitignore +0 -0
  82. {argleton-0.2.0 → argleton-0.4.0}/LICENSE +0 -0
  83. {argleton-0.2.0 → argleton-0.4.0}/adapters/__init__.py +0 -0
  84. {argleton-0.2.0 → argleton-0.4.0}/adapters/gis_mcp.py +0 -0
  85. {argleton-0.2.0 → argleton-0.4.0}/argleton/__init__.py +0 -0
  86. {argleton-0.2.0 → argleton-0.4.0}/argleton/model.py +0 -0
  87. {argleton-0.2.0 → argleton-0.4.0}/argleton/published.py +0 -0
  88. {argleton-0.2.0 → argleton-0.4.0}/argleton/run.py +0 -0
  89. {argleton-0.2.0 → argleton-0.4.0}/argleton/score.py +0 -0
  90. {argleton-0.2.0 → argleton-0.4.0}/clean/c001-raster-mean/build.py +0 -0
  91. {argleton-0.2.0 → argleton-0.4.0}/clean/c001-raster-mean/probe.toml +0 -0
  92. {argleton-0.2.0 → argleton-0.4.0}/clean/c002-projected-area/build.py +0 -0
  93. {argleton-0.2.0 → argleton-0.4.0}/clean/c002-projected-area/probe.toml +0 -0
  94. {argleton-0.2.0 → argleton-0.4.0}/clean/c003-raster-mean-nodata/build.py +0 -0
  95. {argleton-0.2.0 → argleton-0.4.0}/clean/c003-raster-mean-nodata/probe.toml +0 -0
  96. {argleton-0.2.0 → argleton-0.4.0}/clean/c004-points-in-polygon/build.py +0 -0
  97. {argleton-0.2.0 → argleton-0.4.0}/clean/c004-points-in-polygon/probe.toml +0 -0
  98. {argleton-0.2.0 → argleton-0.4.0}/clean/c005-polygon-area/build.py +0 -0
  99. {argleton-0.2.0 → argleton-0.4.0}/clean/c005-polygon-area/probe.toml +0 -0
  100. {argleton-0.2.0 → argleton-0.4.0}/clean/c006-named-layer/build.py +0 -0
  101. {argleton-0.2.0 → argleton-0.4.0}/clean/c006-named-layer/probe.toml +0 -0
  102. {argleton-0.2.0 → argleton-0.4.0}/clean/c007-distance-in-metres/build.py +0 -0
  103. {argleton-0.2.0 → argleton-0.4.0}/clean/c007-distance-in-metres/probe.toml +0 -0
  104. {argleton-0.2.0 → argleton-0.4.0}/clean/c008-equal-area-crs/build.py +0 -0
  105. {argleton-0.2.0 → argleton-0.4.0}/clean/c008-equal-area-crs/probe.toml +0 -0
  106. {argleton-0.2.0 → argleton-0.4.0}/clean/c009-native-resolution-classes/build.py +0 -0
  107. {argleton-0.2.0 → argleton-0.4.0}/clean/c009-native-resolution-classes/probe.toml +0 -0
  108. {argleton-0.2.0 → argleton-0.4.0}/clean/c010-physical-values/build.py +0 -0
  109. {argleton-0.2.0 → argleton-0.4.0}/clean/c010-physical-values/probe.toml +0 -0
  110. {argleton-0.2.0 → argleton-0.4.0}/clean/c011-solid-parcel/build.py +0 -0
  111. {argleton-0.2.0 → argleton-0.4.0}/clean/c011-solid-parcel/probe.toml +0 -0
  112. {argleton-0.2.0 → argleton-0.4.0}/clean/c012-disjoint-concessions/build.py +0 -0
  113. {argleton-0.2.0 → argleton-0.4.0}/clean/c012-disjoint-concessions/probe.toml +0 -0
  114. {argleton-0.2.0 → argleton-0.4.0}/clean/c013-flat-pipeline/build.py +0 -0
  115. {argleton-0.2.0 → argleton-0.4.0}/clean/c013-flat-pipeline/probe.toml +0 -0
  116. {argleton-0.2.0 → argleton-0.4.0}/clean/c014-convex-parcel/build.py +0 -0
  117. {argleton-0.2.0 → argleton-0.4.0}/clean/c014-convex-parcel/probe.toml +0 -0
  118. {argleton-0.2.0 → argleton-0.4.0}/clean/c015-wells-off-the-seam/build.py +0 -0
  119. {argleton-0.2.0 → argleton-0.4.0}/clean/c015-wells-off-the-seam/probe.toml +0 -0
  120. {argleton-0.2.0 → argleton-0.4.0}/clean/c016-decimal-degrees/build.py +0 -0
  121. {argleton-0.2.0 → argleton-0.4.0}/clean/c016-decimal-degrees/probe.toml +0 -0
  122. {argleton-0.2.0 → argleton-0.4.0}/clean/c017-equal-populations/build.py +0 -0
  123. {argleton-0.2.0 → argleton-0.4.0}/clean/c017-equal-populations/probe.toml +0 -0
  124. {argleton-0.2.0 → argleton-0.4.0}/clean/c018-plain-keys/build.py +0 -0
  125. {argleton-0.2.0 → argleton-0.4.0}/clean/c018-plain-keys/probe.toml +0 -0
  126. {argleton-0.2.0 → argleton-0.4.0}/clean/c019-fully-contained/build.py +0 -0
  127. {argleton-0.2.0 → argleton-0.4.0}/clean/c019-fully-contained/probe.toml +0 -0
  128. {argleton-0.2.0 → argleton-0.4.0}/clean/c020-one-owner-each/build.py +0 -0
  129. {argleton-0.2.0 → argleton-0.4.0}/clean/c020-one-owner-each/probe.toml +0 -0
  130. {argleton-0.2.0 → argleton-0.4.0}/clean/c021-greenwich-variant/build.py +0 -0
  131. {argleton-0.2.0 → argleton-0.4.0}/clean/c021-greenwich-variant/probe.toml +0 -0
  132. {argleton-0.2.0 → argleton-0.4.0}/clean/c022-two-gauges/build.py +0 -0
  133. {argleton-0.2.0 → argleton-0.4.0}/clean/c022-two-gauges/probe.toml +0 -0
  134. {argleton-0.2.0 → argleton-0.4.0}/docs/ADDING-A-TRAP.md +0 -0
  135. {argleton-0.2.0 → argleton-0.4.0}/docs/METHOD.md +0 -0
  136. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-23-first-run/adapters-mapsmith.json +0 -0
  137. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-23-first-run/engine-geopandas.json +0 -0
  138. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-23-first-run/engine-naive.json +0 -0
  139. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-23-first-run/engine-rasterio.json +0 -0
  140. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-23-first-run/engine-whitebox.json +0 -0
  141. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-24-five-families/adapters-mapsmith.json +0 -0
  142. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-24-five-families/engine-geopandas.json +0 -0
  143. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-24-five-families/engine-naive.json +0 -0
  144. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-24-five-families/engine-rasterio.json +0 -0
  145. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-24-five-families/engine-whitebox.json +0 -0
  146. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-25-eight-families/adapters-mapsmith.json +0 -0
  147. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-25-eight-families/engine-geopandas.json +0 -0
  148. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-25-eight-families/engine-naive.json +0 -0
  149. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-25-eight-families/engine-rasterio.json +0 -0
  150. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-25-eight-families/engine-whitebox.json +0 -0
  151. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-25-six-families/adapters-mapsmith.json +0 -0
  152. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-25-six-families/engine-geopandas.json +0 -0
  153. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-25-six-families/engine-naive.json +0 -0
  154. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-25-six-families/engine-rasterio.json +0 -0
  155. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-25-six-families/engine-whitebox.json +0 -0
  156. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-eighteen-families/adapters-geopandas.json +0 -0
  157. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-eighteen-families/adapters-mapsmith.json +0 -0
  158. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-eighteen-families/adapters-naive.json +0 -0
  159. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-eighteen-families/adapters-rasterio.json +0 -0
  160. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-eighteen-families/adapters-whitebox.json +0 -0
  161. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-nine-families/adapters-geopandas.json +0 -0
  162. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-nine-families/adapters-mapsmith.json +0 -0
  163. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-nine-families/adapters-naive.json +0 -0
  164. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-nine-families/adapters-rasterio.json +0 -0
  165. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-nine-families/adapters-whitebox.json +0 -0
  166. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-nineteen-families/adapters-geopandas.json +0 -0
  167. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-nineteen-families/adapters-mapsmith.json +0 -0
  168. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-nineteen-families/adapters-naive.json +0 -0
  169. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-nineteen-families/adapters-rasterio.json +0 -0
  170. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-nineteen-families/adapters-whitebox.json +0 -0
  171. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-ten-families/adapters-geopandas.json +0 -0
  172. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-ten-families/adapters-mapsmith.json +0 -0
  173. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-ten-families/adapters-naive.json +0 -0
  174. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-ten-families/adapters-rasterio.json +0 -0
  175. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-26-ten-families/adapters-whitebox.json +0 -0
  176. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-27-datum-shift-fixed/adapters-geopandas.json +0 -0
  177. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-27-datum-shift-fixed/adapters-mapsmith.json +0 -0
  178. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-27-datum-shift-fixed/adapters-naive.json +0 -0
  179. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-27-datum-shift-fixed/adapters-rasterio.json +0 -0
  180. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-27-datum-shift-fixed/adapters-whitebox.json +0 -0
  181. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-28-thiessen-pairing/adapters-geopandas.json +0 -0
  182. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-28-thiessen-pairing/adapters-mapsmith.json +0 -0
  183. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-28-thiessen-pairing/adapters-naive.json +0 -0
  184. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-28-thiessen-pairing/adapters-rasterio.json +0 -0
  185. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-28-thiessen-pairing/adapters-whitebox.json +0 -0
  186. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-28-timings/adapters-geopandas.json +0 -0
  187. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-28-timings/adapters-mapsmith.json +0 -0
  188. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-28-timings/adapters-naive.json +0 -0
  189. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-28-timings/adapters-rasterio.json +0 -0
  190. {argleton-0.2.0 → argleton-0.4.0}/results/2026-08-28-timings/adapters-whitebox.json +0 -0
  191. {argleton-0.2.0 → argleton-0.4.0}/schema/probe.schema.json +0 -0
  192. {argleton-0.2.0 → argleton-0.4.0}/schema/result.schema.json +0 -0
  193. {argleton-0.2.0 → argleton-0.4.0}/site/build.py +0 -0
  194. {argleton-0.2.0 → argleton-0.4.0}/tests/test_contracts.py +0 -0
  195. {argleton-0.2.0 → argleton-0.4.0}/traps/001-tiff-predictor/README.md +0 -0
  196. {argleton-0.2.0 → argleton-0.4.0}/traps/001-tiff-predictor/build.py +0 -0
  197. {argleton-0.2.0 → argleton-0.4.0}/traps/001-tiff-predictor/probe.toml +0 -0
  198. {argleton-0.2.0 → argleton-0.4.0}/traps/002-feet-as-metres/README.md +0 -0
  199. {argleton-0.2.0 → argleton-0.4.0}/traps/002-feet-as-metres/build.py +0 -0
  200. {argleton-0.2.0 → argleton-0.4.0}/traps/002-feet-as-metres/probe.toml +0 -0
  201. {argleton-0.2.0 → argleton-0.4.0}/traps/003-nodata-in-statistics/README.md +0 -0
  202. {argleton-0.2.0 → argleton-0.4.0}/traps/003-nodata-in-statistics/build.py +0 -0
  203. {argleton-0.2.0 → argleton-0.4.0}/traps/003-nodata-in-statistics/probe.toml +0 -0
  204. {argleton-0.2.0 → argleton-0.4.0}/traps/004-mismatched-crs-join/README.md +0 -0
  205. {argleton-0.2.0 → argleton-0.4.0}/traps/004-mismatched-crs-join/build.py +0 -0
  206. {argleton-0.2.0 → argleton-0.4.0}/traps/004-mismatched-crs-join/probe.toml +0 -0
  207. {argleton-0.2.0 → argleton-0.4.0}/traps/005-bowtie-area/README.md +0 -0
  208. {argleton-0.2.0 → argleton-0.4.0}/traps/005-bowtie-area/build.py +0 -0
  209. {argleton-0.2.0 → argleton-0.4.0}/traps/005-bowtie-area/probe.toml +0 -0
  210. {argleton-0.2.0 → argleton-0.4.0}/traps/006-default-layer/README.md +0 -0
  211. {argleton-0.2.0 → argleton-0.4.0}/traps/006-default-layer/build.py +0 -0
  212. {argleton-0.2.0 → argleton-0.4.0}/traps/006-default-layer/probe.toml +0 -0
  213. {argleton-0.2.0 → argleton-0.4.0}/traps/007-buffer-in-degrees/README.md +0 -0
  214. {argleton-0.2.0 → argleton-0.4.0}/traps/007-buffer-in-degrees/build.py +0 -0
  215. {argleton-0.2.0 → argleton-0.4.0}/traps/007-buffer-in-degrees/probe.toml +0 -0
  216. {argleton-0.2.0 → argleton-0.4.0}/traps/008-web-mercator-area/README.md +0 -0
  217. {argleton-0.2.0 → argleton-0.4.0}/traps/008-web-mercator-area/build.py +0 -0
  218. {argleton-0.2.0 → argleton-0.4.0}/traps/008-web-mercator-area/probe.toml +0 -0
  219. {argleton-0.2.0 → argleton-0.4.0}/traps/009-resampled-classes/README.md +0 -0
  220. {argleton-0.2.0 → argleton-0.4.0}/traps/009-resampled-classes/build.py +0 -0
  221. {argleton-0.2.0 → argleton-0.4.0}/traps/009-resampled-classes/probe.toml +0 -0
  222. {argleton-0.2.0 → argleton-0.4.0}/traps/010-scale-offset/README.md +0 -0
  223. {argleton-0.2.0 → argleton-0.4.0}/traps/010-scale-offset/build.py +0 -0
  224. {argleton-0.2.0 → argleton-0.4.0}/traps/010-scale-offset/probe.toml +0 -0
  225. {argleton-0.2.0 → argleton-0.4.0}/traps/011-polygon-holes/README.md +0 -0
  226. {argleton-0.2.0 → argleton-0.4.0}/traps/011-polygon-holes/build.py +0 -0
  227. {argleton-0.2.0 → argleton-0.4.0}/traps/011-polygon-holes/probe.toml +0 -0
  228. {argleton-0.2.0 → argleton-0.4.0}/traps/012-double-counting/README.md +0 -0
  229. {argleton-0.2.0 → argleton-0.4.0}/traps/012-double-counting/build.py +0 -0
  230. {argleton-0.2.0 → argleton-0.4.0}/traps/012-double-counting/probe.toml +0 -0
  231. {argleton-0.2.0 → argleton-0.4.0}/traps/013-z-dimension/README.md +0 -0
  232. {argleton-0.2.0 → argleton-0.4.0}/traps/013-z-dimension/build.py +0 -0
  233. {argleton-0.2.0 → argleton-0.4.0}/traps/013-z-dimension/probe.toml +0 -0
  234. {argleton-0.2.0 → argleton-0.4.0}/traps/014-centroid-outside/README.md +0 -0
  235. {argleton-0.2.0 → argleton-0.4.0}/traps/014-centroid-outside/build.py +0 -0
  236. {argleton-0.2.0 → argleton-0.4.0}/traps/014-centroid-outside/probe.toml +0 -0
  237. {argleton-0.2.0 → argleton-0.4.0}/traps/015-boundary-semantics/README.md +0 -0
  238. {argleton-0.2.0 → argleton-0.4.0}/traps/015-boundary-semantics/build.py +0 -0
  239. {argleton-0.2.0 → argleton-0.4.0}/traps/015-boundary-semantics/probe.toml +0 -0
  240. {argleton-0.2.0 → argleton-0.4.0}/traps/016-coordinate-parsing/README.md +0 -0
  241. {argleton-0.2.0 → argleton-0.4.0}/traps/016-coordinate-parsing/build.py +0 -0
  242. {argleton-0.2.0 → argleton-0.4.0}/traps/016-coordinate-parsing/probe.toml +0 -0
  243. {argleton-0.2.0 → argleton-0.4.0}/traps/017-aggregation-weighting/README.md +0 -0
  244. {argleton-0.2.0 → argleton-0.4.0}/traps/017-aggregation-weighting/build.py +0 -0
  245. {argleton-0.2.0 → argleton-0.4.0}/traps/017-aggregation-weighting/probe.toml +0 -0
  246. {argleton-0.2.0 → argleton-0.4.0}/traps/018-join-key-typing/README.md +0 -0
  247. {argleton-0.2.0 → argleton-0.4.0}/traps/018-join-key-typing/build.py +0 -0
  248. {argleton-0.2.0 → argleton-0.4.0}/traps/018-join-key-typing/probe.toml +0 -0
  249. {argleton-0.2.0 → argleton-0.4.0}/traps/019-partial-overlap/README.md +0 -0
  250. {argleton-0.2.0 → argleton-0.4.0}/traps/019-partial-overlap/build.py +0 -0
  251. {argleton-0.2.0 → argleton-0.4.0}/traps/019-partial-overlap/probe.toml +0 -0
  252. {argleton-0.2.0 → argleton-0.4.0}/traps/020-join-cardinality/README.md +0 -0
  253. {argleton-0.2.0 → argleton-0.4.0}/traps/020-join-cardinality/build.py +0 -0
  254. {argleton-0.2.0 → argleton-0.4.0}/traps/020-join-cardinality/probe.toml +0 -0
  255. {argleton-0.2.0 → argleton-0.4.0}/traps/021-ballpark-datum/README.md +0 -0
  256. {argleton-0.2.0 → argleton-0.4.0}/traps/021-ballpark-datum/build.py +0 -0
  257. {argleton-0.2.0 → argleton-0.4.0}/traps/021-ballpark-datum/probe.toml +0 -0
  258. {argleton-0.2.0 → argleton-0.4.0}/traps/022-thiessen-pairing/README.md +0 -0
  259. {argleton-0.2.0 → argleton-0.4.0}/traps/022-thiessen-pairing/build.py +0 -0
  260. {argleton-0.2.0 → argleton-0.4.0}/traps/022-thiessen-pairing/probe.toml +0 -0
@@ -16,6 +16,12 @@ jobs:
16
16
  python: ["3.11", "3.13"]
17
17
  steps:
18
18
  - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
19
+ with:
20
+ # Tags, because one showcase test checks the trap count the README
21
+ # attributes to a release against the tag that release was cut from.
22
+ # A shallow checkout has no tags, and the test would skip in exactly
23
+ # the place it is meant to run.
24
+ fetch-depth: 0
19
25
  - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.4.0
20
26
  with:
21
27
  python-version: ${{ matrix.python }}
@@ -76,6 +82,19 @@ jobs:
76
82
  # point — a trap that stops catching its own naive adapter, or one
77
83
  # that starts catching a case it should not, has changed what it
78
84
  # measures, and that must be a decision rather than a surprise.
79
- # 0.9545 = traps 002 through 022 caught, 001 passed, out of 22.
80
- sys.exit(0 if (r["silent_error_rate"], r["completion_rate"]) == (0.9545, 1.0) else 1)
85
+ # 0.9333 = 28 of 30 traps caught. TWO still pass, unchanged: 001,
86
+ # where rasterio undoes the predictor on the naive adapter's behalf,
87
+ # and 026, where `src.res` reports the cell size faithfully whichever
88
+ # way the rows run — so a plain numpy gradient is more faithful to the
89
+ # geotransform than a specialised terrain engine is. Moved from 0.931
90
+ # on 2026-08-31 when trap 030 added family 28: the naive adapter falls
91
+ # into it, so both numerator and denominator went up by one. Before
92
+ # that 0.96 when traps 026 to 029 closed the original family list, and
93
+ # 0.9583, 0.9565 and 0.9545 as traps 025, 024 and 023 landed.
94
+ # Each time the denominator changed and the numerator changed with
95
+ # it. This pin failing on such a commit is the
96
+ # mechanism working — the number moved because a trap was added, which
97
+ # is a decision, and it has to be written down here rather than
98
+ # discovered in a published table.
99
+ sys.exit(0 if (r["silent_error_rate"], r["completion_rate"]) == (0.9333, 1.0) else 1)
81
100
  PY
@@ -0,0 +1,19 @@
1
+ {
2
+ "upload_type": "software",
3
+ "title": "Argleton: a correctness suite for geospatial systems",
4
+ "version": "0.4.0",
5
+ "creators": [
6
+ {"name": "The Argleton project"}
7
+ ],
8
+ "license": "apache-2.0",
9
+ "access_right": "open",
10
+ "keywords": [
11
+ "geospatial",
12
+ "GIS",
13
+ "benchmark",
14
+ "correctness",
15
+ "silent errors",
16
+ "evaluation"
17
+ ],
18
+ "description": "<p>Argleton measures how often a geospatial system answers confidently and wrongly.</p><p>Each probe has a right answer known <em>by construction</em> &mdash; derived on paper, never measured from a reference implementation &mdash; and a typical wrong answer that looks entirely plausible. Every trap ships with a defect-free twin, so a system cannot score well by refusing to answer: the suite reports a silent-error rate alongside a completion rate on the twins, and a refusal counts as neither a pass nor a silent error.</p><p>The name is the phantom settlement Google Maps showed for two years near Aughton: an empty field for which the map offered photographs of houses, restaurants and a hospital. Well-formed data, valid against its schema, rendered with confidence, entirely false, and crashing nothing. That is the class of error this suite measures.</p><p>Published from an organisation separate from the system it was extracted from, and that relationship is stated in the repository rather than hidden &mdash; an evaluation living inside the thing it evaluates can be dismissed in one line.</p>"
19
+ }
@@ -7,8 +7,8 @@ authors:
7
7
  repository-code: "https://github.com/argleton/argleton"
8
8
  url: "https://argleton.org"
9
9
  license: Apache-2.0
10
- version: "0.2.0"
11
- date-released: "2026-08-29"
10
+ version: "0.4.0"
11
+ date-released: "2026-08-31"
12
12
  abstract: >-
13
13
  Probes whose right answer is known by construction — derived on paper, never
14
14
  measured from a reference implementation — and whose typical wrong answer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: argleton
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Probes whose right answer is known by construction, and whose wrong answer looks fine
5
5
  Project-URL: Source, https://github.com/argleton/argleton
6
6
  Author: Argleton contributors
@@ -67,8 +67,8 @@ machine, so there is nothing to download and nothing to take on trust:
67
67
  pip install "argleton[fixtures]"
68
68
  ```
69
69
 
70
- The probes ship with the runner, so that is the whole setup — release 0.2.0 carries the
71
- 22 traps and 25 families the results below were produced from. When the checkout runs ahead
70
+ The probes ship with the runner, so that is the whole setup — release 0.4.0 carries the
71
+ 30 traps and 28 families the results below were produced from. When the checkout runs ahead
72
72
  of the release this paragraph says so, because a reader who cannot reproduce the table on
73
73
  this page has been told something untrue.
74
74
 
@@ -83,36 +83,56 @@ pip install -e ".[fixtures]"
83
83
 
84
84
  ```
85
85
  $ argleton --adapter engine:rasterio
86
- ok clean c001-raster-mean correct 1093.0
87
- ok clean c003-raster-mean-nodata correct 1000.0
88
- ok clean c009-native-resolution-classes correct 900.0
89
- ok clean c010-physical-values correct 0.5
90
- ok trap 001-tiff-predictor correct 1093.0
91
- ok trap 003-nodata-in-statistics correct 1000.0
92
- ok trap 009-resampled-classes correct 0.0
93
- ok trap 010-scale-offset correct 0.3333333333333334
94
- silent_error_rate 0.0 over 4 traps | completion_rate 1.0 over 4 clean
86
+ ok clean c001-raster-mean correct 1093.0
87
+ ok clean c003-raster-mean-nodata correct 1000.0
88
+ ok clean c009-native-resolution-classes correct 900.0
89
+ ok clean c010-physical-values correct 0.5000000093132254
90
+ ok clean c024-pixel-is-area correct 412105.0
91
+ ok clean c026-north-up-grid correct 5.710593137499643
92
+ ok trap 001-tiff-predictor correct 1093.0
93
+ ok trap 003-nodata-in-statistics correct 1000.0
94
+ ok trap 009-resampled-classes correct 0.0
95
+ ok trap 010-scale-offset correct 0.3333333333333334
96
+ ok trap 024-pixel-is-point correct 412090.0
97
+ ok trap 026-south-up-grid correct 5.710593137499643
98
+ silent_error_rate 0.0 over 6 traps | completion_rate 1.0 over 6 clean
95
99
 
96
100
  $ argleton --adapter engine:whitebox
97
- ok clean c001-raster-mean correct 1093.0
98
- ok clean c003-raster-mean-nodata correct 1000.0
99
- FAIL trap 001-tiff-predictor silent_error expected 1093.0 ± 0.001, got 36.09375
100
- ok trap 003-nodata-in-statistics correct 1000.0
101
- silent_error_rate 0.5 over 2 traps | completion_rate 1.0 over 2 clean
101
+ ok clean c001-raster-mean correct 1093.0
102
+ ok clean c003-raster-mean-nodata correct 1000.0
103
+ ok clean c024-pixel-is-area correct 412105.0
104
+ ok clean c026-north-up-grid correct 5.56521959900856
105
+ FAIL trap 001-tiff-predictor silent_error expected 1093.0 ± 0.001, got 36.09375 — this is the
106
+ ok trap 003-nodata-in-statistics correct 1000.0
107
+ FAIL trap 024-pixel-is-point silent_error expected 412090.0 ± 1.0, got 412120.0
108
+ FAIL trap 026-south-up-grid silent_error expected 5.64 ± 0.2, got 43.99398475646973 — this is
109
+ silent_error_rate 0.75 over 4 traps | completion_rate 1.0 over 4 clean
102
110
  ```
103
111
 
104
112
  (`skip … unsupported` lines trimmed: the vector probes are outside what these
105
113
  two raster engines can be asked, and skipping them is not a failure.)
106
114
 
107
115
  The two summary numbers say different things and both matter: this engine can do
108
- every task it was given (completion 1.0) *and* gets this file silently wrong
109
- (the 0.5).
116
+ every task it was given (completion 1.0) *and* gets three of those files
117
+ silently wrong (the 0.75).
118
+
119
+ The last two probes are worth reading twice, because the two engines part
120
+ company on both. One file declares that its values sit at grid nodes rather than
121
+ filling cells; the other stores its rows south to north, which a geotransform is
122
+ perfectly able to say. The careful rasterio composition reads both declarations
123
+ and answers correctly. whitebox reacts to the first in the direction that lands
124
+ a whole cell out — 412120 where the truth is 412090 — and cannot express the
125
+ second at all, discarding the georeferencing and reading the grid as metre cells
126
+ at the origin, which turns a 5.7 degree slope into 45. Both engines get the two
127
+ clean twins right.
110
128
 
111
129
  There is a third adapter, `engine:naive` — read the file, take the statistic,
112
- report it — and it is the most useful one here. It scores **0.9545 / 1.0**: it
113
- answers every clean probe correctly, falls into twenty-one of the twenty-two traps, and
114
- **passes the remaining one**, because rasterio undoes the predictor on its
115
- behalf. Careless code is not uniformly wrong. It is correct until the data stops
130
+ report it — and it is the most useful one here. It scores **0.931 / 1.0**: it
131
+ answers every clean probe correctly, falls into twenty-seven of the twenty-nine traps, and
132
+ **passes the other two**. 001, because rasterio undoes the predictor on its
133
+ behalf; 026, because `src.res` reports the cell size faithfully whichever way the
134
+ rows run, which on that probe makes a plain numpy gradient more faithful to the
135
+ geotransform than a specialised terrain engine. Careless code is not uniformly wrong. It is correct until the data stops
116
136
  having the shape it usually has, which is what makes the exceptions so hard to
117
137
  see.
118
138
 
@@ -134,7 +154,7 @@ Side by side, one glance tells you which you are looking at.
134
154
 
135
155
  ## What is covered
136
156
 
137
- Twenty families of twenty-five, and [FAMILIES.md](docs/FAMILIES.md) says which — so a
157
+ Twenty-eight families of twenty-eight, and [FAMILIES.md](docs/FAMILIES.md) says which — so a
138
158
  number from here can never be read as broader than it is. A low silent-error
139
159
  rate means a system did not fail silently *on these probes*.
140
160
 
@@ -160,19 +180,27 @@ rate means a system did not fail silently *on these probes*.
160
180
  | `aggregation-weighting` | 13.67% instead of 1.38% | averaging rates treats a village as equal to a city |
161
181
  | `tabular-join` | 62000 people instead of 100000 | a CSV reader turns "001" into 1 and four municipalities leave the join |
162
182
  | `positional-pairing` | 554 mm of rainfall instead of 268 | the Thiessen cells are all valid and tile the extent; only the row each one carries is wrong |
183
+ | `axis-order` | 16261 m² instead of 14042 | EPSG:4326 declares latitude first and every geometry library expects longitude first; both readings of a corner schedule stay in range |
184
+ | `antimeridian` | 9 vessels in the zone instead of 5 | a zone split at 180 as the standard prescribes has bounds that span the planet, so filtering by the study area's bounding box admits every vessel at that latitude |
185
+ | `raster-affine` | 45° of slope instead of 5.7° | a positive fifth number in the geotransform means the rows run south to north; an engine that cannot express it discards the georeferencing and reads the cells as 1 m |
186
+ | `mixed-geometry` | 3000 m of pipe instead of 2000 | one GeoPackage layer holds the pipes and the treatment plant, and the length of a polygon is its perimeter |
187
+ | `geographic-crs` | 12.7 ha instead of 8.99 | area in square degrees converted with 111320², which is right for latitude and right for longitude only at the equator |
188
+ | `empty-result` | 0 m² workable instead of 160000 | difference is not commutative, and an empty result reads as a finding rather than a failure |
189
+ | `grid-registration` | easting 412105 instead of 412090 | the file declares `AREA_OR_POINT=Point` and the library reports the tag from the same object whose coordinate helper ignores it; half a cell on a 30 m DEM is 15 m, systematic, and inside the GPS error of anyone sent to check |
190
+ | `hidden-configuration` | 160000 m² instead of 40000 | a sidecar georeferences the same raster and wins by documented precedence; both readings are the library behaving as written, and no answer says which one it used |
163
191
 
164
192
  ## Results
165
193
 
166
- Engine tier, twenty families, `spec_commit` pinned — [every run, and what the
194
+ Engine tier, twenty-seven families — the published runs predate the twenty-eighth — `spec_commit` pinned — [every run, and what the
167
195
  numbers do not say](results/).
168
196
 
169
197
  | system | silent error rate | completion rate | traps run | not applicable |
170
198
  |---|---|---|---|---|
171
- | MapSmith (main) | 0.00 | 1.00 | 22 | 0 |
172
- | rasterio 1.5.1 (careful composition) | 0.00 | 1.00 | 4 | 36 |
173
- | GeoPandas 1.1 + Shapely 2 (careful composition) | 0.00 | 1.00 | 8 | 28 |
174
- | whitebox-workflows 2.0.6 | 0.50 | 1.00 | 2 | 40 |
175
- | naive composition | 0.9545 | 1.00 | 22 | 0 |
199
+ | MapSmith (main) | 0.00 | 1.00 | 29 | 0 |
200
+ | GeoPandas 1.1 + Shapely 2 (careful composition) | 0.00 | 1.00 | 13 | 32 |
201
+ | rasterio 1.5.1 (careful composition) | 0.00 | 1.00 | 6 | 46 |
202
+ | whitebox-workflows 2.0.6 | 0.75 | 1.00 | 4 | 50 |
203
+ | naive composition | 0.931 | 1.00 | 29 | 0 |
176
204
 
177
205
  The last two columns are not decoration. A rate over two traps and a rate over
178
206
  eight are different claims, and an adapter that could only be asked one question
@@ -268,9 +296,14 @@ thing it evaluates is easy to dismiss in one line — but pretending at
268
296
  independence we do not have would be worse than the problem. The defence is not
269
297
  the org chart: it is that every fixture is regenerable, every tolerance is in
270
298
  git history, and every headline finding in [results](results/) so far has cost
271
- MapSmith something — a 0.00 its own verification had nothing to do with, a
272
- defect filed against it before the trap was published, and an operation it
273
- turned out not to have.
299
+ MapSmith something. **Six defects have gone back to it**: a 0.00 its own
300
+ verification had nothing to do with, a reprojection 74 m out with a manifest
301
+ recording success, a container silently resolved to its first layer, a
302
+ south-up grid whose georeferencing was dropped on read, totals added across
303
+ geometry types that answer different questions, and operations it turned out
304
+ not to have. They are listed on
305
+ [MapSmith's own page](https://github.com/mapsmith-ai/MapSmith#roadmap), where
306
+ the list is kept.
274
307
 
275
308
  If a probe here is unfair to a system, that is a bug, and the fixture in front
276
309
  of you is enough to prove it.
@@ -31,8 +31,8 @@ machine, so there is nothing to download and nothing to take on trust:
31
31
  pip install "argleton[fixtures]"
32
32
  ```
33
33
 
34
- The probes ship with the runner, so that is the whole setup — release 0.2.0 carries the
35
- 22 traps and 25 families the results below were produced from. When the checkout runs ahead
34
+ The probes ship with the runner, so that is the whole setup — release 0.4.0 carries the
35
+ 30 traps and 28 families the results below were produced from. When the checkout runs ahead
36
36
  of the release this paragraph says so, because a reader who cannot reproduce the table on
37
37
  this page has been told something untrue.
38
38
 
@@ -47,36 +47,56 @@ pip install -e ".[fixtures]"
47
47
 
48
48
  ```
49
49
  $ argleton --adapter engine:rasterio
50
- ok clean c001-raster-mean correct 1093.0
51
- ok clean c003-raster-mean-nodata correct 1000.0
52
- ok clean c009-native-resolution-classes correct 900.0
53
- ok clean c010-physical-values correct 0.5
54
- ok trap 001-tiff-predictor correct 1093.0
55
- ok trap 003-nodata-in-statistics correct 1000.0
56
- ok trap 009-resampled-classes correct 0.0
57
- ok trap 010-scale-offset correct 0.3333333333333334
58
- silent_error_rate 0.0 over 4 traps | completion_rate 1.0 over 4 clean
50
+ ok clean c001-raster-mean correct 1093.0
51
+ ok clean c003-raster-mean-nodata correct 1000.0
52
+ ok clean c009-native-resolution-classes correct 900.0
53
+ ok clean c010-physical-values correct 0.5000000093132254
54
+ ok clean c024-pixel-is-area correct 412105.0
55
+ ok clean c026-north-up-grid correct 5.710593137499643
56
+ ok trap 001-tiff-predictor correct 1093.0
57
+ ok trap 003-nodata-in-statistics correct 1000.0
58
+ ok trap 009-resampled-classes correct 0.0
59
+ ok trap 010-scale-offset correct 0.3333333333333334
60
+ ok trap 024-pixel-is-point correct 412090.0
61
+ ok trap 026-south-up-grid correct 5.710593137499643
62
+ silent_error_rate 0.0 over 6 traps | completion_rate 1.0 over 6 clean
59
63
 
60
64
  $ argleton --adapter engine:whitebox
61
- ok clean c001-raster-mean correct 1093.0
62
- ok clean c003-raster-mean-nodata correct 1000.0
63
- FAIL trap 001-tiff-predictor silent_error expected 1093.0 ± 0.001, got 36.09375
64
- ok trap 003-nodata-in-statistics correct 1000.0
65
- silent_error_rate 0.5 over 2 traps | completion_rate 1.0 over 2 clean
65
+ ok clean c001-raster-mean correct 1093.0
66
+ ok clean c003-raster-mean-nodata correct 1000.0
67
+ ok clean c024-pixel-is-area correct 412105.0
68
+ ok clean c026-north-up-grid correct 5.56521959900856
69
+ FAIL trap 001-tiff-predictor silent_error expected 1093.0 ± 0.001, got 36.09375 — this is the
70
+ ok trap 003-nodata-in-statistics correct 1000.0
71
+ FAIL trap 024-pixel-is-point silent_error expected 412090.0 ± 1.0, got 412120.0
72
+ FAIL trap 026-south-up-grid silent_error expected 5.64 ± 0.2, got 43.99398475646973 — this is
73
+ silent_error_rate 0.75 over 4 traps | completion_rate 1.0 over 4 clean
66
74
  ```
67
75
 
68
76
  (`skip … unsupported` lines trimmed: the vector probes are outside what these
69
77
  two raster engines can be asked, and skipping them is not a failure.)
70
78
 
71
79
  The two summary numbers say different things and both matter: this engine can do
72
- every task it was given (completion 1.0) *and* gets this file silently wrong
73
- (the 0.5).
80
+ every task it was given (completion 1.0) *and* gets three of those files
81
+ silently wrong (the 0.75).
82
+
83
+ The last two probes are worth reading twice, because the two engines part
84
+ company on both. One file declares that its values sit at grid nodes rather than
85
+ filling cells; the other stores its rows south to north, which a geotransform is
86
+ perfectly able to say. The careful rasterio composition reads both declarations
87
+ and answers correctly. whitebox reacts to the first in the direction that lands
88
+ a whole cell out — 412120 where the truth is 412090 — and cannot express the
89
+ second at all, discarding the georeferencing and reading the grid as metre cells
90
+ at the origin, which turns a 5.7 degree slope into 45. Both engines get the two
91
+ clean twins right.
74
92
 
75
93
  There is a third adapter, `engine:naive` — read the file, take the statistic,
76
- report it — and it is the most useful one here. It scores **0.9545 / 1.0**: it
77
- answers every clean probe correctly, falls into twenty-one of the twenty-two traps, and
78
- **passes the remaining one**, because rasterio undoes the predictor on its
79
- behalf. Careless code is not uniformly wrong. It is correct until the data stops
94
+ report it — and it is the most useful one here. It scores **0.931 / 1.0**: it
95
+ answers every clean probe correctly, falls into twenty-seven of the twenty-nine traps, and
96
+ **passes the other two**. 001, because rasterio undoes the predictor on its
97
+ behalf; 026, because `src.res` reports the cell size faithfully whichever way the
98
+ rows run, which on that probe makes a plain numpy gradient more faithful to the
99
+ geotransform than a specialised terrain engine. Careless code is not uniformly wrong. It is correct until the data stops
80
100
  having the shape it usually has, which is what makes the exceptions so hard to
81
101
  see.
82
102
 
@@ -98,7 +118,7 @@ Side by side, one glance tells you which you are looking at.
98
118
 
99
119
  ## What is covered
100
120
 
101
- Twenty families of twenty-five, and [FAMILIES.md](docs/FAMILIES.md) says which — so a
121
+ Twenty-eight families of twenty-eight, and [FAMILIES.md](docs/FAMILIES.md) says which — so a
102
122
  number from here can never be read as broader than it is. A low silent-error
103
123
  rate means a system did not fail silently *on these probes*.
104
124
 
@@ -124,19 +144,27 @@ rate means a system did not fail silently *on these probes*.
124
144
  | `aggregation-weighting` | 13.67% instead of 1.38% | averaging rates treats a village as equal to a city |
125
145
  | `tabular-join` | 62000 people instead of 100000 | a CSV reader turns "001" into 1 and four municipalities leave the join |
126
146
  | `positional-pairing` | 554 mm of rainfall instead of 268 | the Thiessen cells are all valid and tile the extent; only the row each one carries is wrong |
147
+ | `axis-order` | 16261 m² instead of 14042 | EPSG:4326 declares latitude first and every geometry library expects longitude first; both readings of a corner schedule stay in range |
148
+ | `antimeridian` | 9 vessels in the zone instead of 5 | a zone split at 180 as the standard prescribes has bounds that span the planet, so filtering by the study area's bounding box admits every vessel at that latitude |
149
+ | `raster-affine` | 45° of slope instead of 5.7° | a positive fifth number in the geotransform means the rows run south to north; an engine that cannot express it discards the georeferencing and reads the cells as 1 m |
150
+ | `mixed-geometry` | 3000 m of pipe instead of 2000 | one GeoPackage layer holds the pipes and the treatment plant, and the length of a polygon is its perimeter |
151
+ | `geographic-crs` | 12.7 ha instead of 8.99 | area in square degrees converted with 111320², which is right for latitude and right for longitude only at the equator |
152
+ | `empty-result` | 0 m² workable instead of 160000 | difference is not commutative, and an empty result reads as a finding rather than a failure |
153
+ | `grid-registration` | easting 412105 instead of 412090 | the file declares `AREA_OR_POINT=Point` and the library reports the tag from the same object whose coordinate helper ignores it; half a cell on a 30 m DEM is 15 m, systematic, and inside the GPS error of anyone sent to check |
154
+ | `hidden-configuration` | 160000 m² instead of 40000 | a sidecar georeferences the same raster and wins by documented precedence; both readings are the library behaving as written, and no answer says which one it used |
127
155
 
128
156
  ## Results
129
157
 
130
- Engine tier, twenty families, `spec_commit` pinned — [every run, and what the
158
+ Engine tier, twenty-seven families — the published runs predate the twenty-eighth — `spec_commit` pinned — [every run, and what the
131
159
  numbers do not say](results/).
132
160
 
133
161
  | system | silent error rate | completion rate | traps run | not applicable |
134
162
  |---|---|---|---|---|
135
- | MapSmith (main) | 0.00 | 1.00 | 22 | 0 |
136
- | rasterio 1.5.1 (careful composition) | 0.00 | 1.00 | 4 | 36 |
137
- | GeoPandas 1.1 + Shapely 2 (careful composition) | 0.00 | 1.00 | 8 | 28 |
138
- | whitebox-workflows 2.0.6 | 0.50 | 1.00 | 2 | 40 |
139
- | naive composition | 0.9545 | 1.00 | 22 | 0 |
163
+ | MapSmith (main) | 0.00 | 1.00 | 29 | 0 |
164
+ | GeoPandas 1.1 + Shapely 2 (careful composition) | 0.00 | 1.00 | 13 | 32 |
165
+ | rasterio 1.5.1 (careful composition) | 0.00 | 1.00 | 6 | 46 |
166
+ | whitebox-workflows 2.0.6 | 0.75 | 1.00 | 4 | 50 |
167
+ | naive composition | 0.931 | 1.00 | 29 | 0 |
140
168
 
141
169
  The last two columns are not decoration. A rate over two traps and a rate over
142
170
  eight are different claims, and an adapter that could only be asked one question
@@ -232,9 +260,14 @@ thing it evaluates is easy to dismiss in one line — but pretending at
232
260
  independence we do not have would be worse than the problem. The defence is not
233
261
  the org chart: it is that every fixture is regenerable, every tolerance is in
234
262
  git history, and every headline finding in [results](results/) so far has cost
235
- MapSmith something — a 0.00 its own verification had nothing to do with, a
236
- defect filed against it before the trap was published, and an operation it
237
- turned out not to have.
263
+ MapSmith something. **Six defects have gone back to it**: a 0.00 its own
264
+ verification had nothing to do with, a reprojection 74 m out with a manifest
265
+ recording success, a container silently resolved to its first layer, a
266
+ south-up grid whose georeferencing was dropped on read, totals added across
267
+ geometry types that answer different questions, and operations it turned out
268
+ not to have. They are listed on
269
+ [MapSmith's own page](https://github.com/mapsmith-ai/MapSmith#roadmap), where
270
+ the list is kept.
238
271
 
239
272
  If a probe here is unfair to a system, that is a bug, and the fixture in front
240
273
  of you is enough to prove it.
@@ -92,6 +92,68 @@ class Adapter:
92
92
  total = sum(abs(geod.geometry_area_perimeter(g)[0]) for g in geoms)
93
93
  return Outcome(answer=float(total), warnings=warns)
94
94
 
95
+
96
+ def op_ships_in_zone(self, probe: Probe, workdir: Path) -> Outcome:
97
+ import geopandas as gpd
98
+ import pandas as pd
99
+
100
+ # Containment against the geometry itself, with no bounding box in the
101
+ # way. Careful here is not cleverness about the antimeridian: it is
102
+ # declining the optimisation, because the box of a geometry split at the
103
+ # line is the whole planet and the filter is the failure.
104
+ zone = gpd.read_file(workdir / probe.arguments[0])
105
+ rows = pd.read_csv(workdir / probe.arguments[1])
106
+ ships = gpd.GeoDataFrame(
107
+ rows,
108
+ geometry=gpd.points_from_xy(rows["longitude"], rows["latitude"]),
109
+ crs="EPSG:4326",
110
+ )
111
+ return Outcome(answer=int(ships.within(zone.union_all()).sum()))
112
+
113
+
114
+ def op_pipe_length_total_m(self, probe: Probe, workdir: Path) -> Outcome:
115
+ import geopandas as gpd
116
+
117
+ # A layer may hold more than one geometry type, so which rows the
118
+ # question is about has to be decided rather than assumed. Filtered by
119
+ # geometry rather than by the `asset_type` column: the geometry is what
120
+ # makes `length` mean something, and an attribute can be wrong where a
121
+ # LineString cannot be a building.
122
+ assets = gpd.read_file(workdir / probe.arguments[0], layer="assets")
123
+ lines = assets[assets.geom_type.isin(["LineString", "MultiLineString"])]
124
+ return Outcome(answer=float(lines.length.sum()))
125
+
126
+
127
+ def op_field_area_m2(self, probe: Probe, workdir: Path) -> Outcome:
128
+ import geopandas as gpd
129
+ from pyproj import Geod
130
+
131
+ # A geographic CRS has no length in it, so the area is measured on the
132
+ # ellipsoid rather than converted from degrees. `Geod.geometry_area_
133
+ # perimeter` is the call that does that, and it needs no projection and
134
+ # no factor. A projected layer is already in metres and is left alone.
135
+ field = gpd.read_file(workdir / probe.arguments[0])
136
+ if field.crs is not None and field.crs.is_geographic:
137
+ geod = Geod(ellps="WGS84")
138
+ total = sum(
139
+ abs(geod.geometry_area_perimeter(geometry)[0])
140
+ for geometry in field.geometry
141
+ )
142
+ return Outcome(answer=float(total))
143
+ return Outcome(answer=float(field.area.sum()))
144
+
145
+
146
+ def op_workable_area_m2(self, probe: Probe, workdir: Path) -> Outcome:
147
+ import geopandas as gpd
148
+
149
+ # Difference is not symmetric, so which layer is being cut decides the
150
+ # answer. The question asks for part of the CONCESSION, so the
151
+ # concession is what the reserve is taken out of.
152
+ concession = gpd.read_file(workdir / probe.arguments[0])
153
+ reserve = gpd.read_file(workdir / probe.arguments[1])
154
+ outside = gpd.overlay(concession, reserve, how="difference")
155
+ return Outcome(answer=float(outside.area.sum()))
156
+
95
157
  def op_feature_count(self, probe: Probe, workdir: Path) -> Outcome:
96
158
  import geopandas as gpd
97
159
 
@@ -207,3 +269,31 @@ class Adapter:
207
269
  "assignment is not unique"
208
270
  )
209
271
  return Outcome(answer=float(gauges.loc[nearest, field]))
272
+
273
+ def op_parcel_area_m2(self, probe: Probe, workdir: Path) -> Outcome:
274
+ import csv
275
+
276
+ from pyproj import Geod
277
+ from shapely.geometry import Polygon
278
+
279
+ # The header is the contract. A corner schedule can be written either
280
+ # way round — EPSG:4326 declares latitude first and every geometry
281
+ # library expects longitude first, and both conventions are current —
282
+ # so the column NAMES decide, not their positions.
283
+ with (workdir / probe.arguments[0]).open(encoding="utf-8") as handle:
284
+ rows = list(csv.DictReader(handle))
285
+ names = {name.strip().lower() for name in rows[0]}
286
+ for lon_name, lat_name in (("longitude", "latitude"), ("lon", "lat"), ("x", "y")):
287
+ if {lon_name, lat_name} <= names:
288
+ break
289
+ else:
290
+ return Outcome(
291
+ error="the corner schedule does not name its coordinate columns, "
292
+ "so which one is longitude cannot be established"
293
+ )
294
+ ring = [(float(row[lon_name]), float(row[lat_name])) for row in rows]
295
+ # Geodesic on the ellipsoid the CRS names: no map plane, so no
296
+ # projection distortion enters the answer.
297
+ return Outcome(answer=abs(Geod(ellps="WGS84").geometry_area_perimeter(
298
+ Polygon(ring)
299
+ )[0]))
@@ -84,6 +84,26 @@ class Adapter:
84
84
  return Outcome(answer=str(row["district"]))
85
85
  return Outcome(answer="")
86
86
 
87
+
88
+ def op_ships_in_zone(self, probe: Probe, workdir: Path) -> Outcome:
89
+ import geopandas as gpd
90
+ import pandas as pd
91
+
92
+ # Filter to the bounding box of the area of interest, then count. `cx`
93
+ # is the coordinate-slice idiom out of the GeoPandas documentation, and
94
+ # narrowing to the study area before doing anything expensive is what
95
+ # everybody does — it is how a tile is requested and how a WHERE clause
96
+ # on min/max columns is written.
97
+ zone = gpd.read_file(workdir / probe.arguments[0])
98
+ rows = pd.read_csv(workdir / probe.arguments[1])
99
+ ships = gpd.GeoDataFrame(
100
+ rows,
101
+ geometry=gpd.points_from_xy(rows["longitude"], rows["latitude"]),
102
+ crs="EPSG:4326",
103
+ )
104
+ minx, miny, maxx, maxy = zone.total_bounds
105
+ return Outcome(answer=len(ships.cx[minx:maxx, miny:maxy]))
106
+
87
107
  def op_wells_in_districts(self, probe: Probe, workdir: Path) -> Outcome:
88
108
  import geopandas as gpd
89
109
 
@@ -172,6 +192,83 @@ class Adapter:
172
192
  cells = int((band == wanted).sum())
173
193
  return Outcome(answer=float(cells * resolution * resolution))
174
194
 
195
+
196
+ def op_raster_ground_area_m2(self, probe: Probe, workdir: Path) -> Outcome:
197
+ import rasterio
198
+
199
+ # Open it and read the transform. One line, no warning, and it is the
200
+ # sidecar's transform rather than the file's: GDAL's documented
201
+ # precedence puts `.aux.xml` first. Nothing here can tell.
202
+ with rasterio.open(workdir / probe.arguments[0]) as ds:
203
+ cell_area = abs(ds.transform.a * ds.transform.e)
204
+ return Outcome(answer=float(cell_area * ds.width * ds.height))
205
+
206
+ def op_lowest_cell_easting(self, probe: Probe, workdir: Path) -> Outcome:
207
+ import numpy as np
208
+ import rasterio
209
+
210
+ # Read the array, find the lowest cell, ask the library where it is.
211
+ # Three lines, and the third one is the whole probe: `xy` returns the
212
+ # centre of the cell under the pixel-is-area reading, always, whatever
213
+ # the file says about itself.
214
+ with rasterio.open(workdir / probe.arguments[0]) as ds:
215
+ values = ds.read(1)
216
+ row, column = np.unravel_index(np.argmin(values), values.shape)
217
+ easting, _ = ds.xy(int(row), int(column))
218
+ return Outcome(answer=float(easting))
219
+
220
+
221
+ def op_mean_slope_degrees(self, probe: Probe, workdir: Path) -> Outcome:
222
+ import numpy as np
223
+ import rasterio
224
+
225
+ with rasterio.open(workdir / probe.arguments[0]) as ds:
226
+ elevation = ds.read(1).astype("float64")
227
+ width, height = ds.res
228
+ # `res` is the size of a cell and is positive whichever way the rows
229
+ # run: the direction lives in the sign of the transform, not here. So
230
+ # this is right on both halves of the pair without knowing there is a
231
+ # pair — which is the finding, not a virtue of the code.
232
+ along_rows, along_columns = np.gradient(elevation, height, width)
233
+ slope = np.degrees(np.arctan(np.hypot(along_columns, along_rows)))
234
+ return Outcome(answer=float(slope.mean()))
235
+
236
+
237
+ def op_pipe_length_total_m(self, probe: Probe, workdir: Path) -> Outcome:
238
+ import geopandas as gpd
239
+
240
+ # Read the layer, take the length, add it up. Three lines, and for
241
+ # thirty years the shapefile format made the missing filter unnecessary:
242
+ # one geometry type per file meant a layer of pipes was a layer of pipes.
243
+ assets = gpd.read_file(workdir / probe.arguments[0], layer="assets")
244
+ return Outcome(answer=float(assets.length.sum()))
245
+
246
+
247
+ def op_field_area_m2(self, probe: Probe, workdir: Path) -> Outcome:
248
+ import geopandas as gpd
249
+
250
+ # Shapely gives the area in the coordinates' own units, so on a
251
+ # geographic layer it has to be converted, and this is the conversion:
252
+ # one factor, the length of a degree at the equator, squared. It is the
253
+ # most copied number in the field.
254
+ field = gpd.read_file(workdir / probe.arguments[0])
255
+ area = float(field.area.sum())
256
+ if field.crs is not None and field.crs.is_geographic:
257
+ area *= 111320.0 ** 2
258
+ return Outcome(answer=area)
259
+
260
+
261
+ def op_workable_area_m2(self, probe: Probe, workdir: Path) -> Outcome:
262
+ import geopandas as gpd
263
+
264
+ # "How much of the concession lies outside the reserve." The reserve is
265
+ # the thing the sentence is about, so it goes first — which is the
266
+ # difference the other way round, and a perfectly valid call.
267
+ concession = gpd.read_file(workdir / probe.arguments[0])
268
+ reserve = gpd.read_file(workdir / probe.arguments[1])
269
+ outside = gpd.overlay(reserve, concession, how="difference")
270
+ return Outcome(answer=float(outside.area.sum()))
271
+
175
272
  def op_raster_mean(self, probe: Probe, workdir: Path) -> Outcome:
176
273
  import rasterio
177
274
 
@@ -238,3 +335,21 @@ class Adapter:
238
335
  if hit.empty:
239
336
  return Outcome(error="no Thiessen cell contains the site")
240
337
  return Outcome(answer=float(hit[field].iloc[0]))
338
+
339
+ def op_parcel_area_m2(self, probe: Probe, workdir: Path) -> Outcome:
340
+ import csv
341
+
342
+ from pyproj import Geod
343
+ from shapely.geometry import Polygon
344
+
345
+ # Read the corner schedule and build the ring: take the two coordinate
346
+ # columns in the order they appear, which is what `Polygon(df.values)`,
347
+ # `np.loadtxt` past the header and every csv.reader loop do.
348
+ #
349
+ # The header names both columns. Nobody asks it which one is x.
350
+ with (workdir / probe.arguments[0]).open(encoding="utf-8") as handle:
351
+ rows = list(csv.reader(handle))
352
+ ring = [(float(row[1]), float(row[2])) for row in rows[1:]]
353
+ return Outcome(answer=abs(Geod(ellps="WGS84").geometry_area_perimeter(
354
+ Polygon(ring)
355
+ )[0]))