Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
b0d3917
Research: the São Francisco basin as rasputin's target (DEMs, inputs,…
skavhaug Sep 27, 2026
59024e3
São Francisco note: Ola's 50 m was 30 m
skavhaug Sep 27, 2026
42b4b06
Increment 15: design for a multi-tile, geographic DEM and a computati…
skavhaug Sep 27, 2026
c299425
15: BHO CRS checked, ANADEM data licence still open; ROADMAP: increme…
skavhaug Sep 27, 2026
351a57e
15: Ola's rulings on Q1-Q5
skavhaug Sep 27, 2026
2696bc2
red: increment 15a (multi-tile DEM, one projected CRS)
skavhaug Sep 27, 2026
f74dad1
15: DTM10 fixture licence checked (CC BY 4.0, Geonorge)
skavhaug Sep 27, 2026
ff7cc8d
green: increment 15a (multi-tile DEM, one projected CRS)
skavhaug Sep 27, 2026
7a19dd4
15a: re-point three citations the green commit shifted
skavhaug Sep 27, 2026
2101d06
15a: R6's refusal order as pinned; ROADMAP 15a row
skavhaug Sep 27, 2026
44412b5
Merge remote-tracking branch 'origin/master' into increment15-dem-mosaic
skavhaug Sep 27, 2026
a5aabdc
15a review fixes: CI runs the two codecs suites; status, tile sizes, …
skavhaug Sep 27, 2026
a88dda7
15a: Ola's reading of Q5 for overlapping lattices
skavhaug Sep 27, 2026
1e45970
15a tests: lattice by coverage (Ola's Q5 reading), the disagreeing ti…
skavhaug Sep 27, 2026
540764a
15a: the Acceptance line matches the index-window selection and Ola's…
skavhaug Sep 27, 2026
77c5c4e
15a: lattice chosen by coverage (Ola's Q5 reading), the disagreeing t…
skavhaug Sep 27, 2026
5e3514f
15a docs: re-point the geotiff.py citations shifted by read_header
skavhaug Sep 27, 2026
d34d79d
15a: the ROADMAP row no longer claims the acceptance box meshes; meas…
skavhaug Sep 27, 2026
d0517f6
15a: Ola's Q1 revised: disagreeing overlaps split down the middle and…
skavhaug Sep 28, 2026
0c97ac9
15a tests: overlaps split down the middle and reported (Ola's Q1 revi…
skavhaug Sep 28, 2026
cba0073
15a: overlaps split down the middle and reported (Ola's Q1 revised)
skavhaug Sep 28, 2026
7b02810
15a: LOC after the midline rule
skavhaug Sep 28, 2026
9ea10ce
15a review fixes: the Q1-revised evidence re-measured with a committe…
skavhaug Sep 28, 2026
3bb44a8
15a: Ola's 1 mm threshold for the seam report
skavhaug Sep 28, 2026
0081591
15a tests: the seam report ignores differences below 1 mm; table cell…
skavhaug Sep 28, 2026
776757e
15a: the seam report ignores differences below 1 mm; table cells esca…
skavhaug Sep 28, 2026
a253a77
15a review fixes: why the export date is the side files' (measured), …
skavhaug Sep 28, 2026
c626ef5
CLAUDE.md §2: blank lines do not count (Ola); 20b's contrary note mar…
skavhaug Sep 28, 2026
201a4e7
CLAUDE.md §2: only Ola's ruling (blank lines excluded); 15a's LOC at …
skavhaug Sep 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/main.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,8 @@ jobs:
- name: Tests with the codecs extra
run: |
python -m pip install -e ".[dev,codecs]"
pytest --no-cov tests/python/test_io_geotiff.py tests/python/test_geotiff_fixtures.py
pytest --no-cov tests/python/test_io_geotiff.py tests/python/test_geotiff_fixtures.py \
tests/python/test_io_read_meta.py tests/python/test_cli_mesh_mosaic.py

# Increment 13, ruling 10: read the .vtk output back with the readers
# ParaView uses. The suite skips when vtk is absent, so it is run only
Expand Down
8 changes: 5 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ This project is governed by specialized sub-agents. Always defer tasks to the co

## 2. Core Constraints & Technical Mandates
* **Strict Size Limit:** Under **700 lines of production code per pull request**,
where a line counts unless it is a comment, a docstring, or the body of a raw
literal; tests excluded. The exclusions exist so the ceiling does not penalise
the comment density this project asks for. Lines count as written: packing
where a line counts unless it is blank, a comment, a docstring, or the body of
a raw literal; tests excluded. The exclusions exist so the ceiling does not
penalise the comment density this project asks for, and blank lines add no
reading (Ola, 2026-09-28; before that, 20b counted blank lines). Lines count
as written: packing
code by hand under `# fmt: skip` / `# fmt: off` is allowed, provided the
packed lines stay readable and the review says why each new region is
packed (Ola, 2026-09-27, on `tools/bench.py`). This is the only statement of
Expand Down
12 changes: 10 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ increment that most needs a picture to check against
| 20 | Quality start: before DEM refinement, Steiner points at the DEM node nearest each bad triangle's circumcentre until the start mesh has a 25° minimum angle (`--start-min-angle`, 0 is off); geometry only, input segments not split, serial and deterministic. Removes increment 16's boundary fans | landed with 20b as interim (C1-C3 open, to 20c; C4 -> 20b) | `docs/increments/20-start-quality.md` |
| 20b | Minimum insertion distance from constraints: when refinement's worst node lies within ε = clamp(tol / slope, cell/100, cell/2) of a constraint segment, insert the foot on the segment (off-node, bilinear z) instead; a footed node still above tolerance is inserted after all, so the tolerance guarantee is unchanged. Removes the 0.0117° needle at 1 m (increment 20's C4) | shipped with 20 (#97) | `docs/increments/20b-min-insertion-distance.md` |
| — | `tools/bench.py`: the 1 m benchmark and the thread-scaling sweep from one checked-in command, with power state, quality and commit recorded per run; the one-off scripts in `docs/benchmarks/2026-09-26/` are its specification. Rule 2's acceptance run needs it | shipped with branch `tools-bench`'s PR | `docs/benchmarks/bench-py.md` |
| — | The serial phase: profile refine's serial insert-and-flip phase, then parallelise what the profile blames. Scaling tops out at about 2.0-2.2×. Profiled 2026-09-27: serial part about a third of 1-thread refine, mostly Lawson legalisation; the scan stops near 5× from load imbalance | profiled; designed as increment 21 (Ola's rulings 2026-09-27: L1 determinism, one path, at most 2 % more triangles). **21a shipped with branch `increment21a-quick-wins`'s PR**: dynamic scan blocks, active merge, reused flip stack; mesh bit-identical; on AC refine -10 to -15 % at 8 threads, ceiling 2.1x -> 2.3x (`docs/benchmarks/2026-09-27/21a-acceptance.md`). **21b shipped with branch `increment21b-lattice-incircle`'s PR**: an int64 lattice incircle answers 99.97-100 % of refine's incircle tests; mesh bit-identical; on battery refine -15 % at 8 threads, ceiling 2.3x -> 2.5x (`docs/benchmarks/2026-09-27/21b-acceptance.md`). **21c measured** (`docs/benchmarks/2026-09-27/21c/README.md`): option C costs 6.5-10 % more triangles; A1 about 1 %; A0 is bit-identical (on these two inputs, not proven), and with evaluate-once is modelled at only 7-11 % faster refine at 8 threads and slower at 4. **21d is deferred** (Ola, 2026-09-27: "Review and push 21c, then basin-work"): the multi-tile DEM and the computation CRS come first, then domain decomposition, which parallelises scan and split together | `docs/increments/21-parallel-refine.md`, `docs/benchmarks/2026-09-27/serial-profile/README.md` |
| — | The serial phase: profile refine's serial insert-and-flip phase, then parallelise what the profile blames. Scaling tops out at about 2.0-2.2×. Profiled 2026-09-27: serial part about a third of 1-thread refine, mostly Lawson legalisation; the scan stops near 5× from load imbalance | profiled; designed as increment 21 (Ola's rulings 2026-09-27: L1 determinism, one path, at most 2 % more triangles). **21a shipped with branch `increment21a-quick-wins`'s PR**: dynamic scan blocks, active merge, reused flip stack; mesh bit-identical; on AC refine -10 to -15 % at 8 threads, ceiling 2.1x -> 2.3x (`docs/benchmarks/2026-09-27/21a-acceptance.md`). **21b shipped with branch `increment21b-lattice-incircle`'s PR**: an int64 lattice incircle answers 99.97-100 % of refine's incircle tests; mesh bit-identical; on battery refine -15 % at 8 threads, ceiling 2.3x -> 2.5x (`docs/benchmarks/2026-09-27/21b-acceptance.md`). **21c measured** (`docs/benchmarks/2026-09-27/21c/README.md`): option C costs 6.5-10 % more triangles; A1 about 1 %; A0 is bit-identical (on these two inputs, not proven), and with evaluate-once is modelled at only 7-11 % faster refine at 8 threads and slower at 4. **21d is deferred** (Ola, 2026-09-27: "Review and push 21c, then basin-work"), behind the work in "Order of work" below; domain decomposition parallelises scan and split together | `docs/increments/21-parallel-refine.md`, `docs/benchmarks/2026-09-27/serial-profile/README.md` |
| — | Release hardening: measure libc++'s `_LIBCPP_HARDENING_MODE_FAST` (and libstdc++'s assertions on the GCC leg), which bounds-check `std::vector`, `span` and the like in Release, on the 1 m benchmark; switch it on if the cost is small. Today an out-of-range index in shipped code the tests miss is undefined behaviour and crashes Python (Ola, 2026-09-27: "I'm surprised we don't have proper memory control"; CI's ASan/UBSan/TSan cover what the tests reach) | to measure (Ola, 2026-09-27); 21d, which it was placed after, is deferred behind the basin work | none yet |
| 15 | A DEM in several tiles, and the domain in its own CRS: 15a Norway, many tiles in one projected CRS (Ola's 254 DTM10 UTM33 tiles; only the selected tiles must share a lattice, since 8 of them sit half a cell off); 15b the domain polygon reprojected into the DEM's CRS; 15c and 15d the São Francisco basin (geographic DEMs meshed in the DEM's own lattice frame, not resampled; window decoding and memory) | **15a shipped with branch `increment15-dem-mosaic`'s PR**: `--dem DIR` or several files, `--bbox`, tiles selected and stitched on one lattice, Q2-Q5 refusals; overlaps that disagree (real DTM10 tiles exported on different dates, up to 52 m) are split down the middle and reported per seam (Ola's Q1 revised, 2026-09-28). The design's acceptance box, 9 tiles and 10,051² nodes, meshes: 11.05 M triangles in 31 s on battery. 15b next (Ola, 2026-09-27: Norway first) | `docs/increments/15-dem-mosaic.md` |
| 16b | Interior polygons and polylines as constraints ("terrain polygons": lakes, land cover, roads, rivers): `--features PATH`, a GeoJSON `FeatureCollection`, each feature naming a vocabulary property; closed or open `Breakline`s, crossings noded, off-node vertices with bilinear z. **Its working example is real data** (Ola, 2026-09-27): CORINE Land Cover 2018 over the benchmark tile `7908_3_10m_z33.tif`. The source is Ola's local copy, `rasputin_data/corine_sql/.../U2018_CLC2018_V2020_20u1.gpkg` (8.2 GB, EPSG:3035, a sibling of this repository, which also holds the 254-tile DTM10 archive for gap 6). It reads without GDAL: sqlite3 over its R-tree, the GeoPackage blob header stripped, `shapely.wkb`, then `pyproj` to 25833, so CRS stops in Python as before. Probed 2026-09-27 in 0.2 s: 60 polygons in 8 classes (heath, bare rock, sparse vegetation, bogs, intertidal flats, water, sea, urban), 11 068 vertices clipped to the tile, median segment 54 m against 10 m cells. The EEA's public ArcGIS service (`image.discomap.eea.europa.eu`, `Corine/CLC2018_WM`) returns the same 11 068 clipped vertices and is the route for anyone without the file. What it forces on 16b's design: neighbouring polygons share their boundaries, so each shared edge arrives twice; the polygons run past the domain and must be clipped; the extract is committed as a fixture with the Copernicus attribution. Placed before 20c because 20c may split constraint segments and should be designed and measured on inputs that have interior ones | designed in 16's R6, no increment file yet; after the serial phase, before 20c | `docs/increments/16-domain-polygon.md` (R6) |
| 20c | Soft quality criterion: a penalty that each Steiner node or constraint split must pay for in angle gained, instead of 20's hard 25°; applied at the start and during DEM refinement; may split constraint segments when that improves the mesh. Ola's rulings on 20's C1-C3 | to design after 16b (`@architect` measures cost against 20 first) | `docs/increments/20-start-quality.md` (Ola's rulings) |
| — | Auto-catchment: the watershed upstream of a coordinate, computed from the DEM and handed to `--domain`, so a catchment no longer has to be supplied as a file (Ola, 2026-09-27: "not far into the future"). The textbook route is depression handling (Priority-Flood, Barnes, Lehman and Mulla 2014), D8 flow directions (O'Callaghan and Mark 1984) and accumulation, the pour point snapped to the strongest flow nearby, the upstream cells traced and their outline turned into a polygon; the literature check is `@architect`'s. Open for its design: whether it runs in the C++ core (a 10 m tile is 25 M cells); how a stair-stepped cell outline becomes a domain polygon, which meets input coarsening; and that a real catchment crosses tile edges, so it needs gap 6 (a DEM in several tiles) first. Legacy has nothing on it (`grep -rliE "watershed|flow.?acc|flow.?dir|pour.?point|catchment" legacy` returns no files) | to design; after gap 6, which it needs; placed after 20c, can move ahead of it on Ola's word | none yet |
Expand Down Expand Up @@ -87,7 +88,14 @@ as #93.
extent from its header, selecting the tiles that cover the area, refusing
mixed CRS, cell size or grid alignment, and stitching the selection into one
grid. Aligned tiles are one bigger grid, so everything downstream is
unchanged. Planned after increment 14; no record yet.
unchanged. **Designed as increment 15** (`docs/increments/15-dem-mosaic.md`):
15a and 15b are Norway (many tiles in one projected CRS; the domain in its
own CRS), 15c and 15d the São Francisco basin (geographic DEMs; memory).

**Order of work, ruled by Ola on 2026-09-27** ("My priorities are to get the
Norwegian cases sorted first"): 15a and 15b (Norwegian multi-tile DEM), then
16b (CORINE terrain polygons over Norway), then auto-catchment. The basin's
15c and 15d follow; 21d and domain decomposition come after.

Open and not MVP-blocking: **inputs in their own CRS**. The user (2026-09-26):
"I don't think the domain CRS should have to match the DEM CRS in the future.
Expand Down
4 changes: 2 additions & 2 deletions docs/increments/12-dem-to-mesh.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ fixture is decoded.
triangles, 3.0 s.
6. **Without `imagecodecs`, `decode_dem` refuses the fixture** with a
`GeoTiffError` that names Compression (259) = 5 and points at the `codecs`
extra (`src_python/tin_engine/io/geotiff.py:169`). Nothing new is needed
extra (`src_python/tin_engine/io/geotiff.py:207`). Nothing new is needed
for the no-extra case beyond turning that error into a usage error.

## Rulings
Expand Down Expand Up @@ -179,7 +179,7 @@ section), without changes:
`keep_alive`.
- The NoData value is converted to `T` exactly once. `decode_dem` already
refuses a sentinel the cell type cannot hold
(`src_python/tin_engine/io/geotiff.py:307`), so the conversion is exact.
(`src_python/tin_engine/io/geotiff.py:345`), so the conversion is exact.
- `sample(view, points)` takes a float64 `(N, 2)` array and returns
`(z, valid)`. It releases the GIL.
- `_core.pyi`: the class, the factory and `sample`.
Expand Down
Loading
Loading