Skip to content

Increment 26a: MapBiomas catalogue entries and per-year land-cover class windows - #232

Merged
skavhaug merged 9 commits into
masterfrom
worktree-mapbiomas-26a
Oct 12, 2026
Merged

skavhaug merged 9 commits into
masterfrom
worktree-mapbiomas-26a

Conversation

@skavhaug

Copy link
Copy Markdown
Member

Increment 26a, the first part of docs/increments/26-land-cover.md (section 13 is the as-built record). 163 net production lines (python3 tools/count_loc.py de9b6c03 HEAD).

  • RemoteSource gains years, values (heights or classes), url_for(year=None), credit_for(url=, date=) and check_values; FetchRequest.year, CachedObject.fetched.
  • ClassWindow (origin, pixel size, integer class array), io.cog.decode_class_window, CacheRepository.load_class_window and class_strips share one _decode_into path with the height decoder.
  • MapBiomas catalogue entries fetch per year and decode class codes without touching the height path.

Evidence: @Perf quick check NO CHANGE (docs/benchmarks/2026-10-11/mapbiomas-26a/). No mutation round, as section 9 of the design rules for 26a. Code review rounds 1 and 2 recorded in the increment file, round 2 APPROVED. 26b (exact class fractions) onward is designed, ruled and not built.

🤖 Generated with Claude Code

skavhaug and others added 8 commits October 11, 2026 22:23
…lass strips (@tester)

Failing tests for increment 26a, "MapBiomas in the catalogue, and class
windows" (docs/increments/26-land-cover.md, section 9 row 26a and section 10
"26a (Python)"), written before any production code. 48 tests, all red:

  .venv/bin/python -m pytest tests/python/test_sources_mapbiomas.py \
      tests/python/test_io_class_window.py tests/python/test_fetch_per_year.py \
      tests/python/test_cli_mapbiomas.py -o addopts="" --no-cov
  -> 35 failed, 13 errors

What each file pins and what it fails on today:

- tests/python/class_fixtures.py: a 40 x 48 uint8 class raster in EPSG:4326,
  PixelIsArea, 16 x 16 tiles, cell step 1/128 degree (so every cell corner is
  exact in binary and a box edge "on a cell edge" is on it), one grid per year,
  a 10 x 10 no-data (0) corner, empty tiles by cog_fixtures.with_zero_byte_count;
  the oracle `expected` zeroes the empty tiles' rectangles by hand from
  tifffile's page attributes. A per-year catalogue entry for the local range
  server. The pins are listed in its docstring.
- test_sources_mapbiomas.py (16): SOURCES["mapbiomas-c11"] (KeyError today):
  kind one-cog-per-year, values "classes", years 1985..2025, EPSG:4326, nodata 0,
  the design's URL for 2025 and 1985 through url_for, the default year the last,
  a year outside the range refused naming 1985 and 2025, the credit with
  "Collection 11", {date} and {url}, CC BY-SA 4.0, the Souza et al. citation;
  credit_for renders the date as ISO; anadem-v1 and glo30 hold heights with no
  years; check_values refuses a class source as --dem and a heights source as
  --land-cover, naming source, values and option.
- test_io_class_window.py (20): ClassWindow (AttributeError today): read-only
  uint8, corner(row, col), anything but 2-D uint8 refused; decode_class_window
  over five windows equals the written grid, empty tiles read as zeros, the
  PixelIsArea corner is the cell's corner (the DEM reader's meta is half a cell
  in), LZW with the horizontal predictor through imagecodecs (needs_codecs),
  one thread and eight the same bytes, a missing block NotCached before any
  read; CacheRepository.load_class_window and class_strips: strips of at most
  `rows` rows top to bottom covering the cells whose interior meets the box
  (open cells), a box on cell edges, a box past the raster clipped, a box
  meeting no cell a plain ValueError, a missing block NotCached.
