Skip to content

Increment 26b: exact land-cover class fractions per triangle (--land-cover, --land-cover-year) - #235

Merged
skavhaug merged 18 commits into
masterfrom
worktree-mapbiomas-26b
Oct 12, 2026
Merged

skavhaug merged 18 commits into
masterfrom
worktree-mapbiomas-26b

Conversation

@skavhaug

Copy link
Copy Markdown
Member

Increment 26b, the second PR of docs/increments/26-land-cover.md (section 14 is the as-built record). 621 net production lines by python3 tools/count_loc.py 5f28292c HEAD against the estimate of 420; under 700. The branch also carries the ruling-record commits of #233, merged in to keep the Status line and ROADMAP row consistent.

  • include/terrain/land_cover/overlap.hpp: the exact triangle-against-cell overlap on an index-space raster (strips, full-cell spans, canonical crossings so shared edges give complementary areas, chunks in parallel); the core sees arrays only.
  • bindings/core.cpp, _core.pyi: class_areas and the uint8 view.
  • land_cover_fractions.py: index-space corners, row areas, chunks through class_areas, the ClassFractions table and dominant class.
  • io/vtk_legacy.py, io/ply.py: the fractions arrays; cli.py: --land-cover, --land-cover-year, the run-record entries and seven refusals.

Evidence: red 1820c1d4 (92 cases), green 5b8bb3e0, test-side fixes f165282b (103 cases green); @perf ACCEPTED 98eea44f (docs/benchmarks/2026-10-12/26b-fractions/): on the Velhas basin piece all 19 classes inside the raster's own count band, 0.77 s per million triangles at 8 threads, every quick-check mesh unchanged; idle re-run clean (Lagan features read +14 % is master's drift since the baseline, seen on master itself). Code review rounds 1 and 2 recorded, doc fixes only. No mutation round (section 9 names none for 26b).

🤖 Generated with Claude Code

skavhaug and others added 18 commits October 12, 2026 06:53
…OADMAP row 35 merged, by the main session

Ola, 2026-10-12, asked whether to rule Numedalslagen's features clip (6.0 s of
8.8 s, 68 %, docs/benchmarks/2026-10-11/cleanup-speed/README.md) expected and
leave the fix to the cached-extract idea: "Rule it expected at 6 s, cached
extract is the fix". The hotspots.toml entry carries the words verbatim, so
the quick check stays quiet until the share grows 10 points. Lagan's 43.6 %
is within 10 points of its 40.7 % ruling and gets no new entry.

ROADMAP row 35: built -> merged (#229); its stale "next the push" phrase
replaced by the ruling. Increment 35's section 11 gets the ruling in one
sentence after the default-taken line.

Checked: tests/python/test_bench_quick.py -k shipped, 2 passed (hotspots.toml
loads, every entry has a ruling).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ain session

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…main session

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ings and 36's Status fixed, by the main session

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… to the reviewer's wording, by the main session

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@tester)

docs/increments/26-land-cover.md, section 10 "26b", on 26a's merged head
5f28292. Four suites, 92 cases, all red for the right reason; no production
code, no throwaway implementation, no mutation round (section 9 names none
for 26b).

C++ (Catch2), tests/cpp/unit/test_land_cover_overlap.cpp, 17 cases, registered
in tests/cpp/CMakeLists.txt with the plain helper (overlap.hpp names no
kernel) plus Threads: the hand cases of section 10 (a triangle in one cell; a
right triangle with legs 2 on a 2 x 2 grid giving 1, 0.5, 0.5 and the fourth
cell absent; vertices on grid corners; an edge on a grid line; a sliver along
a row; a nanometre triangle), a cell area per row, vertex order and mirror
orientation, two triangles on a shared edge summing per cell, 2000 random
triangles summing to the shoelace area, four threads and all cores giving one
thread's bits, chunks concatenated equal to one call bit for bit, the closed
window [0, cols] x [0, rows] with OutsideWindow past it, zero-area,
non-finite and wrong-length refusals.

