argleton 0.1.0__py3-none-any.whl
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.
- adapters/__init__.py +0 -0
- adapters/engine_geopandas.py +176 -0
- adapters/engine_naive.py +220 -0
- adapters/engine_rasterio.py +78 -0
- adapters/engine_whitebox.py +34 -0
- adapters/gis_mcp.py +192 -0
- adapters/mapsmith.py +496 -0
- argleton/__init__.py +3 -0
- argleton/model.py +202 -0
- argleton/probes/clean/c001-raster-mean/build.py +36 -0
- argleton/probes/clean/c001-raster-mean/probe.toml +25 -0
- argleton/probes/clean/c002-projected-area/build.py +44 -0
- argleton/probes/clean/c002-projected-area/probe.toml +25 -0
- argleton/probes/clean/c003-raster-mean-nodata/build.py +35 -0
- argleton/probes/clean/c003-raster-mean-nodata/probe.toml +26 -0
- argleton/probes/clean/c004-points-in-polygon/build.py +60 -0
- argleton/probes/clean/c004-points-in-polygon/probe.toml +29 -0
- argleton/probes/clean/c005-polygon-area/build.py +38 -0
- argleton/probes/clean/c005-polygon-area/probe.toml +26 -0
- argleton/probes/clean/c006-named-layer/build.py +35 -0
- argleton/probes/clean/c006-named-layer/probe.toml +26 -0
- argleton/probes/clean/c007-distance-in-metres/build.py +42 -0
- argleton/probes/clean/c007-distance-in-metres/probe.toml +27 -0
- argleton/probes/clean/c008-equal-area-crs/build.py +39 -0
- argleton/probes/clean/c008-equal-area-crs/probe.toml +37 -0
- argleton/probes/clean/c009-native-resolution-classes/build.py +51 -0
- argleton/probes/clean/c009-native-resolution-classes/probe.toml +31 -0
- argleton/probes/clean/c010-physical-values/build.py +49 -0
- argleton/probes/clean/c010-physical-values/probe.toml +30 -0
- argleton/probes/clean/c011-solid-parcel/build.py +52 -0
- argleton/probes/clean/c011-solid-parcel/probe.toml +28 -0
- argleton/probes/clean/c012-disjoint-concessions/build.py +56 -0
- argleton/probes/clean/c012-disjoint-concessions/probe.toml +28 -0
- argleton/probes/clean/c013-flat-pipeline/build.py +42 -0
- argleton/probes/clean/c013-flat-pipeline/probe.toml +28 -0
- argleton/probes/clean/c014-convex-parcel/build.py +70 -0
- argleton/probes/clean/c014-convex-parcel/probe.toml +28 -0
- argleton/probes/clean/c015-wells-off-the-seam/build.py +73 -0
- argleton/probes/clean/c015-wells-off-the-seam/probe.toml +28 -0
- argleton/probes/clean/c016-decimal-degrees/build.py +29 -0
- argleton/probes/clean/c016-decimal-degrees/probe.toml +27 -0
- argleton/probes/clean/c017-equal-populations/build.py +60 -0
- argleton/probes/clean/c017-equal-populations/probe.toml +28 -0
- argleton/probes/clean/c018-plain-keys/build.py +59 -0
- argleton/probes/clean/c018-plain-keys/probe.toml +28 -0
- argleton/probes/clean/c019-fully-contained/build.py +64 -0
- argleton/probes/clean/c019-fully-contained/probe.toml +28 -0
- argleton/probes/clean/c020-one-owner-each/build.py +55 -0
- argleton/probes/clean/c020-one-owner-each/probe.toml +27 -0
- argleton/probes/clean/c021-greenwich-variant/build.py +41 -0
- argleton/probes/clean/c021-greenwich-variant/probe.toml +41 -0
- argleton/probes/schema/probe.schema.json +138 -0
- argleton/probes/schema/result.schema.json +103 -0
- argleton/probes/traps/001-tiff-predictor/README.md +75 -0
- argleton/probes/traps/001-tiff-predictor/build.py +58 -0
- argleton/probes/traps/001-tiff-predictor/probe.toml +57 -0
- argleton/probes/traps/002-feet-as-metres/README.md +79 -0
- argleton/probes/traps/002-feet-as-metres/build.py +44 -0
- argleton/probes/traps/002-feet-as-metres/probe.toml +70 -0
- argleton/probes/traps/003-nodata-in-statistics/README.md +61 -0
- argleton/probes/traps/003-nodata-in-statistics/build.py +51 -0
- argleton/probes/traps/003-nodata-in-statistics/probe.toml +62 -0
- argleton/probes/traps/004-mismatched-crs-join/README.md +65 -0
- argleton/probes/traps/004-mismatched-crs-join/build.py +65 -0
- argleton/probes/traps/004-mismatched-crs-join/probe.toml +75 -0
- argleton/probes/traps/005-bowtie-area/README.md +63 -0
- argleton/probes/traps/005-bowtie-area/build.py +41 -0
- argleton/probes/traps/005-bowtie-area/probe.toml +73 -0
- argleton/probes/traps/006-default-layer/README.md +65 -0
- argleton/probes/traps/006-default-layer/build.py +55 -0
- argleton/probes/traps/006-default-layer/probe.toml +63 -0
- argleton/probes/traps/007-buffer-in-degrees/README.md +57 -0
- argleton/probes/traps/007-buffer-in-degrees/build.py +42 -0
- argleton/probes/traps/007-buffer-in-degrees/probe.toml +64 -0
- argleton/probes/traps/008-web-mercator-area/README.md +55 -0
- argleton/probes/traps/008-web-mercator-area/build.py +39 -0
- argleton/probes/traps/008-web-mercator-area/probe.toml +68 -0
- argleton/probes/traps/009-resampled-classes/README.md +88 -0
- argleton/probes/traps/009-resampled-classes/build.py +46 -0
- argleton/probes/traps/009-resampled-classes/probe.toml +76 -0
- argleton/probes/traps/010-scale-offset/README.md +76 -0
- argleton/probes/traps/010-scale-offset/build.py +55 -0
- argleton/probes/traps/010-scale-offset/probe.toml +69 -0
- argleton/probes/traps/011-polygon-holes/README.md +47 -0
- argleton/probes/traps/011-polygon-holes/build.py +55 -0
- argleton/probes/traps/011-polygon-holes/probe.toml +59 -0
- argleton/probes/traps/012-double-counting/README.md +40 -0
- argleton/probes/traps/012-double-counting/build.py +58 -0
- argleton/probes/traps/012-double-counting/probe.toml +52 -0
- argleton/probes/traps/013-z-dimension/README.md +42 -0
- argleton/probes/traps/013-z-dimension/build.py +43 -0
- argleton/probes/traps/013-z-dimension/probe.toml +53 -0
- argleton/probes/traps/014-centroid-outside/README.md +42 -0
- argleton/probes/traps/014-centroid-outside/build.py +72 -0
- argleton/probes/traps/014-centroid-outside/probe.toml +59 -0
- argleton/probes/traps/015-boundary-semantics/README.md +41 -0
- argleton/probes/traps/015-boundary-semantics/build.py +74 -0
- argleton/probes/traps/015-boundary-semantics/probe.toml +57 -0
- argleton/probes/traps/016-coordinate-parsing/README.md +40 -0
- argleton/probes/traps/016-coordinate-parsing/build.py +36 -0
- argleton/probes/traps/016-coordinate-parsing/probe.toml +52 -0
- argleton/probes/traps/017-aggregation-weighting/README.md +42 -0
- argleton/probes/traps/017-aggregation-weighting/build.py +62 -0
- argleton/probes/traps/017-aggregation-weighting/probe.toml +53 -0
- argleton/probes/traps/018-join-key-typing/README.md +41 -0
- argleton/probes/traps/018-join-key-typing/build.py +61 -0
- argleton/probes/traps/018-join-key-typing/probe.toml +53 -0
- argleton/probes/traps/019-partial-overlap/README.md +43 -0
- argleton/probes/traps/019-partial-overlap/build.py +66 -0
- argleton/probes/traps/019-partial-overlap/probe.toml +53 -0
- argleton/probes/traps/020-join-cardinality/README.md +42 -0
- argleton/probes/traps/020-join-cardinality/build.py +60 -0
- argleton/probes/traps/020-join-cardinality/probe.toml +51 -0
- argleton/probes/traps/021-ballpark-datum/README.md +100 -0
- argleton/probes/traps/021-ballpark-datum/build.py +44 -0
- argleton/probes/traps/021-ballpark-datum/probe.toml +92 -0
- argleton/published.py +49 -0
- argleton/run.py +170 -0
- argleton/score.py +150 -0
- argleton-0.1.0.dist-info/METADATA +275 -0
- argleton-0.1.0.dist-info/RECORD +124 -0
- argleton-0.1.0.dist-info/WHEEL +4 -0
- argleton-0.1.0.dist-info/entry_points.txt +2 -0
- argleton-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
id = "c007-distance-in-metres"
|
|
2
|
+
population = "clean"
|
|
3
|
+
family = "implicit-parameter-units"
|
|
4
|
+
title = "Count of wells within a distance, layer already in metres"
|
|
5
|
+
surface = ["engine", "agent"]
|
|
6
|
+
|
|
7
|
+
[task]
|
|
8
|
+
call = "count_within_distance(wells.gpkg, target=W-1, distance_meters=500)"
|
|
9
|
+
prompt = """
|
|
10
|
+
How many other wells in wells.gpkg lie within 500 meters of the well W-1?
|
|
11
|
+
Answer with the count."""
|
|
12
|
+
|
|
13
|
+
[truth]
|
|
14
|
+
kind = "count"
|
|
15
|
+
value = 5
|
|
16
|
+
tolerance = 0
|
|
17
|
+
derivation = """
|
|
18
|
+
By construction in EPSG:32633 (metres), by Pythagoras on the offsets from W-1:
|
|
19
|
+
300, 400, ~354, ~446 and 460 metres for the five inside wells; every other
|
|
20
|
+
well lies 2,500 m away or more. The farthest included (460) and the nearest
|
|
21
|
+
excluded (2,500) leave no boundary case: the count is 5. The trap and this
|
|
22
|
+
control differ only in whether the layer's unit is the one the question uses."""
|
|
23
|
+
|
|
24
|
+
[provenance]
|
|
25
|
+
source = "control for 007-buffer-in-degrees"
|
|
26
|
+
found_by = "mapsmith"
|
|
27
|
+
date = "2026-08-25"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Build the clean twin of 008: the same question where the plane tells the truth.
|
|
2
|
+
|
|
3
|
+
The parcel is an exact 100 x 100 rectangle in EPSG:3035 (Lambert Azimuthal
|
|
4
|
+
Equal-Area over Europe), at the same latitude as the trap. LAEA preserves area
|
|
5
|
+
by construction, so here — and only here — the planar shoelace IS the ground
|
|
6
|
+
area, and the composition that falls into 008 answers this one correctly.
|
|
7
|
+
Without this twin, a failure on 008 could not be told apart from "the system
|
|
8
|
+
cannot measure an area at all".
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
# GeoPackage stamps the write time into `gpkg_contents.last_change`, so two
|
|
18
|
+
# builds of the same data differ byte for byte unless the clock is pinned.
|
|
19
|
+
os.environ.setdefault("OGR_CURRENT_DATE", "2026-08-25T00:00:00.000Z")
|
|
20
|
+
|
|
21
|
+
import geopandas as gpd
|
|
22
|
+
from shapely.geometry import box
|
|
23
|
+
|
|
24
|
+
CRS = "EPSG:3035"
|
|
25
|
+
X0, Y0, SIDE = 4_532_000.0, 2_050_000.0, 100.0 # ~12.52E 41.52N
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main(destination: Path) -> int:
|
|
29
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
gpd.GeoDataFrame(
|
|
31
|
+
{"parcel_id": ["P-1"]},
|
|
32
|
+
geometry=[box(X0, Y0, X0 + SIDE, Y0 + SIDE)],
|
|
33
|
+
crs=CRS,
|
|
34
|
+
).to_file(destination / "parcel.gpkg", layer="parcel", driver="GPKG")
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
if __name__ == "__main__":
|
|
39
|
+
raise SystemExit(main(Path(sys.argv[1] if len(sys.argv) > 1 else ".")))
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
id = "c008-equal-area-crs"
|
|
2
|
+
population = "clean"
|
|
3
|
+
family = "projection-distortion"
|
|
4
|
+
title = "The same area question where the plane tells the truth"
|
|
5
|
+
surface = ["engine", "agent"]
|
|
6
|
+
|
|
7
|
+
[task]
|
|
8
|
+
call = "ground_area_m2(parcel.gpkg)"
|
|
9
|
+
prompt = """
|
|
10
|
+
parcel.gpkg holds one land parcel in central Italy. How large is the parcel
|
|
11
|
+
on the ground, in square metres? Answer with the number."""
|
|
12
|
+
|
|
13
|
+
[truth]
|
|
14
|
+
kind = "scalar"
|
|
15
|
+
value = 10000.0
|
|
16
|
+
tolerance = 8.0
|
|
17
|
+
derivation = """
|
|
18
|
+
The parcel is an exact 100 x 100 rectangle in EPSG:3035, a Lambert Azimuthal
|
|
19
|
+
Equal-Area projection: equal-area by construction, so its planar area IS the
|
|
20
|
+
ground (ellipsoidal) area — 10000 m2 exactly. Checked against the WGS84
|
|
21
|
+
geodesic area of the same footprint: 10000.000004 m2. A UTM 33N planar
|
|
22
|
+
measurement, 210 km from the central meridian, reads 10002.55 m2 (scale
|
|
23
|
+
factor squared); the tolerance of 8 admits it. Every correct method lands
|
|
24
|
+
within 3 m2 of the truth."""
|
|
25
|
+
|
|
26
|
+
[provenance]
|
|
27
|
+
source = """
|
|
28
|
+
Clean twin of 008-web-mercator-area: same question, same file layout, same
|
|
29
|
+
feature count, an equally ordinary projected CRS in metres. The one thing
|
|
30
|
+
that changed is that this projection preserves area. A system that answers
|
|
31
|
+
this and fails 008 mishandles distortion, not areas; a system that fails
|
|
32
|
+
both cannot answer the question, which is a different (and lesser) finding."""
|
|
33
|
+
found_by = "mapsmith"
|
|
34
|
+
date = "2026-08-25"
|
|
35
|
+
|
|
36
|
+
[provenance.engine_versions]
|
|
37
|
+
geopandas = "1.1.4/shapely 2.1.2"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Clean twin of 009: the same question where the urban class is really there.
|
|
2
|
+
|
|
3
|
+
Same legend, same CRS, already on the 15 m grid the question asks for, and
|
|
4
|
+
four cells genuinely coded urban. A system that can read a class map and add
|
|
5
|
+
up the area of one class answers 900; the trap's answer is zero. Without this
|
|
6
|
+
twin, a zero on the trap could not be told apart from "cannot do the task".
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import numpy as np
|
|
15
|
+
import rasterio
|
|
16
|
+
from rasterio.transform import from_origin
|
|
17
|
+
|
|
18
|
+
CRS = "EPSG:32633"
|
|
19
|
+
CELL = 15.0
|
|
20
|
+
# Four genuinely urban cells (2), the rest forest and water.
|
|
21
|
+
CLASSES = np.array(
|
|
22
|
+
[
|
|
23
|
+
[1, 1, 2, 3],
|
|
24
|
+
[1, 1, 2, 3],
|
|
25
|
+
[1, 1, 2, 3],
|
|
26
|
+
[1, 1, 2, 3],
|
|
27
|
+
],
|
|
28
|
+
dtype="uint8",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main(destination: Path) -> int:
|
|
33
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
with rasterio.open(
|
|
35
|
+
destination / "landcover.tif",
|
|
36
|
+
"w",
|
|
37
|
+
driver="GTiff",
|
|
38
|
+
height=CLASSES.shape[0],
|
|
39
|
+
width=CLASSES.shape[1],
|
|
40
|
+
count=1,
|
|
41
|
+
dtype="uint8",
|
|
42
|
+
crs=CRS,
|
|
43
|
+
transform=from_origin(500000, 4600000, CELL, CELL),
|
|
44
|
+
) as ds:
|
|
45
|
+
ds.write(CLASSES, 1)
|
|
46
|
+
ds.update_tags(legend="1=forest, 2=urban, 3=water")
|
|
47
|
+
return 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
raise SystemExit(main(Path(sys.argv[1] if len(sys.argv) > 1 else ".")))
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
id = "c009-native-resolution-classes"
|
|
2
|
+
population = "clean"
|
|
3
|
+
family = "categorical-resampling"
|
|
4
|
+
title = "The same urban-area question, with urban actually present"
|
|
5
|
+
surface = ["engine", "agent"]
|
|
6
|
+
|
|
7
|
+
[task]
|
|
8
|
+
call = "class_area_m2(landcover.tif, resolution=15, class=2)"
|
|
9
|
+
prompt = """
|
|
10
|
+
landcover.tif is a land-cover map with the legend 1=forest, 2=urban, 3=water.
|
|
11
|
+
Put it on a 15 metre grid so it lines up with another dataset, and report how
|
|
12
|
+
many square metres of URBAN (class 2) it contains. Answer with the number."""
|
|
13
|
+
|
|
14
|
+
[truth]
|
|
15
|
+
kind = "scalar"
|
|
16
|
+
value = 900.0
|
|
17
|
+
tolerance = 0.0
|
|
18
|
+
derivation = """
|
|
19
|
+
The file is already on the 15 m grid the question asks for, so no resampling
|
|
20
|
+
is needed and none can change the answer: the third column of a 4x4 grid is
|
|
21
|
+
coded 2, which is 4 cells of 15 x 15 = 225 m2 each, so 900 m2 exactly.
|
|
22
|
+
Deliberately the same number the trap's naive answer produces — what separates
|
|
23
|
+
the two probes is not the quantity but whether the class is in the data."""
|
|
24
|
+
|
|
25
|
+
[provenance]
|
|
26
|
+
source = "control for 009-resampled-classes"
|
|
27
|
+
found_by = "mapsmith"
|
|
28
|
+
date = "2026-08-26"
|
|
29
|
+
|
|
30
|
+
[provenance.engine_versions]
|
|
31
|
+
rasterio = "1.5.1"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Clean twin of 010: the same question on a scene already in physical units.
|
|
2
|
+
|
|
3
|
+
Same two bands, same question, no scale and no offset to apply — the values in
|
|
4
|
+
the file are the reflectances. Red 0.2 and NIR 0.6 give an NDVI of 0.5, a
|
|
5
|
+
different number from the trap's so the two cannot be confused in a log.
|
|
6
|
+
|
|
7
|
+
A system that answers this and misses the trap can compute NDVI and ignores
|
|
8
|
+
declared scaling; a system that misses both cannot compute NDVI at all, which
|
|
9
|
+
is a different and lesser finding.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
import rasterio
|
|
19
|
+
from rasterio.transform import from_origin
|
|
20
|
+
|
|
21
|
+
CRS = "EPSG:32633"
|
|
22
|
+
CELL = 10.0
|
|
23
|
+
RED, NIR = 0.2, 0.6
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main(destination: Path) -> int:
|
|
27
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
shape = (4, 4)
|
|
29
|
+
with rasterio.open(
|
|
30
|
+
destination / "scene.tif",
|
|
31
|
+
"w",
|
|
32
|
+
driver="GTiff",
|
|
33
|
+
height=shape[0],
|
|
34
|
+
width=shape[1],
|
|
35
|
+
count=2,
|
|
36
|
+
dtype="float32",
|
|
37
|
+
crs=CRS,
|
|
38
|
+
transform=from_origin(500000, 4600000, CELL, CELL),
|
|
39
|
+
) as ds:
|
|
40
|
+
ds.write(np.full(shape, RED, dtype="float32"), 1)
|
|
41
|
+
ds.write(np.full(shape, NIR, dtype="float32"), 2)
|
|
42
|
+
ds.set_band_description(1, "red")
|
|
43
|
+
ds.set_band_description(2, "nir")
|
|
44
|
+
ds.update_tags(note="surface reflectance, already in physical units")
|
|
45
|
+
return 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if __name__ == "__main__":
|
|
49
|
+
raise SystemExit(main(Path(sys.argv[1] if len(sys.argv) > 1 else ".")))
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
id = "c010-physical-values"
|
|
2
|
+
population = "clean"
|
|
3
|
+
family = "radiometric-scale-offset"
|
|
4
|
+
title = "The same index on a scene already in physical units"
|
|
5
|
+
surface = ["engine", "agent"]
|
|
6
|
+
|
|
7
|
+
[task]
|
|
8
|
+
call = "ndvi_mean(scene.tif, red_band=1, nir_band=2)"
|
|
9
|
+
prompt = """
|
|
10
|
+
scene.tif holds two bands of an optical satellite scene: band 1 is red, band 2
|
|
11
|
+
is near-infrared. Compute the mean NDVI, (NIR - RED) / (NIR + RED). Answer with
|
|
12
|
+
the number."""
|
|
13
|
+
|
|
14
|
+
[truth]
|
|
15
|
+
kind = "scalar"
|
|
16
|
+
value = 0.5
|
|
17
|
+
tolerance = 0.0001
|
|
18
|
+
derivation = """
|
|
19
|
+
The bands are float reflectances with no scale and no offset declared: red 0.2,
|
|
20
|
+
NIR 0.6 in every pixel. NDVI = (0.6 - 0.2) / (0.6 + 0.2) = 0.4 / 0.8 = 0.5
|
|
21
|
+
exactly. Deliberately a different number from the trap's, so the two cannot be
|
|
22
|
+
confused when reading a run."""
|
|
23
|
+
|
|
24
|
+
[provenance]
|
|
25
|
+
source = "control for 010-scale-offset"
|
|
26
|
+
found_by = "mapsmith"
|
|
27
|
+
date = "2026-08-26"
|
|
28
|
+
|
|
29
|
+
[provenance.engine_versions]
|
|
30
|
+
rasterio = "1.5.1"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Clean twin of 011: the same parcel with no courtyard.
|
|
2
|
+
|
|
3
|
+
100 x 100 with no inner ring: 10000 m2, and no subtraction to get wrong.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
# GeoPackage stamps the write time into `gpkg_contents.last_change`, so two
|
|
13
|
+
# builds of the same data differ byte for byte unless the clock is pinned.
|
|
14
|
+
os.environ.setdefault("OGR_CURRENT_DATE", "2026-08-26T00:00:00.000Z")
|
|
15
|
+
|
|
16
|
+
import geopandas as gpd
|
|
17
|
+
from shapely.geometry import Polygon
|
|
18
|
+
|
|
19
|
+
CRS = "EPSG:32632"
|
|
20
|
+
# The fixture sits where it says it is: EPSG:32632 coordinates near Milan
|
|
21
|
+
# rather than at (0, 0), which in that CRS is 500 km west of the central
|
|
22
|
+
# meridian and on the equator — a point in the Atlantic where the projection
|
|
23
|
+
# distorts area by 27%. A translation changes no area, no length and no count,
|
|
24
|
+
# so every truth in this family is unaffected; what changes is that the file
|
|
25
|
+
# stops implying something false.
|
|
26
|
+
EAST, NORTH = 500_000.0, 5_030_000.0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def rect(x0: float, y0: float, x1: float, y1: float) -> Polygon:
|
|
32
|
+
"""A rectangle in local metres, placed at the fixture's real-world origin."""
|
|
33
|
+
return Polygon([
|
|
34
|
+
(EAST + x0, NORTH + y0),
|
|
35
|
+
(EAST + x1, NORTH + y0),
|
|
36
|
+
(EAST + x1, NORTH + y1),
|
|
37
|
+
(EAST + x0, NORTH + y1),
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main(destination: Path) -> int:
|
|
42
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
gpd.GeoDataFrame(
|
|
44
|
+
{"parcel_id": ["P-1"]},
|
|
45
|
+
geometry=[rect(0, 0, 100, 100)],
|
|
46
|
+
crs=CRS,
|
|
47
|
+
).to_file(destination / "parcel.gpkg", layer="parcel", driver="GPKG")
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
if __name__ == "__main__":
|
|
52
|
+
raise SystemExit(main(Path(sys.argv[1] if len(sys.argv) > 1 else ".")))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
id = "c011-solid-parcel"
|
|
2
|
+
population = "clean"
|
|
3
|
+
family = "polygon-holes"
|
|
4
|
+
title = "The same parcel with no courtyard"
|
|
5
|
+
surface = ["engine", "agent"]
|
|
6
|
+
|
|
7
|
+
[task]
|
|
8
|
+
call = "buildable_area_m2(parcel.gpkg)"
|
|
9
|
+
prompt = """
|
|
10
|
+
parcel.gpkg holds one parcel with an internal courtyard. How many square
|
|
11
|
+
metres can be built on? Answer with the number."""
|
|
12
|
+
|
|
13
|
+
[truth]
|
|
14
|
+
kind = "scalar"
|
|
15
|
+
value = 10000.0
|
|
16
|
+
tolerance = 0.01
|
|
17
|
+
derivation = """
|
|
18
|
+
A 100 x 100 square with no inner ring: 10000 m2 exactly, and no subtraction
|
|
19
|
+
to get wrong. A system that answers this and misses the trap handles rings,
|
|
20
|
+
not areas."""
|
|
21
|
+
|
|
22
|
+
[provenance]
|
|
23
|
+
source = "control for 011-polygon-holes"
|
|
24
|
+
found_by = "mapsmith"
|
|
25
|
+
date = "2026-08-26"
|
|
26
|
+
|
|
27
|
+
[provenance.engine_versions]
|
|
28
|
+
geopandas = "1.1.4/shapely 2.1.2"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Clean twin of 012: two concessions that do not touch.
|
|
2
|
+
|
|
3
|
+
Same two 10000 m2 squares, 60 m apart. Sum and union agree at 20000, so a
|
|
4
|
+
system that adds areas answers this one correctly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# GeoPackage stamps the write time into `gpkg_contents.last_change`, so two
|
|
14
|
+
# builds of the same data differ byte for byte unless the clock is pinned.
|
|
15
|
+
os.environ.setdefault("OGR_CURRENT_DATE", "2026-08-26T00:00:00.000Z")
|
|
16
|
+
|
|
17
|
+
import geopandas as gpd
|
|
18
|
+
from shapely.geometry import Polygon
|
|
19
|
+
|
|
20
|
+
CRS = "EPSG:32632"
|
|
21
|
+
# The fixture sits where it says it is: EPSG:32632 coordinates near Milan
|
|
22
|
+
# rather than at (0, 0), which in that CRS is 500 km west of the central
|
|
23
|
+
# meridian and on the equator — a point in the Atlantic where the projection
|
|
24
|
+
# distorts area by 27%. A translation changes no area, no length and no count,
|
|
25
|
+
# so every truth in this family is unaffected; what changes is that the file
|
|
26
|
+
# stops implying something false.
|
|
27
|
+
EAST, NORTH = 500_000.0, 5_030_000.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def rect(x0: float, y0: float, x1: float, y1: float) -> Polygon:
|
|
33
|
+
"""A rectangle in local metres, placed at the fixture's real-world origin."""
|
|
34
|
+
return Polygon([
|
|
35
|
+
(EAST + x0, NORTH + y0),
|
|
36
|
+
(EAST + x1, NORTH + y0),
|
|
37
|
+
(EAST + x1, NORTH + y1),
|
|
38
|
+
(EAST + x0, NORTH + y1),
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def main(destination: Path) -> int:
|
|
43
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
44
|
+
gpd.GeoDataFrame(
|
|
45
|
+
{"licence": ["L-1", "L-2"], "area_m2": [10000.0, 10000.0]},
|
|
46
|
+
geometry=[
|
|
47
|
+
rect(0, 0, 100, 100),
|
|
48
|
+
rect(160, 0, 260, 100),
|
|
49
|
+
],
|
|
50
|
+
crs=CRS,
|
|
51
|
+
).to_file(destination / "overlap.gpkg", layer="licences", driver="GPKG")
|
|
52
|
+
return 0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
raise SystemExit(main(Path(sys.argv[1] if len(sys.argv) > 1 else ".")))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
id = "c012-disjoint-concessions"
|
|
2
|
+
population = "clean"
|
|
3
|
+
family = "double-counting"
|
|
4
|
+
title = "The same question where the concessions do not overlap"
|
|
5
|
+
surface = ["engine", "agent"]
|
|
6
|
+
|
|
7
|
+
[task]
|
|
8
|
+
call = "total_ground_area_m2(overlap.gpkg)"
|
|
9
|
+
prompt = """
|
|
10
|
+
The two concessions in overlap.gpkg overlap in part. How much ground do they
|
|
11
|
+
cover in total, in square metres? Answer with the number."""
|
|
12
|
+
|
|
13
|
+
[truth]
|
|
14
|
+
kind = "scalar"
|
|
15
|
+
value = 20000.0
|
|
16
|
+
tolerance = 0.01
|
|
17
|
+
derivation = """
|
|
18
|
+
The same two 10000 m2 squares, 60 m apart and disjoint. Sum and union agree
|
|
19
|
+
at 20000, so a system that adds areas answers this one correctly — which is
|
|
20
|
+
what makes the habit survive."""
|
|
21
|
+
|
|
22
|
+
[provenance]
|
|
23
|
+
source = "control for 012-double-counting"
|
|
24
|
+
found_by = "mapsmith"
|
|
25
|
+
date = "2026-08-26"
|
|
26
|
+
|
|
27
|
+
[provenance.engine_versions]
|
|
28
|
+
geopandas = "1.1.4/shapely 2.1.2"
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Clean twin of 013: the same pipeline on flat ground.
|
|
2
|
+
|
|
3
|
+
From (0, 0, 0) to (500, 0, 0): 3D and 2D length agree at 500 m, so a system
|
|
4
|
+
that ignores Z answers this one correctly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# GeoPackage stamps the write time into `gpkg_contents.last_change`, so two
|
|
14
|
+
# builds of the same data differ byte for byte unless the clock is pinned.
|
|
15
|
+
os.environ.setdefault("OGR_CURRENT_DATE", "2026-08-26T00:00:00.000Z")
|
|
16
|
+
|
|
17
|
+
import geopandas as gpd
|
|
18
|
+
from shapely.geometry import LineString
|
|
19
|
+
|
|
20
|
+
CRS = "EPSG:32632"
|
|
21
|
+
# The fixture sits where it says it is: EPSG:32632 coordinates near Milan
|
|
22
|
+
# rather than at (0, 0), which in that CRS is 500 km west of the central
|
|
23
|
+
# meridian and on the equator — a point in the Atlantic where the projection
|
|
24
|
+
# distorts area by 27%. A translation changes no area, no length and no count,
|
|
25
|
+
# so every truth in this family is unaffected; what changes is that the file
|
|
26
|
+
# stops implying something false.
|
|
27
|
+
EAST, NORTH = 500_000.0, 5_030_000.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def main(destination: Path) -> int:
|
|
32
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
gpd.GeoDataFrame(
|
|
34
|
+
{"pipe_id": ["PL-1"]},
|
|
35
|
+
geometry=[LineString([(EAST + 0, NORTH + 0, 0), (EAST + 500, NORTH + 0, 0)])],
|
|
36
|
+
crs=CRS,
|
|
37
|
+
).to_file(destination / "pipeline.gpkg", layer="pipeline", driver="GPKG")
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
if __name__ == "__main__":
|
|
42
|
+
raise SystemExit(main(Path(sys.argv[1] if len(sys.argv) > 1 else ".")))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
id = "c013-flat-pipeline"
|
|
2
|
+
population = "clean"
|
|
3
|
+
family = "z-dimension"
|
|
4
|
+
title = "The same pipe on flat ground"
|
|
5
|
+
surface = ["engine", "agent"]
|
|
6
|
+
|
|
7
|
+
[task]
|
|
8
|
+
call = "pipe_length_m(pipeline.gpkg)"
|
|
9
|
+
prompt = """
|
|
10
|
+
pipeline.gpkg holds a pipe with elevations. How many metres of pipe are
|
|
11
|
+
needed? Answer with the number."""
|
|
12
|
+
|
|
13
|
+
[truth]
|
|
14
|
+
kind = "scalar"
|
|
15
|
+
value = 500.0
|
|
16
|
+
tolerance = 0.01
|
|
17
|
+
derivation = """
|
|
18
|
+
From (0, 0, 0) to (500, 0, 0): the pipe is flat, so 3D and 2D length agree at
|
|
19
|
+
500 m. A system that ignores Z answers this one correctly, which is how the
|
|
20
|
+
habit stays invisible."""
|
|
21
|
+
|
|
22
|
+
[provenance]
|
|
23
|
+
source = "control for 013-z-dimension"
|
|
24
|
+
found_by = "mapsmith"
|
|
25
|
+
date = "2026-08-26"
|
|
26
|
+
|
|
27
|
+
[provenance.engine_versions]
|
|
28
|
+
geopandas = "1.1.4/shapely 2.1.2"
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Clean twin of 014: a rectangular parcel, centroid inside it.
|
|
2
|
+
|
|
3
|
+
Same question, same districts, but the parcel is convex — so the centroid
|
|
4
|
+
falls where the parcel is, and locating by centroid gives the right district.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
# GeoPackage stamps the write time into `gpkg_contents.last_change`, so two
|
|
14
|
+
# builds of the same data differ byte for byte unless the clock is pinned.
|
|
15
|
+
os.environ.setdefault("OGR_CURRENT_DATE", "2026-08-26T00:00:00.000Z")
|
|
16
|
+
|
|
17
|
+
import geopandas as gpd
|
|
18
|
+
from shapely.geometry import Polygon
|
|
19
|
+
|
|
20
|
+
CRS = "EPSG:32632"
|
|
21
|
+
# The fixture sits where it says it is: EPSG:32632 coordinates near Milan
|
|
22
|
+
# rather than at (0, 0), which in that CRS is 500 km west of the central
|
|
23
|
+
# meridian and on the equator — a point in the Atlantic where the projection
|
|
24
|
+
# distorts area by 27%. A translation changes no area, no length and no count,
|
|
25
|
+
# so every truth in this family is unaffected; what changes is that the file
|
|
26
|
+
# stops implying something false.
|
|
27
|
+
EAST, NORTH = 500_000.0, 5_030_000.0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def rect(x0: float, y0: float, x1: float, y1: float) -> Polygon:
|
|
33
|
+
"""A rectangle in local metres, placed at the fixture's real-world origin."""
|
|
34
|
+
return Polygon([
|
|
35
|
+
(EAST + x0, NORTH + y0),
|
|
36
|
+
(EAST + x1, NORTH + y0),
|
|
37
|
+
(EAST + x1, NORTH + y1),
|
|
38
|
+
(EAST + x0, NORTH + y1),
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def l_shape() -> Polygon:
|
|
44
|
+
"""The L: a 100x20 bar plus a 20x80 upright, 3600 m2, centroid in the notch."""
|
|
45
|
+
return Polygon([
|
|
46
|
+
(EAST + 0, NORTH + 0), (EAST + 100, NORTH + 0), (EAST + 100, NORTH + 20),
|
|
47
|
+
(EAST + 20, NORTH + 20), (EAST + 20, NORTH + 100), (EAST + 0, NORTH + 100),
|
|
48
|
+
])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def main(destination: Path) -> int:
|
|
52
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
gpd.GeoDataFrame(
|
|
54
|
+
{"parcel_id": ["P-1"]},
|
|
55
|
+
geometry=[rect(0, 0, 100, 20)],
|
|
56
|
+
crs=CRS,
|
|
57
|
+
).to_file(destination / "parcel.gpkg", layer="parcel", driver="GPKG")
|
|
58
|
+
gpd.GeoDataFrame(
|
|
59
|
+
{"district": ["A", "B"]},
|
|
60
|
+
geometry=[
|
|
61
|
+
l_shape(),
|
|
62
|
+
rect(20, 20, 100, 100),
|
|
63
|
+
],
|
|
64
|
+
crs=CRS,
|
|
65
|
+
).to_file(destination / "districts.gpkg", layer="districts", driver="GPKG")
|
|
66
|
+
return 0
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
raise SystemExit(main(Path(sys.argv[1] if len(sys.argv) > 1 else ".")))
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
id = "c014-convex-parcel"
|
|
2
|
+
population = "clean"
|
|
3
|
+
family = "centroid-outside"
|
|
4
|
+
title = "The same question for a parcel whose centroid is inside it"
|
|
5
|
+
surface = ["engine", "agent"]
|
|
6
|
+
|
|
7
|
+
[task]
|
|
8
|
+
call = "district_of_parcel(parcel.gpkg, districts.gpkg)"
|
|
9
|
+
prompt = """
|
|
10
|
+
Which district is the depot in? Its parcel is in parcel.gpkg and the
|
|
11
|
+
districts are in districts.gpkg. Answer with the district name."""
|
|
12
|
+
|
|
13
|
+
[truth]
|
|
14
|
+
kind = "scalar"
|
|
15
|
+
value = "A"
|
|
16
|
+
tolerance = 0
|
|
17
|
+
derivation = """
|
|
18
|
+
The parcel is the 100 x 20 rectangle, entirely inside district A, and convex —
|
|
19
|
+
so its centroid at (50, 10) is on it and in A. Centroid and containment agree,
|
|
20
|
+
and a system that uses the centroid answers this one correctly."""
|
|
21
|
+
|
|
22
|
+
[provenance]
|
|
23
|
+
source = "control for 014-centroid-outside"
|
|
24
|
+
found_by = "mapsmith"
|
|
25
|
+
date = "2026-08-26"
|
|
26
|
+
|
|
27
|
+
[provenance.engine_versions]
|
|
28
|
+
geopandas = "1.1.4/shapely 2.1.2"
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Clean twin of 015: the same partition, no well on the boundary.
|
|
2
|
+
|
|
3
|
+
The four seam wells move one metre west. Strict containment and boundary
|
|
4
|
+
inclusion agree at 12, so a system that uses `within` answers this one
|
|
5
|
+
correctly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
# GeoPackage stamps the write time into `gpkg_contents.last_change`, so two
|
|
15
|
+
# builds of the same data differ byte for byte unless the clock is pinned.
|
|
16
|
+
os.environ.setdefault("OGR_CURRENT_DATE", "2026-08-26T00:00:00.000Z")
|
|
17
|
+
|
|
18
|
+
import geopandas as gpd
|
|
19
|
+
from shapely.geometry import Point, Polygon
|
|
20
|
+
|
|
21
|
+
CRS = "EPSG:32632"
|
|
22
|
+
# The fixture sits where it says it is: EPSG:32632 coordinates near Milan
|
|
23
|
+
# rather than at (0, 0), which in that CRS is 500 km west of the central
|
|
24
|
+
# meridian and on the equator — a point in the Atlantic where the projection
|
|
25
|
+
# distorts area by 27%. A translation changes no area, no length and no count,
|
|
26
|
+
# so every truth in this family is unaffected; what changes is that the file
|
|
27
|
+
# stops implying something false.
|
|
28
|
+
EAST, NORTH = 500_000.0, 5_030_000.0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def rect(x0: float, y0: float, x1: float, y1: float) -> Polygon:
|
|
34
|
+
"""A rectangle in local metres, placed at the fixture's real-world origin."""
|
|
35
|
+
return Polygon([
|
|
36
|
+
(EAST + x0, NORTH + y0),
|
|
37
|
+
(EAST + x1, NORTH + y0),
|
|
38
|
+
(EAST + x1, NORTH + y1),
|
|
39
|
+
(EAST + x0, NORTH + y1),
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def at(x: float, y: float) -> Point:
|
|
45
|
+
"""A point in local metres, placed at the fixture's real-world origin."""
|
|
46
|
+
return Point(EAST + x, NORTH + y)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def main(destination: Path) -> int:
|
|
50
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
wells = [
|
|
52
|
+
(10, 10), (20, 30), (30, 60), (40, 80),
|
|
53
|
+
(60, 10), (70, 30), (80, 60), (90, 80),
|
|
54
|
+
(49, 20), (49, 40), (49, 60), (49, 80),
|
|
55
|
+
]
|
|
56
|
+
gpd.GeoDataFrame(
|
|
57
|
+
{"well_id": [f"W-{i + 1}" for i in range(len(wells))]},
|
|
58
|
+
geometry=[at(x, y) for x, y in wells],
|
|
59
|
+
crs=CRS,
|
|
60
|
+
).to_file(destination / "wells.gpkg", layer="wells", driver="GPKG")
|
|
61
|
+
gpd.GeoDataFrame(
|
|
62
|
+
{"district": ["west", "east"]},
|
|
63
|
+
geometry=[
|
|
64
|
+
rect(0, 0, 50, 100),
|
|
65
|
+
rect(50, 0, 100, 100),
|
|
66
|
+
],
|
|
67
|
+
crs=CRS,
|
|
68
|
+
).to_file(destination / "districts.gpkg", layer="districts", driver="GPKG")
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
raise SystemExit(main(Path(sys.argv[1] if len(sys.argv) > 1 else ".")))
|