- test_fetch_per_year.py (9): RemoteSource refuses the per-year kind today.
  Default year names brazil_coverage-col11_2025; each year its own object in
  one manifest; a year outside the range or a year given for a one-cog source
  is a FetchError before any request; empty tiles never requested and written
  as empty files (report.empty 2); the cached class window reads them as zeros;
  CachedObject.fetched is the day the header was first fetched and is kept on
  a later run; NOTICE.txt carries each object's credit with its date and URL.
  Note: FetchRequest(year=...) is silently ignored by Pydantic today, so the
  one-cog refusal test is red as "did not raise" until `year` exists.
- test_cli_mapbiomas.py (5): `fetch --year` ("No such option" today), the
  object named in the output, a year outside the range a usage error naming
  it and the range with no request made, --year on a source without years a
  usage error, and `mesh --dem mapbiomas-c11` refused naming the key, "classes"
  and --dem with the network closed (today the unknown key is read as a path).
- test_cli_fetch.py: catalogue_with split into install(sources, monkeypatch,
  entry), so the new CLI suite installs a per-year entry the same way; the
  existing 15 tests pass unchanged.

No throwaway implementation: section 10 says "no throwaway implementations",
so the red reasons were checked per test (--tb=line) instead. No mutation
targets: section 9 names only the ledger and the reduction suites.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d class strips (@developer)

The green step of increment 26a (docs/increments/26-land-cover.md, section 9
row 26a; section 3.1 "The catalogue entry"; Ola's rulings of 2026-10-04:
Collection 11, year 2025 by default, CC BY-SA 4.0). The 48 tests of
cb5f254 pass; no test file is touched. Python only, no new dependency; the
C++ core sees no file and no CRS.

sources.py: `kind` gains "one-cog-per-year" (`url_template` with `{year}`,
validated), `years` (ascending, the default is the last) and `values`
("heights" | "classes"). `url_for(year=None)` picks the year's file and
refuses a year outside the range naming the first and last, or any year for a
source without years. `credit_for(url=, date=)` fills `{date}` (ISO) and
`{url}` into the credit; a credit without them is unchanged. `check_values`
is the one refusal by `values`, named by the option. The entry
`mapbiomas-c11` carries the design's URL template, 1985..2025, EPSG:4326,
nodata 0, MapBiomas's credit format, the CC BY-SA 4.0 note and the Souza et
al. (2020) citation. `notice(source, credits=())` renders the given credits
instead of the entry's, so `NOTICE.txt` carries each object's fetch date and
URL; with none given it is unchanged (test_fetch_run pins the old text). The
date type is a Protocol (`Dated`: `isoformat()`), because
test_sources_imports_pydantic_only pins this module to Pydantic alone.

fetch/plan.py: `FetchRequest.year: int | None = None`.

fetch/run.py: every non-tiled source takes its file from `url_for(year)`, the
object id from the file name as before (`brazil_coverage-col11_<year>`); a
`ValueError` there is a `FetchError` before any request, as is `--year` on a
tiled source. A new object records `fetched=date.today()`; known objects keep
theirs. The notice is built from every object's rendered credit, deduplicated.

io/models.py: `ClassWindow(x_min, y_max, delta_x, delta_y, array)`, frozen,
a read-only uint8 copy (anything else refused naming uint8), `rows`, `cols`,
`corner(row, col)`.

io/cog.py: `decode_class_window(source, meta, window, *, threads=None)`: the
same window checks and NotCached-before-any-read as `decode_window`, the
page's dtype must be uint8, empty blocks (byte count 0) stay 0 in a zeroed
canvas, the corner is the node moved out half a cell (PixelIsArea). The block
decoding loop is now `_decode_into`, shared with `decode_window`, whose
behaviour is unchanged.

io/repository.py: `CachedObject.fetched: date | None = None`;
`CacheRepository.load_class_window(name, window)` and `class_strips(name,
box, *, rows)`: the cells whose open interior meets the box (floor of the
lower index, ceil of the upper, so a box edge on a cell edge takes neither
neighbour), clipped to the raster, a box meeting none a plain ValueError,
top to bottom in strips of at most `rows` rows.

cli.py: `fetch --year`, refused as a usage error before any request when the
source has no years or lacks the year; `mesh --dem <class source>` refused by
`check_values` in `_dem_sources`, before the cache is read.

Checks run: the four 26a suites (48 passed), test_fetch_run, test_cli_fetch,
test_cli_mesh_cache, test_refine_golden (.vtk unchanged) and test_layering
(no new rows); mypy strict, ruff check and format; check_prohibited_deps,
check_detria_boundary, check_citations. Whole Python suite: 7191 passed, 17
skipped, 1 failed on the datetime import, fixed as above and re-run.

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

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…thin band; ACCEPTED (@Perf)

tools/bench_quick.py run at f51f0a1 (worktree-mapbiomas-26a, base master
de9b6c0) against docs/benchmarks/quick/baseline-ac.json (357808b, AC,
numpy 2.5.4, saved by #229). Verdict NO CHANGE, exit 0: no MESH CHANGED,
BROKEN, SLOWER or FASTER line on any of the seven cases, so every mesh
SHA-256 equals the baseline's, every total is within its band, and decode
(judged on every case, 42 % of geilo-al-ramp) is within its band. 26a
touches no refine or mesh code; the one speed-relevant change is the
block-decoding loop moved into _decode_into in io/cog.py (8eec354).

Hotspot lines: the same eight as the baseline run, six at the same share,
tile refine 46 -> 44 % and quarter refine 48 -> 47 %. Lagan features clip
(ruled 40.7 %) did not print.

AC power, battery 100 %, caffeinate on, nothing else running. Evidence:
docs/benchmarks/2026-10-11/mapbiomas-26a/ (README.md, run.log,
power-state.txt). The run's per-case medians are not recorded: the tool
saves a record only as the baseline, and a second run with --save-baseline
(to copy the record here and restore the baseline) was refused by the
session's permission classifier. baseline-ac.json is untouched.

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ady for @tester' phrases fixed, by the main session

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@skavhaug
skavhaug enabled auto-merge October 12, 2026 04:44
@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 a conflict with the base branch Oct 12, 2026
…26a (@developer)

PR #232 (increment 26a, land-cover class raster in the cache) went DIRTY in
the merge queue after master moved. origin/master at a1aec50 brings two
merged PRs: #231 (increment 36, GeoPackage output, merge commit fa6e485)
and #230 (release hygiene, merge commit a1aec50). Merge commit, no rebase:
the branch is pushed and the PR is open.

One conflict, src_python/tin_engine/io/repository.py, in its module
docstring only. Master wrote the paragraph "Increment 36 adds
:func:`create_sqlite` ..." directly under the "Audit PR C" line; this branch
wrote the paragraph "Increment 26a adds the class raster's reads ..." after
a blank line. Resolved by keeping both, 36's first (it merged first), then
26a's, each as its side wrote it. The rest of the file auto-merged, and
both sides' code is present: CachedObject.fetched, CacheRepository
.load_class_window and .class_strips (26a), create_sqlite (36).

ROADMAP.md, cli.py and io/models.py auto-merged. ROADMAP rows 26 and 36 are
byte-identical to their own branch's text. In cli.py, 26a's --year option
and check_values on --dem and 36's .gpkg output path are disjoint
additions; in io/models.py, 26a's ClassWindow and 36's SrsEntry likewise.

Gates, in this worktree's venv, after the merge:
  ruff check .                           all checks passed
  ruff format --check .                  250 files already formatted
  mypy                                   no issues in 68 source files
  python3 tools/check_prohibited_deps.py OK, 399 files clean
  python3 tools/check_citations.py       exit 0; 60 at-risk lines listed,
                                         all of the form "this branch edits
                                         <file>" from the merged edits
  pytest tests/python/                   7236 passed, 17 skipped (environment
                                         skips only), coverage 99% (floor 85)
No test broke. No C++ change on either side; _core not rebuilt.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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