Python, tests/python/:
- fraction_fixtures.py: the pins (its docstring) and the oracles, which
  share nothing with the producer: GEOS's clip of each triangle against each
  cell box, GEOS's Delaunay of a seeded point set with points snapped to grid
  corners and lines, pyproj.Geod's cell area per row, pyproj's Transformer
  from the mesh's CRS; the CLI scene (a UTM 23S DEM over the class raster's
  class-0 quarter, the class cache with a fetch date).
- test_land_cover_overlap.py (36): F2 against GEOS on random triangulations
  of a 20 x 20 class grid, with unit and with ellipsoidal row areas; class
  totals against the square's exact cover; per-cell sums on a 15 x 15 grid
  of unique classes (uint8 holds 255, so not 20 x 20) to 1e-12 of a cell;
  threads 1, 4 and 0 equal; refusals; row_areas against Geod and the
  design's 887.5 / 836.0 m2 figures; the ClassFractions table; and the
  window-corner pin for PixelIsArea and PixelIsPoint (green on 26a's code:
  read_page puts the node at the cell's centre for both registrations, so
  decode_class_window's half-cell move is right either way and no refusal is
  needed; 26a's code review, round 1).
- test_io_land_cover_arrays.py (17): the .vtk FieldData arrays
  land_cover_offsets (unsigned_int), land_cover_classes (unsigned_char),
  land_cover_fractions (float) read back through vtkread in both encodings,
  the names reserved, a wrong triangle count refused; the .ply face lists
  (list uchar uchar, list uchar float) and the property order read back
  through plyread, refused beside edges.
- test_cli_mesh_land_cover_fractions.py (22): --land-cover and
  --land-cover-year; F1 on the file; land_cover_code the largest class and 0
  on every line cell; the fractions against the oracle to 1e-6; class 0
  among the classes; the strings (codes, source, credit with the fetch date
  and URL, licence note, cite) in the file, --stats and --record; the year
  honoured and 2025 the default; refusals (year out of range, year without
  the option, a heights source, a year not in the cache naming fetch, a coded
  features map beside it, no --dem); the control without the option; the
  .ply lists against the .vtk table; the VTK reader read-back (skipped
  without vtk, as 16c's).

Red check, run in this worktree's own build and venv:
- cmake -S . -B build && cmake --build build --target test_land_cover_overlap:
  "fatal error: 'terrain/land_cover/overlap.hpp' file not found"; the only
  error in cmake --build build -j 6 -- -k.
- ctest --test-dir build -j 6 -E test_land_cover_overlap: 100% of 1014
  existing tests passed.
- .venv/bin/python -m pytest tests/python/test_land_cover_overlap.py
  tests/python/test_io_land_cover_arrays.py
  tests/python/test_cli_mesh_land_cover_fractions.py -o addopts="" --no-cov:
  68 red (AttributeError: _core has no class_areas; ImportError:
  ClassFractions; "No such option: --land-cover"), 6 green (the
  window-corner pin, the cover oracle's self-check, the three controls
  without the option), 1 skipped (vtk absent).
- .venv/bin/python -m pytest tests/python (with coverage): 7120 passed, 139
  skipped, coverage 99%; red: the 68 of these suites and one pre-existing,
  environment-bound failure also red on the untouched head 5f28292
  (test_io_geotiff_parametric.py::test_the_real_austrian_file_reads_as_austria_lambert,
  `AUSTRIA_FILE is None`: the real file is not on this machine).
- ruff check and ruff format --check on the four files: clean.

Pinned beyond the design (also in fraction_fixtures.py's docstring): a
trailing `threads` parameter on class_areas (1.2's signature has none, yet
the result must not depend on the thread count); index space with y the row
growing downward and the window closed; the view's geometry
RasterGeometry{0, 0, 1, 1, cols, rows} with only rows(), cols() and row()
read; either orientation accepted; OutsideWindow and the zero-area refusal
as std::invalid_argument, ValueError through the binding; the table as
io.models.ClassFractions (the design names the arrays, not a container);
land_cover_fractions.row_areas(window); the writers' keywords
class_fractions and face_fractions and the .ply property order; the CLI's
refusal wording asserted by option name; land_cover_source asserted by the
year only (RemoteSource has no display name for "MapBiomas Collection 11").

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Provisional message; amended with the verification record after the full run.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…out, pins added (@tester)

Follows @developer's green 5b8bb3e on docs/increments/26-land-cover.md,
section 10 "26b". Tests only (tests/); no production file touched.

The four cases red because of the tests, fixed:
- tests/cpp/unit/test_land_cover_overlap.cpp case 6: the nanometre triangle's
  expected area was the intended 0.5e-18, but 0.5 + 1e-9 rounds to legs of
  1e-9 - 2.8e-17, so its true area is 4.9999997e-19 (relative 5.7e-8 off,
  past the 1e-9 tolerance). Now compared against the shoelace area of the
  corners as rounded, at relative 1e-9, plus a floor of 0.4e-18.
- tests/python/plyread.py: list properties of varying length (the 26b face
  lists) are stacked into an object array, one record each; fixed-length
  lists (vertex_indices) stay 2-D as every older ply test expects.
- TestTheYear: tmp_path / "a" is created before the run writes into it.
- tests/python/test_layering.py: land_cover_fractions at L3 importing
  `_core crs io.models io.repository`; cli gains land_cover_fractions;
  io.ply and io.vtk_legacy gain io.models. Each row checked against
  importscan on 5b8bb3e before writing.

Red-step scaffolding removed (tester.md section 1): the importlib fixtures
(`core`, `fractions`, `sources`), the call-time imports of ClassFractions,
decode_class_window and read_page, the "HOW THIS FILE GOES RED" paragraphs
and the C++ "RED at the commit" note. The `fractions` fixture was also a
class-scoped fixture as an instance method, a PytestRemovedIn10Warning.

Pins added, with the reasons in the tests and fraction_fixtures' docstring:
- land_cover_codes == "<source id> class number; 0 = no data, and every
  constraint line" and land_cover_source == "<source id>, <year>" (1.6
  spells both with a display name RemoteSource does not have; the id is
  what --land-cover and fetch take).
- assign(..., chunk=5) equals the default bit for bit and equals the file's
  arrays: the stitching of several chunks, each with its own window and row
  areas, which the CLI scene (far under 2^18 triangles) never runs. The
  chunk size itself is not pinned.
- Refusals the green code has and the red suite did not reach: an unknown
  --land-cover key (naming the catalogue), no --cache and no RASPUTIN_DATA,
  a cache root without the source (naming fetch), a mesh past the raster
  and an object not cached through assign, a row without entries in
  fractions_of, a 256-class face in write_ply; dominant's tie rule and the
  empty mesh.
Not pinned: a cache without a fetch date credited with today's date
(fetch/run.py:169 does the same for NOTICE.txt; raised as a question).

Verification, this worktree's build and venv, after `cmake --build
build-pyext --target _core`, the copy into the venv and `touch`:
- cmake --build build -j 8: exit 0, no warnings; ctest --test-dir build:
  100% of 1031 tests passed (1014 before 26b + 17).
- pytest tests/python (coverage gate on): 7192 passed, 139 skipped, 1 failed
  (test_io_geotiff_parametric.py::test_the_real_austrian_file_reads_as_austria_lambert:
  GeoTiffError "Predictor 3 cannot be decoded as installed; imagecodecs";
  io/geotiff.py is unchanged against the base 5f28292), total coverage
  98.48%, floor 85. That run preceded the pins added here; the three 26b
  suites were rerun after them: 84 passed, 1 skipped (vtk absent).
- Coverage of the green commit's Python files in the full run: cli.py 97%,
  io/models.py 99%, io/ply.py 99%, io/repository.py 97%, io/vtk_legacy.py
  99%, land_cover_fractions.py 94%, run_record.py 99%; _core.pyi and the
  C++ are not measured. 26b lines missed in that run: land_cover_fractions
  79, 100, 115, 133, 146; cli 1401-1402, 1424-1425; ply 218. After the pins
  only land_cover_fractions 115 (more than 2^32 - 1 entries: unreachable)
  and cli 1424-1425 (the except around assign; the mesh past the raster
  needs a scene the fixtures do not have) remain.
- mypy: no issues in 69 files; ruff check and ruff format --check: clean.

Case count: 92 at red (17 C++ + 75 Python), 103 now (17 + 86, of which one
skips without vtk); all green.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…rded, by the main session

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e the MapBiomas band), pass 0.77 s per million triangles at 8 threads, quick check meshes unchanged, timings judged back to back under a foreign load (@Perf)

Evidence in docs/benchmarks/2026-10-12/26b-fractions/ (README, run_meshes.sh,
analyse.py, raw/). AC power throughout; Apple M1 Max; tree 4149b50, Release
_core 55bb3993 built into build-bench and installed into the worktree venv.

Gate 1, the Velhas piece (ANADEM 30 m UTM grid, outline 11,667.6 km2) at 10 m
(649,504 triangles) and 5 m (1,571,955) with --land-cover mapbiomas-c11
--land-cover-year 2025 (the design's default year, Ola's Q1 ruling; the
cache fetched 5.1 MB in 10 s):
- Class areas: all 19 classes inside the reference band [cells wholly
  inside, cells wholly inside plus cells the outline crosses] (28.7 km2 of
  boundary cells), 0 outside, identical for both meshes; the worst difference
  from the centre count is class 15, +4.49 ha (+0.0011 %). ACCEPTED.
- Fractions pass: CLI phase 0.493 s (10 m) and 0.984 s (5 m) on 10 cores;
  assign at 8 threads 0.500 and 0.999 s, 0.77 and 0.64 s per million
  triangles (gate 2 s). _core.class_areas alone scales 7.4x from 1 to 8
  threads; the pass 2.6x and 2.2x, the rest being serial Python (window
  reads of nearly the whole raster per chunk in file order, row areas,
  fractions, dominant class). ACCEPTED.
- Per-cell sums on a 1 km square against GEOS and the cell's area: 2.7e-16
  worst relative, gate 1e-6. ACCEPTED.
- Peak RSS added by the pass: +146 MB at 10 m (225 B per triangle), +42 MB at
  5 m (27 B); the design estimated 25 B. Recorded, not a gate.
- File arrays equal assign's output bit for bit; mesh geometry equal with
  and without --land-cover; misplaced share reported as a binning bound only.

Gate 2, tools/bench_quick.py against the stored AC baseline 357808b: no
MESH CHANGED line in three runs (every mesh SHA-256 equal), but SLOWER on
every case (+7 to +33 %), for the untouched base commit 5f28292 as much as
for the branch, under a measured foreign load (Microsoft Teams call, load
average 7). Back to back, branch/base is within 1 % on six cases and at most
1.11 on quarter (0.41 vs under 0.37 s). No change attributable to 26b; the
quick check should be re-run on an idle machine before the merge. HOTSPOT
lines are the same set the 2026-10-11 runs printed, Numedalslagen features
clip 67 % among them (ruled on another branch); Lagan features clip 43 %.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…sion

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…es applied, by the main session

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…26b built and review-approved, by the main session

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…is master's drift since the baseline, not 26b's, by the main session

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@skavhaug
skavhaug enabled auto-merge October 12, 2026 10:36
@skavhaug
skavhaug added this pull request to the merge queue Oct 12, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to invalid changes in the merge commit Oct 12, 2026
@skavhaug
skavhaug added this pull request to the merge queue Oct 12, 2026
Merged via the queue into master with commit c317c7e Oct 12, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant