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,64 @@
|
|
|
1
|
+
id = "007-buffer-in-degrees"
|
|
2
|
+
population = "trap"
|
|
3
|
+
family = "implicit-parameter-units"
|
|
4
|
+
title = "A distance of 500, applied in the layer's units instead of the question's"
|
|
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 = 3
|
|
16
|
+
tolerance = 0
|
|
17
|
+
derivation = """
|
|
18
|
+
By construction, in the local metric at 41.9 degrees north (1 degree of
|
|
19
|
+
latitude = 111.1 km, 1 degree of longitude = 82.9 km): three wells lie ~300 m
|
|
20
|
+
north, ~398 m east and ~359 m south-west of W-1; every other well lies at
|
|
21
|
+
least ~4 km away (0.05 degrees in both axes; nearest excluded measured at
|
|
22
|
+
6,932 m). The margins carry the derivation: at these distances a flat-earth
|
|
23
|
+
approximation, a geodesic, and a UTM-planar distance agree to within
|
|
24
|
+
centimetres, while the gap between the farthest included well (398 m) and the
|
|
25
|
+
nearest excluded one (6.9 km) is three orders of magnitude wider than any
|
|
26
|
+
method disagreement. The count is 3 under every correct reading; tolerance 0."""
|
|
27
|
+
|
|
28
|
+
[naive_failure]
|
|
29
|
+
description = """
|
|
30
|
+
Buffer W-1 by 500 and count the wells inside. Shapely, GeoPandas and PostGIS
|
|
31
|
+
all buffer in the LAYER'S units by design — here degrees — and none of them
|
|
32
|
+
can know the caller meant metres. A buffer of 500 degrees spans the planet
|
|
33
|
+
several times over: every other well in the file falls inside, and the count
|
|
34
|
+
comes back 24. No exception, no warning — the buffer is a perfectly valid
|
|
35
|
+
polygon."""
|
|
36
|
+
observed_value = 24
|
|
37
|
+
plausible = true
|
|
38
|
+
why_plausible = """
|
|
39
|
+
"24 wells within half a kilometre" is an ordinary number for a dense urban
|
|
40
|
+
wellfield — nothing about it looks broken, and a count carries no trace of the
|
|
41
|
+
absurd geometry that produced it. The file is honest, the CRS is declared, the
|
|
42
|
+
figure 500 is exactly what the question said. The only thing implicit was the
|
|
43
|
+
unit, and the code applied it in the one the layer happened to use."""
|
|
44
|
+
derivation = """
|
|
45
|
+
Closed form: a buffer of 500 (degrees) around (12.40, 41.90) spans longitudes
|
|
46
|
+
-487.6 to 512.4 and latitudes -458.1 to 541.9 — the whole coordinate space of
|
|
47
|
+
the file. All 24 other wells fall inside; the count is exactly 24."""
|
|
48
|
+
|
|
49
|
+
[refusal]
|
|
50
|
+
accept_if_mentions = ["degree", "geographic", "crs", "unit", "reproject", "utm", "4326"]
|
|
51
|
+
|
|
52
|
+
[provenance]
|
|
53
|
+
source = """
|
|
54
|
+
The buffer-units confusion is the canonical first question of working with
|
|
55
|
+
geographic coordinates: GeoPandas' own documentation states that buffer
|
|
56
|
+
distances are expressed in the units of the CRS and tells users to project
|
|
57
|
+
first, and 'buffer in meters with lat/lon data' is among the most-asked
|
|
58
|
+
GIS Stack Exchange questions. The trap plants the case where the wrong answer
|
|
59
|
+
is a plausible COUNT rather than a planet-sized area nobody would believe."""
|
|
60
|
+
found_by = "mapsmith"
|
|
61
|
+
date = "2026-08-25"
|
|
62
|
+
|
|
63
|
+
[provenance.engine_versions]
|
|
64
|
+
geopandas = "1.1.4/shapely 2.1.2 - buffer in the coordinates' own units, by design; no unit awareness"
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# 008 — Metres of map read as metres of ground
|
|
2
|
+
|
|
3
|
+
## The file
|
|
4
|
+
|
|
5
|
+
`parcel.gpkg` holds one land parcel in **EPSG:3857** (Web Mercator), an exact
|
|
6
|
+
120 × 100 rectangle in the map plane at the latitude of Rome. Everything about
|
|
7
|
+
the file is honest: valid geometry, declared CRS, and — unlike the
|
|
8
|
+
feet-as-metres trap — the CRS's unit really **is** the metre. What the unit
|
|
9
|
+
does not say is metres *of what*.
|
|
10
|
+
|
|
11
|
+
## The right answer, on paper
|
|
12
|
+
|
|
13
|
+
EPSG:3857 is defined on a sphere of radius R = 6378137 m, and on that sphere
|
|
14
|
+
the ground area of a map rectangle has a closed form:
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
width × R × (tanh(y₂/R) − tanh(y₁/R))
|
|
18
|
+
= 120 × 6378137 × (tanh(5140100/6378137) − tanh(5140000/6378137))
|
|
19
|
+
= 6656.30 m² (equivalently: 12000 × cos²(41.8601°))
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
On the WGS84 ellipsoid, the geodesic area of the same footprint is
|
|
23
|
+
**6651.34 m²** (0.075% below the sphere); a UTM 33N planar measurement gives
|
|
24
|
+
**6653.65 m²**. Every correct method lands in a 5 m² band; the truth is pinned
|
|
25
|
+
at **6654 ± 8**, which spans them all. The map-plane answer misses the band by
|
|
26
|
+
668 tolerances.
|
|
27
|
+
|
|
28
|
+
## The wrong answer
|
|
29
|
+
|
|
30
|
+
Read the file, sum `.area`, report it: **12000 m²** — 1.80× the ground.
|
|
31
|
+
|
|
32
|
+
Web Mercator is conformal, not equal-area. At latitude φ its linear scale is
|
|
33
|
+
1/cos φ (1.34 at 41.86°N), so every planar area is 1/cos²φ times the ground it
|
|
34
|
+
covers. The shoelace over the coordinates is arithmetically exact — in the
|
|
35
|
+
wrong plane. No exception, no warning: the CRS declared metres and delivered
|
|
36
|
+
metres, of map.
|
|
37
|
+
|
|
38
|
+
## Why it is admitted
|
|
39
|
+
|
|
40
|
+
Both numbers are ordinary parcels — 1.2 hectares against 0.67 — and nothing in
|
|
41
|
+
any single number betrays the smooth, latitude-dependent factor between them.
|
|
42
|
+
EPSG:3857 is the CRS every web-exported dataset ships in; the EPSG registry
|
|
43
|
+
itself remarks the projection is "not a recognised geodetic system", and a 2014
|
|
44
|
+
NGA advisory bans it for mission use over errors "of up to 40,000 meters".
|
|
45
|
+
This is feet-as-metres one step deeper: there the unit label lied; here the
|
|
46
|
+
label is true and the plane it measures is not the ground.
|
|
47
|
+
|
|
48
|
+
## The clean twin
|
|
49
|
+
|
|
50
|
+
[c008-equal-area-crs](../../clean/c008-equal-area-crs/) asks the same question
|
|
51
|
+
about the same kind of file in **EPSG:3035** (Lambert Azimuthal Equal-Area),
|
|
52
|
+
where the planar shoelace *is* the ground area — 10000 m² exactly, by
|
|
53
|
+
construction. The composition that falls into this trap answers the twin
|
|
54
|
+
correctly, which is what separates "mishandles distortion" from "cannot
|
|
55
|
+
measure an area".
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Build a parcel stored in EPSG:3857, where every coordinate is in metres.
|
|
2
|
+
|
|
3
|
+
The parcel is an exact 120 x 100 rectangle in the map plane, sitting at the
|
|
4
|
+
latitude of Rome. Web Mercator's metres are real metres of MAP: at 41.86 north
|
|
5
|
+
one metre of map is cos(41.86) metres of ground, so the 12,000 m2 the plane
|
|
6
|
+
shows covers 6,656 m2 of land. The file is honest, the CRS is declared, and
|
|
7
|
+
its unit really is the metre. What the unit does not say is metres of what.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
# GeoPackage stamps the write time into `gpkg_contents.last_change`, so two
|
|
17
|
+
# builds of the same data differ byte for byte unless the clock is pinned.
|
|
18
|
+
os.environ.setdefault("OGR_CURRENT_DATE", "2026-08-25T00:00:00.000Z")
|
|
19
|
+
|
|
20
|
+
import geopandas as gpd
|
|
21
|
+
from shapely.geometry import box
|
|
22
|
+
|
|
23
|
+
CRS = "EPSG:3857"
|
|
24
|
+
X0, WIDTH = 1_380_000.0, 120.0 # ~12.40E
|
|
25
|
+
Y0, HEIGHT = 5_140_000.0, 100.0 # ~41.86N
|
|
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 + WIDTH, Y0 + HEIGHT)],
|
|
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,68 @@
|
|
|
1
|
+
id = "008-web-mercator-area"
|
|
2
|
+
population = "trap"
|
|
3
|
+
family = "projection-distortion"
|
|
4
|
+
title = "Metres of map read as metres of ground"
|
|
5
|
+
surface = ["engine", "agent"]
|
|
6
|
+
|
|
7
|
+
[task]
|
|
8
|
+
call = "ground_area_m2(parcel.gpkg)"
|
|
9
|
+
prompt = """
|
|
10
|
+
parcel.gpkg holds one land parcel near Rome. How large is the parcel on the
|
|
11
|
+
ground, in square metres? Answer with the number."""
|
|
12
|
+
|
|
13
|
+
[truth]
|
|
14
|
+
kind = "scalar"
|
|
15
|
+
value = 6654.0
|
|
16
|
+
tolerance = 8.0
|
|
17
|
+
derivation = """
|
|
18
|
+
The parcel is an exact 120 x 100 m rectangle in the EPSG:3857 map plane, at
|
|
19
|
+
y in [5140000, 5140100] (latitude 41.8598 to 41.8605 north). On the sphere
|
|
20
|
+
EPSG:3857 is defined on (R = 6378137), the ground area of a map rectangle has
|
|
21
|
+
a closed form: width x R x (tanh(y2/R) - tanh(y1/R)) = 120 x 6378137 x
|
|
22
|
+
(tanh(5140100/6378137) - tanh(5140000/6378137)) = 6656.30 m2 — equivalently,
|
|
23
|
+
12000 x cos2(41.8601) = 6656.30. On the WGS84 ellipsoid the geodesic area of
|
|
24
|
+
the same footprint is 6651.34 m2 (0.075% below the sphere); a UTM 33N planar
|
|
25
|
+
measurement gives 6653.65 m2. The truth 6654 +/- 8 spans every correct
|
|
26
|
+
method, whose whole spread is 5 m2; the map-plane answer 12000 misses the
|
|
27
|
+
band by 668 tolerances."""
|
|
28
|
+
|
|
29
|
+
[naive_failure]
|
|
30
|
+
description = """
|
|
31
|
+
Read the file, sum .area, report it: the coordinates are in metres — the CRS
|
|
32
|
+
says so, and for once the label is telling the truth — so the shoelace over
|
|
33
|
+
them comes back 12000 and gets reported as square metres of land. Web Mercator
|
|
34
|
+
is conformal, not equal-area: at latitude 41.86 its linear scale is 1/cos(lat)
|
|
35
|
+
= 1.34, so every planar area is 1.80 times the ground it covers. No exception,
|
|
36
|
+
no warning — the arithmetic is exact, in the wrong plane."""
|
|
37
|
+
observed_value = 12000.0
|
|
38
|
+
plausible = true
|
|
39
|
+
why_plausible = """
|
|
40
|
+
Both numbers are ordinary parcels — 1.2 hectares against 0.67 — and nothing
|
|
41
|
+
in the file tips the reader off: the geometry is valid, the CRS is declared
|
|
42
|
+
and common (every web-exported dataset ships in 3857), and the unit really is
|
|
43
|
+
the metre. The factor between wrong and right is cos2(latitude), which is
|
|
44
|
+
smooth, latitude-dependent, and invisible in any single number. This is the
|
|
45
|
+
same failure as feet-read-as-metres one step deeper: there the unit label
|
|
46
|
+
lied; here the label is true and the plane it measures is not the ground."""
|
|
47
|
+
derivation = """
|
|
48
|
+
Closed form: the rectangle is exactly 120 x 100 in map coordinates, so the
|
|
49
|
+
planar shoelace is exactly 12000 whatever the vertex order. GeoPandas .area
|
|
50
|
+
computes precisely that."""
|
|
51
|
+
|
|
52
|
+
[refusal]
|
|
53
|
+
accept_if_mentions = ["mercator", "3857", "distortion", "cos", "equal-area", "reproject", "not equal area", "web mercator"]
|
|
54
|
+
|
|
55
|
+
[provenance]
|
|
56
|
+
source = """
|
|
57
|
+
The EPSG registry entry for 3857 carries the remark that the projection is
|
|
58
|
+
'not a recognised geodetic system' and 'uses spherical development of
|
|
59
|
+
ellipsoidal coordinates'; NGA advisory notice on web Mercator (2014) warns
|
|
60
|
+
that it 'may cause geo-location / geo-coordinate errors of up to 40,000
|
|
61
|
+
meters' and bans it for DoD mission use; 'why is my area wrong in 3857' is a
|
|
62
|
+
perennial GIS Stack Exchange genre. The trap plants the case where the wrong
|
|
63
|
+
area is an ordinary parcel rather than an absurd one."""
|
|
64
|
+
found_by = "mapsmith"
|
|
65
|
+
date = "2026-08-25"
|
|
66
|
+
|
|
67
|
+
[provenance.engine_versions]
|
|
68
|
+
geopandas = "1.1.4/shapely 2.1.2 - .area is the planar shoelace in the CRS plane, by design"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# 009 — A class that is not in the file, produced by resampling
|
|
2
|
+
|
|
3
|
+
## The file
|
|
4
|
+
|
|
5
|
+
`landcover.tif` is a 3×3 grid of 20 m cells in EPSG:32633, with the legend
|
|
6
|
+
`1=forest, 2=urban, 3=water` written into its tags. The two west columns are
|
|
7
|
+
forest, the east column is water. **No cell is urban.**
|
|
8
|
+
|
|
9
|
+
The question asks for the urban area on a **15 m** grid — the resolution of
|
|
10
|
+
another dataset it has to line up with, which is the ordinary reason anyone
|
|
11
|
+
resamples anything.
|
|
12
|
+
|
|
13
|
+
## The right answer, on paper
|
|
14
|
+
|
|
15
|
+
The file contains exactly two values, 1 and 3. A class code is a label, not a
|
|
16
|
+
measurement, so no resampling rule that is valid for labels can produce a label
|
|
17
|
+
that was not there: nearest neighbour and mode both *select* one of the
|
|
18
|
+
contributing cells' values, so the output alphabet is a subset of the input
|
|
19
|
+
alphabet `{1, 3}`.
|
|
20
|
+
|
|
21
|
+
**The urban area is exactly 0 m².** Tolerance 0 — this is not a measurement with
|
|
22
|
+
error bars, it is the extent of something that does not exist. The answer holds
|
|
23
|
+
at any target resolution, which is what makes the derivation robust: it does not
|
|
24
|
+
depend on how the grids happen to line up.
|
|
25
|
+
|
|
26
|
+
## The wrong answer
|
|
27
|
+
|
|
28
|
+
Resample with a method that produces smooth output: **900 m² of urban.**
|
|
29
|
+
|
|
30
|
+
With a 60 m extent and 15 m cells the output is 4×4, and the new cell centres
|
|
31
|
+
fall *between* the old ones. Across the forest/water boundary the interpolated
|
|
32
|
+
value is the average of 1 and 3 — which is 2. Four cells of 225 m² come back
|
|
33
|
+
coded urban.
|
|
34
|
+
|
|
35
|
+
Measured on rasterio 1.5.1, three methods do this and two do not:
|
|
36
|
+
|
|
37
|
+
| method | urban cells | urban area | distinct codes |
|
|
38
|
+
|---|---|---|---|
|
|
39
|
+
| `nearest` | 0 | 0 m² | 1, 3 |
|
|
40
|
+
| `mode` | 0 | 0 m² | 1, 3 |
|
|
41
|
+
| **`bilinear`** | **4** | **900 m²** | 1, **2**, 3 |
|
|
42
|
+
| **`cubic`** | **4** | **900 m²** | 1, **2**, 3 |
|
|
43
|
+
| **`average`** | **4** | **900 m²** | 1, **2**, 3 |
|
|
44
|
+
|
|
45
|
+
## Why it is admitted
|
|
46
|
+
|
|
47
|
+
900 m² of urban between a forest and a lake is not a suspicious number: it is
|
|
48
|
+
what a road, a car park or a few buildings on a shoreline look like — and
|
|
49
|
+
shorelines are exactly where settlements are. Nothing in the result hints that
|
|
50
|
+
the value was derived rather than observed: the raster is well formed, every
|
|
51
|
+
code is in the legend, and the areas still add up.
|
|
52
|
+
|
|
53
|
+
The tell-tale would be the code 2 appearing where the input had none, and no
|
|
54
|
+
raster library reports that. rasterio's documentation recommends bilinear and
|
|
55
|
+
cubic for *"continuous data"* and carries no warning anywhere about categorical
|
|
56
|
+
data — reasonably, because **nothing in a GeoTIFF says which of the two it is
|
|
57
|
+
holding**. The rule "use nearest or majority for discrete data" is in every
|
|
58
|
+
manual of every toolkit that offers the method (GDAL's `-r mode` among
|
|
59
|
+
them) and is checked by none of them.
|
|
60
|
+
|
|
61
|
+
The trap plants the case where the invented code is a class that *exists in the
|
|
62
|
+
legend*, so the wrong answer is a plausible quantity rather than an obvious
|
|
63
|
+
artefact. A `1.5` would have been noticed; a `2` is urban.
|
|
64
|
+
|
|
65
|
+
## The clean twin
|
|
66
|
+
|
|
67
|
+
[c009-native-resolution-classes](../../clean/c009-native-resolution-classes/)
|
|
68
|
+
asks the same question about a file that is already on the 15 m grid and has
|
|
69
|
+
four genuinely urban cells: **900 m², for real**. Deliberately the same number
|
|
70
|
+
the trap's naive answer produces — what separates the two probes is not the
|
|
71
|
+
quantity but whether the class is in the data.
|
|
72
|
+
|
|
73
|
+
## Observed
|
|
74
|
+
|
|
75
|
+
| system | answer | verdict |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| naive composition | 900.0 | silent error |
|
|
78
|
+
| rasterio 1.5.1 (informed) | 0.0 | correct |
|
|
79
|
+
| MapSmith | 0.0 | correct |
|
|
80
|
+
|
|
81
|
+
MapSmith's pass is earned in a specific and narrow way, worth stating because
|
|
82
|
+
the suite exists to keep its authors honest: `resample_raster` has **no default
|
|
83
|
+
method**, so the composition had to choose one, and a caller who has been told
|
|
84
|
+
the legend chooses a categorical method. Had it chosen `bilinear` anyway, the
|
|
85
|
+
result would have come back carrying `invented_values: [2.0]` and a check named
|
|
86
|
+
`no_invented_class_codes` — not a refusal, a report. The defence is that the
|
|
87
|
+
question is asked, and that the answer is checked against the input's own
|
|
88
|
+
alphabet afterwards.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Build a land-cover raster that must be resampled to answer the question.
|
|
2
|
+
|
|
3
|
+
Two classes in a 3x3 grid of 20 m cells: the two west columns are forest (1),
|
|
4
|
+
the east column is water (3). The legend also defines 2 = urban, and no cell
|
|
5
|
+
in this file carries it.
|
|
6
|
+
|
|
7
|
+
The question asks for the urban area on a 15 m grid — the resolution of a
|
|
8
|
+
different dataset it has to line up with. 60 m of extent divides into four
|
|
9
|
+
15 m cells, and the new cell centres fall between the old ones, which is the
|
|
10
|
+
ordinary case whenever two grids have to meet.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import rasterio
|
|
20
|
+
from rasterio.transform import from_origin
|
|
21
|
+
|
|
22
|
+
CRS = "EPSG:32633"
|
|
23
|
+
CELL = 20.0
|
|
24
|
+
CLASSES = np.array([[1, 1, 3], [1, 1, 3], [1, 1, 3]], dtype="uint8")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main(destination: Path) -> int:
|
|
28
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
29
|
+
with rasterio.open(
|
|
30
|
+
destination / "landcover.tif",
|
|
31
|
+
"w",
|
|
32
|
+
driver="GTiff",
|
|
33
|
+
height=CLASSES.shape[0],
|
|
34
|
+
width=CLASSES.shape[1],
|
|
35
|
+
count=1,
|
|
36
|
+
dtype="uint8",
|
|
37
|
+
crs=CRS,
|
|
38
|
+
transform=from_origin(500000, 4600000, CELL, CELL),
|
|
39
|
+
) as ds:
|
|
40
|
+
ds.write(CLASSES, 1)
|
|
41
|
+
ds.update_tags(legend="1=forest, 2=urban, 3=water")
|
|
42
|
+
return 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
raise SystemExit(main(Path(sys.argv[1] if len(sys.argv) > 1 else ".")))
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
id = "009-resampled-classes"
|
|
2
|
+
population = "trap"
|
|
3
|
+
family = "categorical-resampling"
|
|
4
|
+
title = "A class that is not in the file, produced by resampling"
|
|
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 = 0.0
|
|
17
|
+
tolerance = 0.0
|
|
18
|
+
derivation = """
|
|
19
|
+
The file contains exactly two values, 1 and 3 — verifiable by reading it. A
|
|
20
|
+
class code is a label, not a measurement, so no resampling rule that is valid
|
|
21
|
+
for labels can produce a label that was not there: nearest neighbour and mode
|
|
22
|
+
both select one of the contributing cells' values, so the output alphabet is a
|
|
23
|
+
subset of the input alphabet {1, 3}. Class 2 is therefore absent from every
|
|
24
|
+
correct answer, at any target resolution, and the urban area is exactly zero.
|
|
25
|
+
Tolerance 0: this is not a measurement with error bars, it is the extent of
|
|
26
|
+
something that does not exist."""
|
|
27
|
+
|
|
28
|
+
[naive_failure]
|
|
29
|
+
description = """
|
|
30
|
+
Resample with the method that produces smooth output — bilinear, cubic or
|
|
31
|
+
average, all three of which are the recommended choice for continuous rasters
|
|
32
|
+
and are what a pipeline configured once for elevation applies to everything
|
|
33
|
+
that passes through it. The 15 m cell centres fall between the 20 m ones, so
|
|
34
|
+
across the forest/water boundary the interpolated value is the average of 1
|
|
35
|
+
and 3. That is 2. Four cells of 225 m2 come back as urban: 900 m2 of a class
|
|
36
|
+
that is not in the source. No exception, no warning — the output is a valid
|
|
37
|
+
GeoTIFF whose values are all legal class codes."""
|
|
38
|
+
observed_value = 900.0
|
|
39
|
+
plausible = true
|
|
40
|
+
why_plausible = """
|
|
41
|
+
900 m2 of urban between a forest and a lake is not a suspicious number: it is
|
|
42
|
+
what a road, a car park or a few buildings on a shoreline look like, and
|
|
43
|
+
shorelines are exactly where settlements are. Nothing in the result hints that
|
|
44
|
+
the value was derived rather than observed — the raster is well formed, every
|
|
45
|
+
code is in the legend, and the areas still add up. The tell-tale would be the
|
|
46
|
+
value 2 appearing where the input had none, and no raster library reports
|
|
47
|
+
that: rasterio's own documentation recommends bilinear and cubic for
|
|
48
|
+
'continuous data' and never warns about categorical data, because nothing in a
|
|
49
|
+
GeoTIFF says which of the two it is holding."""
|
|
50
|
+
derivation = """
|
|
51
|
+
Closed form: with a 60 m extent and 15 m cells the output is 4x4. Bilinear
|
|
52
|
+
interpolation at the new cell centres averages the neighbouring 20 m cells;
|
|
53
|
+
across the forest/water boundary that is (1+3)/2 = 2, stored in the uint8 band
|
|
54
|
+
as 2. Measured on rasterio 1.5.1: bilinear, cubic and average each give
|
|
55
|
+
exactly 4 cells of value 2 (4 x 225 = 900 m2), while nearest and mode give 0."""
|
|
56
|
+
|
|
57
|
+
[refusal]
|
|
58
|
+
accept_if_mentions = ["categorical", "class code", "nearest", "mode", "interpolat", "discrete", "legend"]
|
|
59
|
+
|
|
60
|
+
[provenance]
|
|
61
|
+
source = """
|
|
62
|
+
That categorical rasters must be resampled with nearest neighbour or majority
|
|
63
|
+
is stated in the documentation of the toolkits that offer the method (GDAL's
|
|
64
|
+
warp documentation offers 'mode' for thematic rasters) and is a standing
|
|
65
|
+
question on GIS Stack Exchange. What no
|
|
66
|
+
library does is CHECK it: rasterio 1.5.1 carries no warning about bilinear on
|
|
67
|
+
categorical data anywhere in its source or docs, and recommends bilinear and
|
|
68
|
+
cubic for continuous data without saying how a caller is meant to tell the
|
|
69
|
+
difference. The trap plants the case where the invented code is a class that
|
|
70
|
+
exists in the legend, so the wrong answer is a plausible quantity rather than
|
|
71
|
+
an obvious artefact."""
|
|
72
|
+
found_by = "mapsmith"
|
|
73
|
+
date = "2026-08-26"
|
|
74
|
+
|
|
75
|
+
[provenance.engine_versions]
|
|
76
|
+
rasterio = "1.5.1 - read(out_shape=...) accepts nine of the fifteen Resampling members; no categorical-data warning exists"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# 010 — The band was stored, not measured
|
|
2
|
+
|
|
3
|
+
## The file
|
|
4
|
+
|
|
5
|
+
`scene.tif` holds two bands of an optical scene: band 1 red, band 2
|
|
6
|
+
near-infrared, both `uint16` digital numbers. The file **declares its own
|
|
7
|
+
calibration** in its metadata: scale `0.0001`, offset `-0.1` on both bands.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
physical = raw × scale + offset (the GDAL definition, and the one in the file)
|
|
11
|
+
red = 3000 × 0.0001 − 0.1 = 0.2
|
|
12
|
+
nir = 5000 × 0.0001 − 0.1 = 0.4
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## The right answer, on paper
|
|
16
|
+
|
|
17
|
+
NDVI = (NIR − RED) / (NIR + RED) = (0.4 − 0.2) / (0.4 + 0.2) = 0.2 / 0.6 =
|
|
18
|
+
**1/3 exactly**. Every pixel carries the same pair, so the mean is that number
|
|
19
|
+
and no averaging error enters. Tolerance 1e-4, which covers float32 storage and
|
|
20
|
+
nothing else.
|
|
21
|
+
|
|
22
|
+
## The wrong answer
|
|
23
|
+
|
|
24
|
+
Read the two bands, put them in the formula: **0.25**.
|
|
25
|
+
|
|
26
|
+
GDAL's raster data model states it plainly — *"applying scale and offset is of
|
|
27
|
+
the responsibility of the user, and is not done by methods such as RasterIO()
|
|
28
|
+
or ReadBlock()"*. The arrays that come back hold 3000 and 5000. The index comes
|
|
29
|
+
out (5000−3000)/(5000+3000) = 0.25, and nothing in an array says it is not a
|
|
30
|
+
reflectance.
|
|
31
|
+
|
|
32
|
+
## Why it is admitted
|
|
33
|
+
|
|
34
|
+
0.25 and 0.3333 are both ordinary NDVI values for vegetation. Neither is out of
|
|
35
|
+
range, neither looks like an artefact, and the gap between them is the sort
|
|
36
|
+
that separates a stressed crop from a healthy one — not a bug from a result.
|
|
37
|
+
|
|
38
|
+
The deeper reason this survives everywhere is worth stating, because it
|
|
39
|
+
explains why careful people have this bug:
|
|
40
|
+
|
|
41
|
+
> **Without an offset, NDVI is invariant to the scale factor.** The scale
|
|
42
|
+
> cancels in the ratio. Code that divided digital numbers by 10000 — or skipped
|
|
43
|
+
> the conversion entirely — produced exactly the right answer, for years.
|
|
44
|
+
|
|
45
|
+
Then ESA introduced `BOA_ADD_OFFSET` (−1000 DN) with Sentinel-2 processing
|
|
46
|
+
baseline 04.00 on **25 January 2022**. The invariance stopped holding. The same
|
|
47
|
+
unchanged pipeline now returns one number for a scene acquired on 24 January
|
|
48
|
+
and a different one for the same ground on 26 January, with no error anywhere,
|
|
49
|
+
and the difference is not a constant — it depends on the brightness of what you
|
|
50
|
+
are looking at.
|
|
51
|
+
|
|
52
|
+
## The clean twin
|
|
53
|
+
|
|
54
|
+
[c010-physical-values](../../clean/c010-physical-values/) asks the same
|
|
55
|
+
question of a scene already in physical units (float reflectances 0.2 and 0.6,
|
|
56
|
+
no scale, no offset): **0.5**. A system that answers the twin and misses the
|
|
57
|
+
trap can compute NDVI and ignores declared calibration; one that misses both
|
|
58
|
+
cannot compute NDVI, which is a different and lesser finding.
|
|
59
|
+
|
|
60
|
+
## Observed
|
|
61
|
+
|
|
62
|
+
| system | answer | verdict |
|
|
63
|
+
|---|---|---|
|
|
64
|
+
| naive composition | 0.25 | silent error |
|
|
65
|
+
| rasterio 1.5.1 (informed) | 0.3333 | correct |
|
|
66
|
+
| MapSmith | 0.3333 | correct |
|
|
67
|
+
|
|
68
|
+
MapSmith had **no band arithmetic at all** when this trap was written, and the
|
|
69
|
+
first run of it returned `unsupported` — the fourth time this suite has named a
|
|
70
|
+
gap in its own author's catalogue rather than a defect in someone's code. The
|
|
71
|
+
operation that answers it, `band_math`, applies the declared scale and offset
|
|
72
|
+
and records the conversion in the manifest; it also computes in float64,
|
|
73
|
+
because subtracting two `uint16` bands wraps around at zero, and writes float32
|
|
74
|
+
output, because inheriting an integer profile would round an index in [−1, 1]
|
|
75
|
+
to zeros and ones. Three silent failures in one operation, which is roughly the
|
|
76
|
+
density this family has.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Build a two-band scene whose values are stored, not measured.
|
|
2
|
+
|
|
3
|
+
Band 1 is red, band 2 is near-infrared, both uint16 digital numbers with a
|
|
4
|
+
scale and an offset declared in the GeoTIFF's own metadata — the arrangement
|
|
5
|
+
every optical satellite archive uses, and the one Sentinel-2 changed under the
|
|
6
|
+
world's feet in January 2022 when it added a non-zero offset.
|
|
7
|
+
|
|
8
|
+
physical = raw * scale + offset (the GDAL definition, and the one written in
|
|
9
|
+
the file). Red 3000 and NIR 5000 with scale 0.0001 and offset -0.1 are
|
|
10
|
+
reflectances of 0.2 and 0.4.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import rasterio
|
|
20
|
+
from rasterio.transform import from_origin
|
|
21
|
+
|
|
22
|
+
CRS = "EPSG:32633"
|
|
23
|
+
CELL = 10.0
|
|
24
|
+
SCALE, OFFSET = 0.0001, -0.1
|
|
25
|
+
RED_DN, NIR_DN = 3000, 5000
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def main(destination: Path) -> int:
|
|
29
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
shape = (4, 4)
|
|
31
|
+
with rasterio.open(
|
|
32
|
+
destination / "scene.tif",
|
|
33
|
+
"w",
|
|
34
|
+
driver="GTiff",
|
|
35
|
+
height=shape[0],
|
|
36
|
+
width=shape[1],
|
|
37
|
+
count=2,
|
|
38
|
+
dtype="uint16",
|
|
39
|
+
crs=CRS,
|
|
40
|
+
transform=from_origin(500000, 4600000, CELL, CELL),
|
|
41
|
+
) as ds:
|
|
42
|
+
ds.write(np.full(shape, RED_DN, dtype="uint16"), 1)
|
|
43
|
+
ds.write(np.full(shape, NIR_DN, dtype="uint16"), 2)
|
|
44
|
+
ds.scales = (SCALE, SCALE)
|
|
45
|
+
ds.offsets = (OFFSET, OFFSET)
|
|
46
|
+
ds.set_band_description(1, "red")
|
|
47
|
+
ds.set_band_description(2, "nir")
|
|
48
|
+
ds.update_tags(
|
|
49
|
+
note="digital numbers; physical = raw * scale + offset",
|
|
50
|
+
)
|
|
51
|
+
return 0
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
raise SystemExit(main(Path(sys.argv[1] if len(sys.argv) > 1 else ".")))
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
id = "010-scale-offset"
|
|
2
|
+
population = "trap"
|
|
3
|
+
family = "radiometric-scale-offset"
|
|
4
|
+
title = "The band was stored, not measured, and the index skipped the conversion"
|
|
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.3333333333
|
|
17
|
+
tolerance = 0.0001
|
|
18
|
+
derivation = """
|
|
19
|
+
The file declares scale 0.0001 and offset -0.1 on both bands, so the physical
|
|
20
|
+
value of a stored number is raw * scale + offset — the definition GDAL uses and
|
|
21
|
+
the one written in the file. Red 3000 becomes 0.2 and NIR 5000 becomes 0.4.
|
|
22
|
+
NDVI = (0.4 - 0.2) / (0.4 + 0.2) = 0.2 / 0.6 = 1/3 exactly. Every pixel carries
|
|
23
|
+
the same pair, so the mean is that number and no averaging error enters.
|
|
24
|
+
Tolerance 0.0001 covers float32 storage, nothing more."""
|
|
25
|
+
|
|
26
|
+
[naive_failure]
|
|
27
|
+
description = """
|
|
28
|
+
Read the two bands and put them in the formula. GDAL's own documentation states
|
|
29
|
+
that "applying scale and offset is of the responsibility of the user, and is not
|
|
30
|
+
done by methods such as RasterIO() or ReadBlock()", so the arrays that come back
|
|
31
|
+
hold 3000 and 5000, and the index comes out (5000-3000)/(5000+3000) = 0.25.
|
|
32
|
+
No exception, no warning, and nothing in the array says it is not a
|
|
33
|
+
reflectance."""
|
|
34
|
+
observed_value = 0.25
|
|
35
|
+
plausible = true
|
|
36
|
+
why_plausible = """
|
|
37
|
+
0.25 and 0.3333 are both ordinary NDVI values for vegetation — neither is out
|
|
38
|
+
of range, neither looks like an artefact, and the difference between them is a
|
|
39
|
+
quarter, which is the sort of gap that separates a stressed crop from a healthy
|
|
40
|
+
one rather than a bug from a result. The reason this survives everywhere is
|
|
41
|
+
subtler: without an offset, NDVI is invariant to the scale factor, because the
|
|
42
|
+
scale cancels in the ratio. Code that divided digital numbers by 10000 — or
|
|
43
|
+
skipped the conversion entirely — gave the right answer for years. Sentinel-2
|
|
44
|
+
processing baseline 04.00 introduced a non-zero BOA_ADD_OFFSET on 25 January
|
|
45
|
+
2022, and that invariance quietly stopped holding, on the same code, for scenes
|
|
46
|
+
acquired after that date and not before."""
|
|
47
|
+
derivation = """
|
|
48
|
+
Closed form: (5000 - 3000) / (5000 + 3000) = 2000 / 8000 = 0.25 exactly, for
|
|
49
|
+
every pixel, so the mean is 0.25."""
|
|
50
|
+
|
|
51
|
+
[refusal]
|
|
52
|
+
accept_if_mentions = ["scale", "offset", "digital number", "physical", "reflectance", "calibrat", "baseline"]
|
|
53
|
+
|
|
54
|
+
[provenance]
|
|
55
|
+
source = """
|
|
56
|
+
GDAL's raster data model states that "applying scale and offset is of the
|
|
57
|
+
responsibility of the user, and is not done by methods such as RasterIO() or
|
|
58
|
+
ReadBlock()" — the conversion is declared in the file and performed by nobody.
|
|
59
|
+
ESA introduced BOA_ADD_OFFSET (-1000 DN) with Sentinel-2 processing baseline
|
|
60
|
+
04.00 on 25 January 2022, so the same unchanged pipeline returns different
|
|
61
|
+
numbers for scenes on either side of that date; the change is documented by ESA
|
|
62
|
+
and was still generating corrections in downstream archives long afterwards.
|
|
63
|
+
The trap plants the case where the wrong answer is a plausible index rather
|
|
64
|
+
than a physically impossible reflectance."""
|
|
65
|
+
found_by = "mapsmith"
|
|
66
|
+
date = "2026-08-26"
|
|
67
|
+
|
|
68
|
+
[provenance.engine_versions]
|
|
69
|
+
rasterio = "1.5.1 - read() returns stored values; scales and offsets are exposed as dataset attributes and applied by nobody"
|