diff --git a/ROADMAP.md b/ROADMAP.md index bc15d96e..9c56e5d0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -49,7 +49,7 @@ increment that most needs a picture to check against | 16c | A land-cover label per triangle: which input polygon (CORINE class) each triangle lies in, by a flood fill over the unconstrained edges. The bits on an edge say what kind of line it is; which class lies on each side is 16c's | **built** (2026-09-29, overnight; net 249 lines, one PR; Bygdin at 10 m: every triangle labelled, class shares equal to CORINE's clipped areas at two decimals). Designed by `@architect`, 2026-09-29: components across unconstrained edges, one point-in-polygon test per component, cell array `land_cover_code`, a natural-colour ParaView preset from `rasputin palette corine`; defaults D1-D7 for Ola to confirm; ~230 production lines. Ruled by Ola 2026-09-28 (16b's Q2) as a separate increment | `docs/increments/16c-landcover-labels.md`, `docs/increments/16b-terrain-polygons.md` (Q2, R6) | | 16d | A record of the input polylines in the mesh. Today a constraint edge keeps only its type bits; the source feature, its attributes (a river's name or order, a road's class) and which edges belong to one polyline are lost, and where edges from two features merge only the union of their bits survives. Proposed: a feature table (one row per input feature: source ID and attributes) and, per constraint edge, the list of features it came from (a list, since a merged edge belongs to several). The noder already tracks each piece's input chain; the open part is the output: a per-edge list in `.vtk`/`.ply`, or a side file | proposed by Ola 2026-09-28; to be designed by `@architect` once Ola has said what it is for; after 16b | - | | 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; **a requirement, not an option (Ola, 2026-09-29): "we will get an extreme amount of points in the catchment polygon. This must be taken into account, or the number of triangles will be overwhelming."** A cell outline has a vertex at every cell step (tens of thousands for a mid-sized catchment at 10 m, far more for Glomma), and every boundary edge is a constraint the mesh must honour; 16b measured what dense constraints cost (CORINE borders make the 10 m mesh 3.6x larger). The design must state how the outline is reduced, to what horizontal tolerance, and what it guarantees (a simple polygon, the pour point inside); the fine outline is defined on the DEM's node lattice (Ola, 2026-09-29: "Isn't the DEM points, really?"), but the reduced polygon's vertices need not be DEM nodes: off-node domain vertices are supported since 16 (Ola: "We have aleady established that the polygons and DEM don't need to match"), so vertices may be placed to keep the area; and the reduced polygon keeps approximately the fine one's area (Ola, 2026-09-29: "the resulting polygon has approximately the same area as the fine one"), since catchment area drives runoff volume. Literature to check: area-preserving polyline simplification (e.g. Bose et al. 2006; Kronenfeld et al. 2020, segment collapse); 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) | **increment 22, PR 1 built** (2026-09-29, overnight): the fine catchment, net 619 lines. Bygdin: 304.91 km² against NVE's 305.54 km² (−0.21 %), in 6.5 s. PR 2, the outline reduction, follows as a stacked PR. Designed by `@architect`, 2026-09-29, Bygdin first, moved ahead of 20c for Ola's night run: a lake polygon (CORINE) as the seed, a Priority-Flood labelling the lake's catchment, a marching-squares outline, area-preserving segment collapse to `--outline-tolerance` (default twice the cell). Two PRs on one branch; defaults for Ola to confirm are marked in the file | `docs/increments/22-auto-catchment.md` | +| — | 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; **a requirement, not an option (Ola, 2026-09-29): "we will get an extreme amount of points in the catchment polygon. This must be taken into account, or the number of triangles will be overwhelming."** A cell outline has a vertex at every cell step (tens of thousands for a mid-sized catchment at 10 m, far more for Glomma), and every boundary edge is a constraint the mesh must honour; 16b measured what dense constraints cost (CORINE borders make the 10 m mesh 3.6x larger). The design must state how the outline is reduced, to what horizontal tolerance, and what it guarantees (a simple polygon, the pour point inside); the fine outline is defined on the DEM's node lattice (Ola, 2026-09-29: "Isn't the DEM points, really?"), but the reduced polygon's vertices need not be DEM nodes: off-node domain vertices are supported since 16 (Ola: "We have aleady established that the polygons and DEM don't need to match"), so vertices may be placed to keep the area; and the reduced polygon keeps approximately the fine one's area (Ola, 2026-09-29: "the resulting polygon has approximately the same area as the fine one"), since catchment area drives runoff volume. Literature to check: area-preserving polyline simplification (e.g. Bose et al. 2006; Kronenfeld et al. 2020, segment collapse); 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) | **built as increment 22** (2026-09-29, overnight), in two PRs: PR 1, the fine catchment, net 619 lines; PR 2, the outline reduction, net 406 lines. Bygdin: 304.91 km² against NVE's 305.54 km² (−0.21 %), 99.1/99.3 % node overlap with NVE's polygon, 17 812 → 740 outline vertices at 20 m with the area kept, catchment in 6.5 s (`docs/benchmarks/2026-09-29/bygdin.md`). The reduction bounds the distance to the fine outline at its vertices only (measured on Bygdin: 19.92 m at 20 m). Parked for later (Ola, 2026-09-29): "It could be useful to have the option of some tolerance, doing more adaptive coarsening, but write it down and leave it for now": an enforced whole-outline tolerance as an option, which also belongs with 20c's input coarsening. Designed by `@architect`, 2026-09-29, Bygdin first, moved ahead of 20c for Ola's night run: a lake polygon (CORINE) as the seed, a Priority-Flood labelling the lake's catchment, a marching-squares outline, area-preserving segment collapse to `--outline-tolerance` (default twice the cell). Two PRs on one branch; defaults for Ola to confirm are marked in the file | `docs/increments/22-auto-catchment.md` | | — | `raster/`: grid-to-world geometry and bilinear sampling | shipped (`7785fea`), **no record** | none — predates the protocol | ## What stands between here and an operational MVP diff --git a/bindings/core.cpp b/bindings/core.cpp index 0382a3ae..69a6b848 100644 --- a/bindings/core.cpp +++ b/bindings/core.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include @@ -299,6 +300,17 @@ struct BoundUpstream { std::size_t cols; }; +// An (N, 2) float64 array as Point2 pairs, copied; any other shape is a ValueError. +[[nodiscard]] std::vector xy_points(const py::object& a, const char* what) { + const auto xy = py::array_t::ensure(a); + if (!xy || xy.ndim() != 2 || xy.shape(1) != 2) + throw py::value_error(std::format("reduce_ring: {} must have shape (N, 2)", what)); + std::vector out(static_cast(xy.shape(0))); + for (std::size_t i = 0; i < out.size(); ++i) + out[i] = Point2{xy.at(static_cast(i), 0), xy.at(static_cast(i), 1)}; + return out; +} + } // namespace PYBIND11_MODULE(_core, m) { @@ -1004,5 +1016,52 @@ The catchment of a seed mask: every node draining into a seed (Priority-Flood). seed is (rows, cols), non-zero (or True) for a seed; any other shape is a ValueError. NoData is never in. Releases the GIL. +)doc"); + + using terrain::vector_simplify::ReduceOutcome; + using terrain::vector_simplify::ReduceStatus; + py::enum_(m, "ReduceStatus", R"doc( +Why reduce_ring reduced a ring or did not: Ok, or a refusal of the input. +)doc") + .value("Ok", ReduceStatus::Ok) + .value("InvalidTolerance", ReduceStatus::InvalidTolerance) + .value("NotCounterClockwise", ReduceStatus::NotCounterClockwise) + .value("TooFewVertices", ReduceStatus::TooFewVertices); + + py::class_(m, "ReduceOutcome", R"doc( +What reduce_ring returned: the reduced ring, open, a status, and the counts. +)doc") + .def_property_readonly( + "ring", + [](const py::object& self) { + return point_view(self, self.cast().ring); + }, + "Read-only (M, 2) float64, open: the first vertex is not repeated.") + .def_readonly("status", &ReduceOutcome::status) + .def_property_readonly("collinear", [](const ReduceOutcome& o) { return o.counts.collinear; }) + .def_property_readonly("collapses", [](const ReduceOutcome& o) { return o.counts.collapses; }) + .def_property_readonly("rejected_crossing", + [](const ReduceOutcome& o) { return o.counts.rejected_crossing; }) + .def_property_readonly("rejected_seed", + [](const ReduceOutcome& o) { return o.counts.rejected_seed; }) + .def_property_readonly("rejected_tolerance", + [](const ReduceOutcome& o) { return o.counts.rejected_tolerance; }); + + m.def( + "reduce_ring", + [](const py::object& ring, double tolerance, const py::object& keep) { + const std::vector points = xy_points(ring, "ring"); + const std::vector kept = xy_points(keep, "keep"); + const py::gil_scoped_release unlocked; + return terrain::vector_simplify::reduce_ring( + points, tolerance, kept); + }, + py::arg("ring"), py::arg("tolerance"), py::arg("keep"), R"doc( +Reduce an open counter-clockwise ring to a tolerance, keeping its area and the +keep-points inside (area-preserving segment collapse, exact crossing tests). + +ring is (N, 2) and keep (K, 2), float64-convertible; any other shape is a +ValueError. Tolerance 0 only drops exactly collinear vertices. A refused input +comes back as a status. Releases the GIL. )doc"); } diff --git a/docs/benchmarks/2026-09-29/bygdin.md b/docs/benchmarks/2026-09-29/bygdin.md new file mode 100644 index 00000000..b2de0823 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin.md @@ -0,0 +1,68 @@ +# Increment 22 acceptance: Bygdin (@perf, 2026-09-29): summary + +**Verdict: ACCEPTED** against the design's acceptance, which asks for the +area to be within 2 % of NVE. The reduced catchment is **304.9096 km² +against NVE's 305.54 km², -0.206 %**. + +There is **no performance baseline**: this is the first `catchment` run and +the first mesh of this domain. Its speed figures are the baseline for the +next run. + +- **Run conditions**: AC power at the start and end of every block. The + commit is `6757925`, with a Release `_core`. Method, tables and raw logs: + `bygdin/README.md`. + +**`rasputin catchment`** (seed 8.5425 61.3512, CORINE lake, DTM10): + +- **Windows**: three (4.35 M, 8.15 M and 16.94 M nodes). The design + expected two. +- **Result**: 3,049,095 nodes. The fine outline has 17,812 vertices and the + reduced one 740, at 20 m. Both are 304.909550 km²; they differ by + -3e-07 m². +- **Hausdorff distance, fine to reduced**: 19.92 m at 20 m. +- **Times, median of 3**: flood 4.68 s, trace 0.13 s, reduce 0.04 s, wall + 6.53 s. +- **Memory**: max RSS 989 MB (peak footprint 920 MB). + +`--outline-tolerance` ladder (the area is equal at every step; all four +outlines are valid): + +| tolerance | vertices | Hausdorff | +|---|---|---| +| 0 | 5,513 | 0 m | +| 10 | 1,382 | 9.99 m | +| 20 | 740 | 19.92 m | +| 50 | 295 | 49.85 m | + +**Against NVE's polygon** (delfelt 1187, fetched without GDAL and not +committed; the README gives the command and the sha256), comparing DEM nodes: + +- 99.12 % of NVE's nodes are in ours; +- 99.33 % of ours are in NVE's; +- 2.05 km² are ours only, and 2.67 km² are NVE's only. + +This is the design's prototype figure, reproduced by the built code. + +**`rasputin mesh --domain`** (10 threads, median of 3): + +| domain | tol | triangles | refine | total | max RSS | worst angle | max degree | +|---|---|---|---|---|---|---|---| +| reduced | 10 m | 52,198 | 0.034 s | 0.36 s | 646 MB | 0.356° | 12 | +| fine | 10 m | 72,292 | 0.043 s | 0.39 s | 647 MB | 0.655° | 13 | +| reduced | 1 m | 1,129,026 | 0.497 s | 0.93 s | 925 MB | 0.046° | 18 | +| fine | 1 m | 1,137,703 | 0.483 s | 0.92 s | 942 MB | 0.083° | 16 | +| reduced + CORINE | 10 m | 83,171 | 0.046 s | 1.97 s | 776 MB | 0.0041° | 16 | + +- **Checks**: every mesh is within its tolerance, has 0 uncovered nodes and + 0 constrained-Delaunay violations. +- **The reduction** saves 27.8 % of the triangles at 10 m and 0.8 % at 1 m. +- **Land cover inside the catchment** (CORINE 2018): + - sparse vegetation (333) 42 %; + - bare rock (332) 21 %; + - heath (322) 17 %; + - water (512) 16 %; + - glacier (335) 2.4 %. + +**For the main session**: + +- No defects were found. diff --git a/docs/benchmarks/2026-09-29/bygdin/README.md b/docs/benchmarks/2026-09-29/bygdin/README.md new file mode 100644 index 00000000..75b845eb --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/README.md @@ -0,0 +1,265 @@ +# Increment 22 acceptance: Bygdin, end to end (@perf, 2026-09-29) + +The summary and the verdict are in `../bygdin.md`. This file has the method, +the tables and where every number comes from. + +## What was run + +- **Commit**: `6757925` on `increment22-autocatchment` (`logs/commit.txt`). +- **Build**: `build-pyext` (Release), rebuilt with + `cmake --build build-pyext -j --target _core` (exit 0, nothing out of + date) and copied into `.venv`; the installed `_core` sha256 is in + `logs/commit.txt`, equal to the build's. +- **Machine**: Apple M1 Max, 10 cores, 32 GB. `mesh` used 10 threads + (hardware concurrency, as `--stats` reports). +- **Power**: AC throughout. `pmset -g batt` was recorded at the start and end + of the catchment block, of the timed mesh block, and after the quality runs + (`logs/pmset_*.txt`): every one reads "AC Power, 80 %, AC attached". +- **Data**: DEM `../rasputin_data/DTM10_UTM33_20260925` (10 m, EPSG:25833). + Lakes and features: the CORINE 2018 Norway extract + `../rasputin_data/corine2018_dtm10_utm33.gpkg`, layer `corine2018`. +- **Seed**: `--seed 8.5425 61.3512` (EPSG:4326), as in the design; it falls + in CORINE's Bygdin polygon (39.936963 km², 399,371 nodes). +- **Reference**: NVE's reservoir catchment ("delfelt") 1187 "BYGDIN", + `delfeltAreal_km2` = 305.54. Its polygon was fetched on 2026-09-29 (by + curl, no GDAL) with exactly this command: + + ```sh + curl -sS --max-time 60 -o nve_delfelt_1187.geojson 'https://gis3.nve.no/map/rest/services/Mapservices/VassdragsreguleringVannkraft/MapServer/8/query?where=delfeltNr%3D1187&outFields=*&returnGeometry=true&outSR=25833&f=geojson' + ``` + + The file as fetched has sha256 + `fe6d298b08573e50e50e711f1be9f70e9459ea823a0626516a539cd62cc4e76c`: one + Polygon, 1376 coordinates, 305.5393 km² by shapely, valid. As the design + says, it is **not committed** (see "Data and licence" below). The + service can change, so a later fetch may not match the sha256. If it + does not, the overlap figures below apply to the file with this hash, + not to the new one. + +Scripts, both in this directory: + +- `run.sh` runs every timed block and writes the raw logs to `logs/`: + - `catchment` three times at the default tolerance, then once each at + `--outline-tolerance` 0, 10, 20 and 50; + - five mesh configurations, each three times with `--binary`; + - then each once more with `--ascii`, for the quality check. + Each command runs under `/usr/bin/time -l`, which gives wall time, + maximum RSS and peak memory footprint at the end of each `.err`. +- `analyse.py` reads the logs and outputs and prints `logs/analysis.txt`. + That is every table below. The quality check calls `tools/bench.py`'s + `quality()` and `read_vtk_ascii()` unchanged: the angles and degree as + `--stats` computes them, the tolerance check, and the constrained Delaunay + check. + +Meshes and catchment GeoJSONs other than the committed one were written to +the session scratchpad and are not committed. To regenerate them, run +`SCRATCH= bash docs/benchmarks/2026-09-29/bygdin/run.sh` from the +repository root with `.venv` active, fetch NVE's polygon as above, then +`python .../analyse.py `. +The committed `bygdin_reduced_t20.geojson` is `catchment_t20_run1.geojson`, +the default output. All four default-tolerance outputs have the same sha256, +`8083fb5b...`. + +## 1. `rasputin catchment` + +Windows, as every run printed them (`logs/catchment_*.err`; all seven runs +chose the same windows): + +| window | x (m) | y (m) | rows x cols | nodes | flood (s, run 1/2/3) | outcome | +|---|---|---|---|---|---|---| +| 1 | 140240-170470 | 6811200-6825570 | 1438 x 3024 | 4.35 M | 0.62 / 0.61 / 0.61 | grown on north, west, south, east | +| 2 | 136250-172700 | 6807210-6829560 | 2236 x 3646 | 8.15 M | 1.22 / 1.22 / 1.22 | grown on north, west | +| 3 | 128580-176700 | 6802360-6837550 | 3520 x 4813 | 16.94 M | 2.84 / 2.84 / 2.89 | contained | + +The design expected one growth step, to about 4100 x 3000 nodes (12 M). The +loop took two growth steps and ended at 16.9 M nodes. + +The result, identical in every run: + +| item | value | +|---|---| +| catchment nodes | 3,049,095 (304.909500 km² of node area) | +| fine outline | 17,812 vertices, 304.909550 km², 1 hole filled (0.000050 km²), 0 rings dropped | +| reduced outline (20 m) | 740 vertices, 304.909550 km², difference -2.98e-07 m² (-9.8e-16 relative) | +| against NVE's 305.54 km² | **-0.206 %** (fine and reduced alike; -0.630 km²) | +| Hausdorff, fine to reduced | 19.92 m at the 20 m tolerance | + +Times, the median of three runs at the default tolerance, from each run's +stderr and `/usr/bin/time -l`: + +| phase | median (s) | runs | +|---|---|---| +| flood, three windows summed | 4.68 | 4.68, 4.67, 4.72 | +| trace | 0.13 | 0.13, 0.14, 0.11 | +| reduce | 0.04 | 0.04, 0.04, 0.04 | +| the rest of the wall time (start-up, DEM read, seed, write) | 1.68 | by difference | +| **wall** | **6.53** | 6.71, 6.48, 6.53 | +| maximum RSS | 989 MB | 1069, 989, 982 MB | +| peak memory footprint | 920 MB | 923, 920, 861 MB | + +The "rest" row is the wall time less the three reported phases. It is not +broken down further: the command does not report a read time, and nothing was +profiled. + +**`--outline-tolerance` ladder.** One run each. The vertex count, area and +validity come from the output GeoJSON. The Hausdorff distance was measured +two ways between the fine outline's boundary and the reduced one's: + +- shapely `hausdorff_distance(..., densify=0.05)`, as the tests measure it; +- every 1 m sample of one boundary against the other's segments, both ways. + +| tolerance (m) | vertices | area (km²) | vs fine (m²) | vs NVE | Hausdorff, densify 0.05 | 1 m: fine to reduced, reduced to fine | valid | +|---|---|---|---|---|---|---|---| +| 0 | 5,513 | 304.909550 | 0 | -0.206 % | 0.00 m | 0.00, 0.00 m | yes | +| 10 | 1,382 | 304.909550 | 0 | -0.206 % | 9.99 m | 9.99, 9.96 m | yes | +| 20 (default) | 740 | 304.909550 | -2.98e-07 | -0.206 % | 19.92 m | 19.92, 19.62 m | yes | +| 50 | 295 | 304.909550 | -1.43e-06 | -0.206 % | 49.85 m | 49.85, 48.06 m | yes | + +The traced outline has 17,812 vertices. The Hausdorff distance stayed under +the tolerance at every step of the ladder. The design lists that as measured, +not guaranteed. + +## 2. Node overlap with NVE's polygon + +This compares the DEM nodes (10 m lattice, x and y multiples of 10) strictly +inside our fine outline with those inside NVE's polygon, using shapely +`contains_xy` over the union's bounds. + +| item | nodes | +|---|---| +| ours | 3,049,096 | +| NVE's | 3,055,360 | +| both | 3,028,611 | +| **NVE's nodes in ours** | **99.12 %** | +| **ours in NVE's** | **99.33 %** | +| ours only | 20,485 (2.05 km²) | +| NVE's only | 26,749 (2.67 km²) | + +Our count is one more than the command's 3,049,095. The fine outline has +one hole filled, of 50 m² (half a cell, the midpoint diamond around a single +out-node), so one node that is outside the catchment falls inside the filled +ring. + +The largest pieces of disagreement, as area and centroid in EPSG:25833, are +in `logs/analysis.txt`: + +- ours and not NVE's: 0.255 km² at (163743, 6813036); +- NVE's and not ours: 0.577 km² at (149046, 6826265). + +Between the two boundaries, the 1 m-sampled distance is at most 485 m from +ours to NVE's and 782 m from NVE's to ours. Why the divides differ was not +investigated. The prototype's figures in the design (99.1 % and 99.3 %, +-0.21 %) are reproduced by the built code. + +## 3. `rasputin mesh --domain` + +- **Timed runs**: three per configuration, `--binary`, `--stats`. The + medians are shown. +- **Refine and total** are from `--stats`. Total is the command body, without + interpreter start-up. +- **Wall and maximum RSS** are from `/usr/bin/time -l`. +- **Quality** is from the one `--ascii` run per configuration, which has the + same triangle count as the timed runs. +- "reduced" is the default 740-vertex outline; "fine" is + `--outline-tolerance 0` (5,513 vertices). + +| domain | tol | triangles | vertices | start vertices | refine (s) | total (s) | wall (s) | max RSS (MB) | worst angle | max degree | max error (m) | Delaunay checked / ambiguous / violations | +|---|---|---|---|---|---|---|---|---|---|---|---|---| +| reduced | 10 m | 52,198 | 26,576 | 740 | 0.034 | 0.356 | 0.62 | 646 | 0.356° | 12 | 9.9998 | 77,821 / 1,122 / 0 | +| fine | 10 m | 72,292 | 38,914 | 5,513 | 0.043 | 0.388 | 0.66 | 647 | 0.655° | 13 | 10.0000 | 105,671 / 1,546 / 0 | +| reduced | 1 m | 1,129,026 | 565,511 | 740 | 0.497 | 0.927 | 1.38 | 925 | 0.0461° | 18 | 1.0000 | 1,692,542 / 172,872 / 0 | +| fine | 1 m | 1,137,703 | 571,651 | 5,513 | 0.483 | 0.917 | 1.38 | 942 | 0.0834° | 16 | 1.0000 | 1,703,756 / 172,995 / 0 | +| reduced + CORINE | 10 m | 83,171 | 42,110 | 8,242 | 0.046 | 1.972 | 2.24 | 776 | 0.00408° | 16 | 9.9997 | 116,398 / 1,095 / 0 | + +Every run is within its tolerance, with 0 valid DEM nodes uncovered and 0 +Delaunay violations. Per-run figures are in `logs/analysis.txt`. The +refine-time spread between runs is at most 0.008 s. + +**What the reduction saves.** + +- At 10 m it saves 27.8 % of the triangles: 52,198 against 72,292. +- At 1 m it saves 0.8 %: 1,129,026 against 1,137,703. At that tolerance the + interior dominates the triangle count, and the outline does not. +- At both tolerances the reduced outline gives the smaller worst angle: + 0.356° against 0.655° at 10 m, and 0.046° against 0.083° at 1 m. The + maximum degree is 12 against 13 at 10 m, and 18 against 16 at 1 m. The + cause was not measured. + +**Land cover inside the catchment** (`--features +--features-layer corine2018 --features-map corine`, 10 m). stderr reports: + +- 79 features kept, 342 dropped outside, 35 clipped; +- 15,846 input vertices, 8,242 after noding. + +In the mesh: + +- 8,882 constraint edges; +- 7,835 edge cells with `land_cover` = 1; +- 2,005 edge cells with `water` = 1. + +The worst angle, 0.00408°, comes with the CORINE constraints; without them +the same domain gives 0.356°. Of the 1.97 s total, refine takes 0.046 s. The +rest is the feature read, clip and noding, which were not broken down here +(see the `.stats.md` phase table). + +The class areas below come from clipping CORINE to the reduced polygon with +shapely, which is separate from the mesh: + +| CORINE code | class | km² | share | +|---|---|---|---| +| 333 | sparsely vegetated areas | 129.093 | 42.34 % | +| 332 | bare rocks | 65.012 | 21.32 % | +| 322 | moors and heathland | 51.849 | 17.00 % | +| 512 | water bodies | 50.191 | 16.46 % | +| 335 | glaciers and perpetual snow | 7.452 | 2.44 % | +| 412 | peat bogs | 1.021 | 0.33 % | +| 142 | sport and leisure facilities | 0.292 | 0.10 % | + +The classes cover 304.910 km² of the polygon's 304.910 km². + +## No performance baseline + +This is the first run of `catchment`, and the first mesh of this domain. +There is no earlier figure on AC or on battery to compare speed against, so +the speed figures above are the baseline for the next increment. The +increment does not change refine. The 1 m benchmark and the thread sweep +were not run; this brief did not ask for them. + +## Data and licence + +- **NVE's catchment polygon** is © Norges vassdrags- og energidirektorat + (NVE). It is NVE's reservoir sub-catchment ("delfelt") 1187, "BYGDIN", + from the map service VassdragsreguleringVannkraft, layer 8 + (https://gis3.nve.no/map/rest/services/Mapservices/VassdragsreguleringVannkraft/MapServer/8). + It is not a REGINE unit. + - It is used under the Norwegian Licence for Open Government Data + (NLOD), which is compatible with CC BY 4.0. NVE asks users to credit + the rights holder and link to its services: + https://konto.nve.no/Information. + - The polygon was committed in `ad3df66` and removed in `1985720`, so it + stays in the history. + - To reproduce the overlap figures, fetch it with the curl command under + "What was run". + - The main session checked these terms on 2026-09-29. +- **The DEM**, DTM10 (`DTM10_UTM33_20260925`), is © Kartverket. +- **CORINE**, the lake polygon and the `--features` land cover, is credited + as increment 16b's NOTICE words it (`CORINE_NOTICE` in + `src_python/tin_engine/feature_input.py`): "Contains modified CORINE Land + Cover 2018 data (version 2020_20u1), (c) European Union, Copernicus Land + Monitoring Service 2018, European Environment Agency (EEA): clipped and + re-encoded, produced with funding by the European Union, not endorsed by + the EU". + +## Files + +- `run.sh`, `analyse.py`: the scripts above. +- `logs/`: + - `*.err`: stderr with `/usr/bin/time -l`; + - `*.out`: stdout; + - `*.stats.md`: `mesh --stats`; + - `exits.txt`: every command's exit status, all 0; + - `pmset_*.txt`; + - `commit.txt`; + - `analysis.txt`. +- `bygdin_reduced_t20.geojson`: the reduced catchment (740 vertices, + EPSG:25833), which `mesh --domain` reads. diff --git a/docs/benchmarks/2026-09-29/bygdin/analyse.py b/docs/benchmarks/2026-09-29/bygdin/analyse.py new file mode 100644 index 00000000..75b8a43d --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/analyse.py @@ -0,0 +1,157 @@ +"""Increment 22 acceptance on Bygdin: the numbers in README.md, from run.sh's +logs and outputs. Usage, from the repository root with .venv active: + + python docs/benchmarks/2026-09-29/bygdin/analyse.py $SCRATCH $NVE_GEOJSON > .../logs/analysis.txt +""" + +import json +import re +import sqlite3 +import statistics +import sys +from pathlib import Path + +import numpy as np +import shapely +from shapely.geometry import shape + +sys.path.insert(0, "tools") +import bench # noqa: E402 quality() and read_vtk_ascii(), unchanged + +from tin_engine.io.geopackage import decode_geometry # noqa: E402 + +HERE = Path("docs/benchmarks/2026-09-29/bygdin") +LOG = HERE / "logs" +SCRATCH = Path(sys.argv[1]) +NVE = Path(sys.argv[2]) # NVE's delfelt 1187 GeoJSON, fetched as README.md says +NVE_KM2 = 305.54 # delfeltAreal_km2 of delfelt 1187 +CELL = 10.0 + + +def polygon(path: Path) -> shapely.Polygon: + return shape(json.loads(path.read_text())["features"][0]["geometry"]) + + +def hausdorff_1m(a: shapely.LineString, b: shapely.LineString) -> tuple[float, float]: + """Directed distances a->b and b->a: every 1 m sample of one boundary to + the other boundary's segments (exact point-segment distance).""" + + def directed(p: shapely.LineString, q: shapely.LineString) -> float: + pts = shapely.points(shapely.get_coordinates(shapely.segmentize(p, 1.0))) + c = shapely.get_coordinates(q) + segs = shapely.linestrings(np.stack([c[:-1], c[1:]], axis=1)) + _, d = shapely.STRtree(segs).query_nearest(pts, return_distance=True, all_matches=False) + return float(d.max()) + + return directed(a, b), directed(b, a) + + +print("## catchment") +walls, floods, traces, reduces, rss, foot = [], [], [], [], [], [] +for i in (1, 2, 3): + err = (LOG / f"catchment_t20_run{i}.err").read_text() + floods.append(sum(float(x) for x in re.findall(r"flood ([\d.]+) s", err))) + traces.append(float(re.search(r"traced in ([\d.]+) s", err).group(1))) + reduces.append(float(re.search(r"tolerance 20 m, ([\d.]+) s", err).group(1))) + walls.append(float(re.search(r"([\d.]+) real", err).group(1))) + rss.append(int(re.search(r"(\d+)\s+maximum resident", err).group(1))) + foot.append(int(re.search(r"(\d+)\s+peak memory footprint", err).group(1))) + per_window = re.findall(r"window (\d): .*?flood ([\d.]+) s", err) + print(f"run {i}: windows {per_window}, flood sum {floods[-1]:.2f}, trace {traces[-1]}," + f" reduce {reduces[-1]}, wall {walls[-1]}, maxRSS {rss[-1]/1e6:.0f} MB," + f" peak footprint {foot[-1]/1e6:.0f} MB") +med = statistics.median +print(f"median: flood {med(floods):.2f} s, trace {med(traces):.2f} s, reduce {med(reduces):.2f} s," + f" wall {med(walls):.2f} s, other (read, seed, write, start-up) {med(walls)-med(floods)-med(traces)-med(reduces):.2f} s," + f" maxRSS {med(rss)/1e6:.0f} MB, peak footprint {med(foot)/1e6:.0f} MB") + +fine = polygon(SCRATCH / "catchment_t0.geojson") +nve = polygon(NVE) +print(f"\nNVE polygon: {nve.area/1e6:.4f} km2 by shapely, {shapely.get_num_coordinates(nve)} coords," + f" valid {nve.is_valid}; published {NVE_KM2} km2") +print("\n| tolerance | vertices | area km2 | vs fine m2 | vs NVE 305.54 | Hausdorff densify=0.05 | 1 m: fine->red, red->fine | valid |") +for t in (0, 10, 20, 50): + red = polygon(SCRATCH / f"catchment_t{t}.geojson") + n = shapely.get_num_coordinates(red.exterior) - 1 + h = shapely.hausdorff_distance(fine.exterior, red.exterior, densify=0.05) + f2r, r2f = hausdorff_1m(fine.exterior, red.exterior) + print(f"| {t} | {n} | {red.area/1e6:.6f} | {red.area-fine.area:+.3g} |" + f" {100*(red.area/1e6-NVE_KM2)/NVE_KM2:+.3f} % | {h:.2f} m | {f2r:.2f}, {r2f:.2f} m | {red.is_valid} |") + props = json.loads((SCRATCH / f"catchment_t{t}.geojson").read_text())["features"][0]["properties"] + if t == 20: + print(f" properties: {props}") + +f2n, n2f = hausdorff_1m(fine.exterior, nve.exterior) +print(f"\nfine outline vs NVE boundary, 1 m samples: fine->NVE {f2n:.0f} m, NVE->fine {n2f:.0f} m") + +print("\n## node overlap with NVE (DEM nodes at multiples of 10 m)") +x0, y0, x1, y1 = shapely.union(fine, nve).bounds +xs = np.arange(np.floor(x0 / CELL) * CELL, x1 + CELL, CELL) +ys = np.arange(np.floor(y0 / CELL) * CELL, y1 + CELL, CELL) +gx, gy = np.meshgrid(xs, ys) +shapely.prepare(fine) +shapely.prepare(nve) +ours = shapely.contains_xy(fine, gx, gy) +theirs = shapely.contains_xy(nve, gx, gy) +both = ours & theirs +print(f"ours {ours.sum()} nodes (catchment reports 3049095), NVE {theirs.sum()} nodes, both {both.sum()}") +print(f"of NVE's nodes in ours: {100*both.sum()/theirs.sum():.2f} %; of ours in NVE's: {100*both.sum()/ours.sum():.2f} %") +print(f"ours only {(ours & ~theirs).sum()} nodes ({(ours & ~theirs).sum()*1e-4:.2f} km2)," + f" NVE only {(theirs & ~ours).sum()} nodes ({(theirs & ~ours).sum()*1e-4:.2f} km2)") +diff = shapely.difference(fine, nve) +parts = sorted(getattr(diff, "geoms", [diff]), key=lambda g: -g.area)[:3] +print("largest ours-not-NVE parts:", [(round(p.area / 1e6, 3), tuple(round(v) for v in p.centroid.coords[0])) for p in parts]) +diff = shapely.difference(nve, fine) +parts = sorted(getattr(diff, "geoms", [diff]), key=lambda g: -g.area)[:3] +print("largest NVE-not-ours parts:", [(round(p.area / 1e6, 3), tuple(round(v) for v in p.centroid.coords[0])) for p in parts]) + +print("\n## meshes (median of 3 binary runs; quality from the ASCII run)") +print("| run | triangles | vertices | start vertices | constraint edges | refine s | total s | wall s | maxRSS MB | worst angle | max degree | max error | within tol | Delaunay checked/ambiguous/violations |") +for tag, tol in (("red_t10", 10), ("fine_t10", 10), ("red_t1", 1), ("fine_t1", 1), ("feat_t10", 10)): + rows = [] + for i in (1, 2, 3): + md = (LOG / f"mesh_{tag}_run{i}.stats.md").read_text() + err = (LOG / f"mesh_{tag}_run{i}.err").read_text() + rows.append(( + float(re.search(r"^\| refine \| ([\d.]+)", md, re.M).group(1)), + float(re.search(r"^\| \*\*total\*\* \| \*\*([\d.]+)", md, re.M).group(1)), + float(re.search(r"([\d.]+) real", err).group(1)), + int(re.search(r"(\d+)\s+maximum resident", err).group(1)), + )) + get = lambda k: med(r[k] for r in rows) # noqa: E731 + md = (LOG / f"mesh_{tag}_ascii.stats.md").read_text() + err = (LOG / f"mesh_{tag}_ascii.err").read_text() + item = lambda name: int(re.search(rf"^\| {name} \| (\d+)", md, re.M).group(1)) # noqa: E731 + max_error = float(re.search(r"achieved max error ([\d.]+) m", err).group(1)) + m = bench.read_vtk_ascii(SCRATCH / f"mesh_{tag}_ascii.vtk") + q = bench.quality(m.points, m.triangles, m.edges, tol, max_error) + print(f"| {tag} | {item('output triangles')} | {item('output vertices')} | {item('start vertices')} |" + f" {item('constraint edges')} | {get(0):.3f} | {get(1):.3f} | {get(2):.2f} | {get(3)/1e6:.0f} |" + f" {q.worst_angle:.4g}° | {q.max_degree} | {max_error:.4f} | {q.within_tolerance} |" + f" {q.delaunay_checked}/{q.delaunay_ambiguous}/{q.delaunay_violations} |") + print(f" runs (refine, total, wall, maxRSS): {rows}") + +print("\n## land cover inside the reduced catchment (CORINE 2018, clipped by shapely)") +red = polygon(SCRATCH / "catchment_t20.geojson") +conn = sqlite3.connect("../rasputin_data/corine2018_dtm10_utm33.gpkg") +bx = red.bounds +ids = [r[0] for r in conn.execute( + "SELECT id FROM rtree_corine2018_geom WHERE maxx >= ? AND minx <= ? AND maxy >= ? AND miny <= ?", + (bx[0], bx[2], bx[1], bx[3]))] +area: dict[str, float] = {} +for fid in ids: + blob, code = conn.execute("SELECT geom, code_18 FROM corine2018 WHERE fid = ?", (fid,)).fetchone() + a = shapely.intersection(decode_geometry(blob)[1], red).area + if a > 0: + area[code] = area.get(code, 0.0) + a +total = sum(area.values()) +print(f"{len(ids)} candidate rows; covered {total/1e6:.3f} km2 of {red.area/1e6:.3f}") +for code, a in sorted(area.items(), key=lambda kv: -kv[1]): + print(f"| {code} | {a/1e6:.3f} km2 | {100*a/red.area:.2f} % |") +m = bench.read_vtk_ascii(SCRATCH / "mesh_feat_t10_ascii.vtk") +text = (SCRATCH / "mesh_feat_t10_ascii.vtk").read_text(encoding="latin-1").split("\n") +for name in ("land_cover", "water"): + i = next(k for k, line in enumerate(text) if line.startswith(f"{name} 1 ")) + n = int(text[i].split()[2]) + vals = np.array(" ".join(text[i + 1 : i + 1 + n]).split()[:n], dtype=int) + print(f"mesh cells with {name}=1: {int(vals.sum())} (of {n} cells: {len(m.edges)} lines + {len(m.triangles)} triangles)") diff --git a/docs/benchmarks/2026-09-29/bygdin/bygdin_reduced_t20.geojson b/docs/benchmarks/2026-09-29/bygdin/bygdin_reduced_t20.geojson new file mode 100644 index 00000000..5a63f92e --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/bygdin_reduced_t20.geojson @@ -0,0 +1 @@ +{"type": "FeatureCollection", "crs": {"type": "name", "properties": {"name": "EPSG:25833"}}, "features": [{"type": "Feature", "properties": {"seed": [8.5425, 61.3512], "seed_crs": "EPSG:4326", "nodes": 3049095, "fine_vertices": 17812, "fine_area_m2": 304909550.0, "reduced_vertices": 740, "reduced_area_m2": 304909549.9999997, "outline_tolerance_m": 20.0, "windows": [[1438, 3024], [2236, 3646], [3520, 4813]]}, "geometry": {"type": "Polygon", "coordinates": [[[137640.0, 6832215.0], [137597.74193548388, 6832243.172043011], [137458.29391628047, 6832099.4653071035], [137272.3691336265, 6832052.957247976], [137184.90341039226, 6831980.659617026], [137201.01864562993, 6831903.119034275], [136903.35714285713, 6831628.214285715], [136951.62316462147, 6831459.250051314], [136817.0754716981, 6831303.773584906], [136830.00038878224, 6831200.698025623], [136684.96189964417, 6831184.071259593], [136435.6189184878, 6831273.199872388], [136395.8257108954, 6831332.053447732], [135865.0, 6831270.0], [135885.40185471406, 6831247.897990727], [135635.0, 6830980.0], [135645.86030125848, 6830903.977891191], [135592.49409990516, 6830828.017502136], [135632.2171388938, 6830610.685336256], [135633.68724762482, 6830376.88081057], [135589.39452549722, 6830304.652792333], [135964.71533574187, 6829950.284664258], [136513.89924577868, 6829517.547606546], [136530.10616212134, 6829406.420685099], [136610.09717929043, 6829289.689134087], [136572.75051124743, 6829053.128834356], [136676.59931295138, 6828870.89274336], [136835.26730887045, 6828688.502266878], [136877.84676395185, 6828693.759262805], [137088.33333333334, 6828465.0], [137057.04474659674, 6828423.815620678], [137186.22954293294, 6828252.867802737], [137245.515878252, 6828247.818994536], [137496.96684188143, 6827874.340167733], [137586.42857142858, 6827642.857142857], [137591.7123287671, 6827533.287671233], [137650.25901788575, 6827515.823043272], [137722.1739130435, 6827398.043478261], [137818.72049870715, 6827340.082686023], [138004.6911740526, 6827112.268315339], [138129.00346474705, 6827096.180873251], [138191.4623733689, 6826868.500055129], [138135.0, 6826840.0], [138257.3819876784, 6826602.968908627], [138238.62261836408, 6826547.302507902], [138328.53135983145, 6826454.936038019], [138303.061546746, 6826412.9448269205], [138419.98539481877, 6826358.102763013], [138385.0, 6826290.0], [138514.84023017494, 6826180.874040825], [138576.58487743896, 6825839.170865189], [138718.05190812348, 6825860.803012173], [138802.4414514038, 6825760.811100701], [138781.7282051282, 6825686.420512821], [138844.19485053458, 6825643.713724635], [138863.115942029, 6825468.115942029], [138808.125, 6825413.125], [138893.70368496655, 6825265.600430721], [138929.1959398952, 6825247.993253361], [138960.0, 6825125.0], [139175.0, 6824910.0], [139116.94111216627, 6824813.235186944], [138949.77487567315, 6824698.088308348], [139098.8443611255, 6824566.148784354], [139065.0, 6824470.0], [139120.51022533225, 6824410.39306786], [138917.7332545976, 6824214.83718576], [138941.75736597166, 6824169.45830872], [139011.99137029288, 6824182.391474895], [139117.5, 6824107.5], [139035.84347231753, 6823966.130352153], [139261.66666666666, 6823673.333333333], [139288.7288135593, 6823534.406779661], [139345.0, 6823450.0], [139371.095371097, 6823292.340466289], [139498.8679245283, 6823156.509433962], [139550.6470393466, 6823014.014679101], [139505.71212772815, 6822934.286085522], [139649.8381673627, 6822703.225457756], [139763.43880354054, 6822680.531590193], [139845.0, 6822563.895642195], [139845.0, 6822510.0], [139933.17778456188, 6822421.822215438], [140025.35194260805, 6822426.207276433], [140062.3109441052, 6822509.62188821], [140168.80157170922, 6822471.640471512], [140210.0, 6822525.0], [140505.9208167372, 6822229.079183263], [140604.20249812232, 6822247.359009317], [140804.2085916462, 6821961.682867177], [140899.38604496184, 6822003.772089924], [140936.32, 6822077.64], [141007.86755289164, 6822086.714784027], [141157.0, 6821912.0], [141098.07046615635, 6821872.320620127], [141142.49400479617, 6821822.482014389], [141112.51491139995, 6821701.06200086], [141355.0, 6821440.0], [141336.42857142858, 6821318.571428572], [141365.0, 6821290.0], [141224.0416141236, 6821147.389659521], [141408.40847831414, 6820937.04239157], [141395.0, 6820870.0], [141499.44444444444, 6820765.555555556], [141465.45526353584, 6820730.716645124], [141593.33333333334, 6820571.666666667], [141569.6974925791, 6820536.908077322], [141659.26065162907, 6820424.260651629], [141651.84448836243, 6820324.296281796], [141772.8260869565, 6820162.173913044], [141816.25, 6820061.25], [142002.6073224465, 6820199.636999488], [142075.57326495965, 6820154.970894979], [142145.0, 6820180.0], [142126.74311926606, 6820253.302752294], [142205.56356523783, 6820277.275755774], [142217.27272727274, 6820382.2727272725], [142326.66666666666, 6820308.333333333], [142388.09997058453, 6820353.3151237555], [142415.0, 6820220.0], [142562.42090701312, 6820072.579092987], [142496.78952869328, 6819997.666816089], [142545.0, 6819670.0], [142500.23076923078, 6819625.230769231], [142605.5162615763, 6819519.49576577], [142622.45828279847, 6819373.558505002], [142764.9480968858, 6819290.017301038], [142802.37950475726, 6819326.935949897], [142957.33098629283, 6819326.905978922], [143108.52788881605, 6819207.685083574], [143320.7669052955, 6819176.611933246], [143329.91124260356, 6819067.061143984], [143440.0, 6818955.0], [143500.0, 6818955.0], [143668.3153049157, 6818845.289064038], [143728.84615384616, 6818876.923076923], [143832.3291834707, 6818814.509030335], [143810.82940272454, 6818728.670749027], [143766.25954198473, 6818729.122137405], [143798.94779576827, 6818644.411090414], [143724.88005997002, 6818546.64167916], [143728.33333333334, 6818373.333333333], [143665.70455182076, 6818215.276290152], [143951.67372638808, 6818022.302804808], [144000.3285880899, 6817943.895927811], [144332.65235579392, 6817745.0], [144365.87257886506, 6817745.0], [144498.42865702798, 6817640.664382635], [144528.09024704012, 6817525.1893014405], [144490.5572646479, 6817377.004492921], [144252.18583446817, 6817108.268572732], [144020.63138396858, 6817064.226122432], [144103.0, 6816882.0], [144078.0923694779, 6816670.361445783], [144277.27753076606, 6816595.668464988], [144557.5, 6816682.5], [144680.52583922824, 6816643.4437025385], [144647.251725348, 6816502.276289624], [144749.5221573919, 6816395.708043541], [144745.86872586873, 6816358.281853282], [144850.0, 6816345.0], [145040.46826041545, 6816171.163978183], [145023.57142857142, 6816067.142857143], [145102.99888205703, 6815921.453325881], [145065.0, 6815880.0], [145013.74513079796, 6815591.887975379], [145075.07522179233, 6815561.809962153], [145124.17391542502, 6815188.856079254], [145175.52631578947, 6815144.736842105], [145195.0, 6815040.0], [145155.0, 6815000.0], [145172.14285714287, 6814947.142857143], [145111.0, 6814886.0], [145139.67534438922, 6814816.195210808], [145468.04373820877, 6814477.181769896], [145615.5698981443, 6814257.181863997], [145604.56123577632, 6814219.313332136], [145914.0, 6813891.8], [146001.37096774194, 6813903.629032258], [146144.44444444444, 6813760.555555556], [146350.0, 6813645.0], [146335.0, 6813620.0], [146401.43835616438, 6813606.712328767], [146405.0, 6813460.0], [146366.15659509707, 6813430.677432868], [146527.69600301265, 6813171.358123098], [146503.70486860673, 6813136.148679506], [146611.05376965224, 6813106.046539101], [146847.60120585765, 6813085.9007122805], [146966.527700098, 6812958.03581416], [147052.18302184745, 6812997.871228581], [147169.1780821918, 6812955.273972603], [147196.0, 6812995.0], [147346.35462052366, 6812939.574463671], [147421.92532942898, 6812993.97510981], [147479.09752825845, 6812965.4824469825], [147558.09108487266, 6813045.788113164], [147519.6041081912, 6813077.697945904], [147608.4982822893, 6813168.276441781], [147667.1334431631, 6813151.573311367], [147790.91385169327, 6813284.549521231], [147842.47737909516, 6813293.602184087], [148220.3207003201, 6813636.607356718], [148361.08580106302, 6813519.130599848], [148460.0, 6813485.0], [148493.6742456259, 6813531.714563435], [148606.3060144191, 6813516.527023941], [148728.69185386883, 6813553.855230316], [148879.0, 6813714.0], [148892.34732105123, 6813769.057699337], [149049.7259037944, 6813832.720556439], [149340.6027820711, 6813827.364760432], [149443.44871181578, 6813949.081502975], [149783.62941755238, 6814088.053182289], [149787.3673469388, 6814149.102040816], [149913.8162243507, 6814447.021466103], [150027.98398457805, 6814525.496019012], [150141.04797171257, 6814506.503744745], [150194.78260869565, 6814540.217391305], [150435.0, 6814530.0], [150360.75, 6814604.25], [150416.25, 6814698.75], [150445.8695652174, 6814670.869565218], [150597.77777777778, 6814822.777777778], [150799.40637330865, 6814738.635584262], [150994.22455816425, 6814787.81637211], [151012.97354325702, 6814963.590357457], [150951.63579225936, 6815083.9067151835], [150962.72727272726, 6815115.0], [151030.48751697718, 6815110.134990859], [151102.07909634485, 6815245.279159011], [151271.65451519756, 6815239.984295697], [151308.7682386734, 6815311.079564392], [151460.57106393375, 6815222.896297737], [151563.125, 6815341.875], [151684.30056524486, 6815324.256142117], [151834.57169459964, 6815543.575418995], [151893.27946552495, 6815707.847712303], [151880.50264550265, 6815858.783068783], [151990.6010042372, 6815986.497164915], [152060.12504789885, 6816209.749904202], [152035.0, 6816290.0], [152084.0909090909, 6816311.515151516], [152092.80805687205, 6816360.912322275], [152299.2829360342, 6816553.41516797], [152422.85714285713, 6816443.571428572], [152477.49710791605, 6816486.557593786], [152521.42857142858, 6816456.428571428], [152592.2393430768, 6816512.669524852], [152596.76479812863, 6816637.450847148], [152549.84938495586, 6816705.217555064], [152762.9001756771, 6816982.259353165], [152932.75378831074, 6816779.468768818], [153164.8737720111, 6816642.538832252], [153165.02191609266, 6816592.614276769], [153287.77621563748, 6816513.054280999], [153606.79801008, 6816526.356951625], [153739.00240476284, 6816450.161591488], [153688.694621263, 6816254.935961682], [153870.23303808243, 6816121.92212627], [153852.40340794573, 6816058.978151899], [154044.0, 6816041.0], [154143.77704498603, 6815941.222955014], [154418.8745345136, 6816050.63404243], [154483.23751478214, 6816023.997543892], [154682.17736722674, 6816036.966404154], [154750.1219986578, 6816065.8835090455], [154933.96544251748, 6816022.994869282], [155041.75461367719, 6816104.826975314], [155126.02943487625, 6816021.415705997], [155364.18056245425, 6815908.366837521], [155473.4518828452, 6815801.338912134], [155576.0787598512, 6815918.459955267], [155673.1800339771, 6815792.479119474], [155757.8260869565, 6815830.217391305], [155829.44544463628, 6815790.353100789], [155879.4713349174, 6815939.219827721], [156068.19036510444, 6815921.3572261715], [156182.09302325582, 6815835.930232558], [156202.26098923897, 6815726.969625716], [156260.625, 6815685.0], [156376.49083068728, 6815772.95243927], [156345.0, 6815850.0], [156415.0, 6815960.0], [156350.33945324935, 6816024.449926077], [156217.47260584292, 6816064.129555845], [156130.1851851852, 6816183.333333333], [156111.44192111958, 6816302.650209459], [156391.81677091605, 6816286.592340345], [156426.4556794017, 6816495.392917727], [156558.90990598194, 6816665.272419229], [156707.94926474956, 6816481.194127676], [157091.39304937274, 6816355.131812555], [157100.0, 6816425.0], [157305.47173560766, 6816362.353989709], [157390.1019995881, 6816381.039124564], [157273.75, 6816628.75], [157337.97297297296, 6816740.540540541], [157316.11111111112, 6816773.333333333], [157380.42857142858, 6816864.571428572], [157345.0, 6816900.0], [157474.2857142857, 6817029.285714285], [157510.0, 6816995.0], [157604.04255319148, 6817011.808510638], [157765.5383315261, 6816937.592302455], [157812.5, 6816973.0], [157925.68808132003, 6816903.948551952], [157895.0, 6816803.333333333], [157951.5116479476, 6816690.878120243], [157875.94978325407, 6816565.120443983], [157930.0, 6816505.0], [158094.7585828061, 6816526.831066811], [158441.32971239905, 6816305.462029215], [158484.7425422148, 6816089.556151916], [158755.6582352538, 6815868.595579216], [158717.7460571056, 6815769.164069157], [158766.58437423463, 6815731.4305657605], [159016.40058227236, 6815653.148119857], [158997.54284853733, 6815563.793145235], [159254.1424648113, 6815392.845778662], [159190.83333333334, 6815303.229166667], [159241.66666666666, 6815233.333333333], [159330.39325842698, 6815254.7191011235], [159444.41583315123, 6815179.457590648], [159518.08560806658, 6815001.26311421], [159610.0, 6814955.0], [159655.0, 6815000.0], [159669.50205278205, 6815115.172084419], [159871.07197186654, 6815149.299696093], [159952.22222222222, 6815107.222222222], [160069.10729420255, 6815179.692141028], [160134.23048735395, 6815139.763186853], [160204.5, 6815139.5], [160398.81895860942, 6815238.118430292], [160522.43506943982, 6815104.396783581], [160624.0604394343, 6815155.430899723], [160830.04368030006, 6814930.551773602], [161066.24030172775, 6814855.777698366], [161350.01393145724, 6814953.180551685], [161510.0, 6814915.0], [161598.26306397247, 6814975.643329668], [161780.19607843139, 6814952.647058823], [161840.0, 6814845.0], [161876.86315789475, 6814861.484210527], [161954.70612328488, 6814750.924636494], [162190.0934019556, 6814568.886670609], [162169.10988743644, 6814521.359502801], [162266.37049742692, 6814460.777893015], [162299.0909090909, 6814490.454545454], [162350.0, 6814465.0], [162403.04314329737, 6814538.998459168], [162378.6015243573, 6814574.734254772], [162522.26744186046, 6814651.220930233], [162516.74387723103, 6814616.8511016825], [162587.29036769632, 6814563.175514227], [162620.0, 6814415.0], [162751.02946044356, 6814479.826216484], [162840.0, 6814475.0], [162961.65952121164, 6814353.340478788], [163276.6738121217, 6814158.326187878], [163248.3261878783, 6814106.673812122], [163318.57142857142, 6814036.428571428], [163410.0, 6814085.0], [163484.88372093023, 6814020.813953488], [163609.2154664364, 6814065.748037321], [163892.62440494008, 6813782.779033474], [163741.62921348313, 6813622.471910113], [163552.71878099983, 6813603.240766249], [163471.222741433, 6813515.288161994], [163552.03058304757, 6813460.807038269], [163423.73463132296, 6813291.364642098], [163644.32819325296, 6813163.016771873], [163567.88869182774, 6812999.652704162], [163698.85066599218, 6812751.698001093], [163635.8378165902, 6812658.929392556], [163821.70311841829, 6812385.71047471], [163808.98071837446, 6812291.979156205], [163917.43243243243, 6812173.513513514], [163923.49914439756, 6812025.5785719445], [164084.99727816283, 6811926.5085076], [164173.68676790822, 6811790.963314203], [164263.90259560433, 6811485.964099594], [164428.94132231225, 6811306.58511767], [164441.15527007577, 6811201.8750435], [164543.2696481575, 6811027.505426047], [164525.81517928684, 6810915.243012267], [164687.76002290647, 6810745.419509138], [164648.83532978804, 6810678.5318045085], [164645.0, 6810520.0], [164790.76588878914, 6810427.424689603], [165041.0616470588, 6810353.613647059], [165255.01829893093, 6810409.439022539], [165355.3015424999, 6810379.725468889], [165807.77777777778, 6810492.777777778], [165862.34620659557, 6810453.302744165], [166164.88580153522, 6810652.314058491], [166357.33333333334, 6810733.222222222], [166504.79644088983, 6810702.478892937], [166738.2570838119, 6810924.066940101], [167062.79121971395, 6810825.960480761], [167499.8949211909, 6810833.021015761], [167622.72727272726, 6810912.2727272725], [167650.0, 6810885.0], [167833.04171334364, 6810991.066069858], [167898.69837386673, 6811139.245934667], [168105.2605042017, 6811334.478991597], [168024.4240491967, 6811467.551301144], [168059.70967741936, 6811663.548387097], [168036.46786122074, 6811744.73555327], [168048.9334216628, 6811997.866843326], [168105.70533241477, 6812299.294667585], [168046.04465184055, 6812358.955348159], [168134.59571527297, 6812469.595715273], [168120.12841631015, 6812599.02955548], [168305.32710050038, 6812698.043566311], [168440.4606585253, 6812940.626835678], [168455.73499161843, 6812932.546442499], [168521.4814814815, 6813123.518518519], [168216.6542235406, 6813511.0663074], [168345.10455040447, 6813676.958179838], [168406.41025641025, 6813652.435897436], [168454.5945945946, 6813719.594594595], [168498.42973970083, 6813714.518389612], [168563.73029364986, 6813866.044721538], [168520.9112981316, 6814040.718329472], [168533.679245283, 6814088.867924528], [168417.0148486697, 6814242.79852416], [168314.74741928652, 6814237.949877645], [168117.49437586972, 6814455.506079413], [168229.12993961514, 6814745.610354501], [168439.9903529381, 6814713.798655271], [168566.31719061558, 6814723.66341645], [168707.1066854023, 6814894.870021078], [168654.27544051432, 6814970.128122582], [168356.7192067656, 6815072.068766756], [168204.62177231206, 6815255.0], [168042.2355061998, 6815234.290297861], [167990.77338129497, 6815283.543165468], [167837.66177546134, 6815323.520864536], [167901.0237011057, 6815437.318948756], [167523.23344481905, 6815822.238529126], [167451.0630340981, 6815801.026141856], [167306.06762680024, 6815847.0788979335], [167190.0, 6815705.0], [167044.6100144439, 6815835.117958594], [167237.1842529126, 6816083.455123402], [167209.17775182932, 6816131.461973824], [167355.66460390794, 6816329.335396092], [167329.44444444444, 6816355.555555556], [167387.96205630354, 6816616.499388005], [167497.21773979685, 6816778.822526856], [167451.0272743894, 6816847.633378786], [167549.15078972108, 6817139.476878677], [167243.98866247002, 6817199.603574305], [167174.06875028307, 6817294.2011025585], [166996.3145335883, 6817359.908163894], [166971.0, 6817472.0], [166884.0909090909, 6817515.454545454], [166775.05378511976, 6817506.144659716], [166566.31215009288, 6817598.261576196], [166549.1625748903, 6817751.680748954], [166492.55927025742, 6817753.93929139], [166233.4210483845, 6818133.2116055], [166115.3551138529, 6818471.641239512], [166302.33333333334, 6818686.666666667], [166341.41249586048, 6818833.392177055], [166734.65753424657, 6819245.0], [166785.99877184516, 6819232.320050565], [167011.5880375926, 6819333.411962408], [166880.89496928419, 6819542.104949057], [166924.33247057255, 6819628.391934455], [167058.63821719208, 6819722.779751538], [167131.96713615023, 6819871.615023474], [167098.4764497793, 6819919.100137919], [167161.05042016806, 6820079.075630252], [167146.6634105652, 6820200.538060977], [167282.5, 6820387.5], [167248.75347801892, 6820436.769616026], [167256.65750346836, 6820618.65411646], [167206.68033090775, 6820686.246072778], [167267.62458181655, 6820786.998011628], [167215.5, 6820851.833333333], [167259.5918349431, 6820946.2756069675], [167216.78021978022, 6821043.472527472], [167354.9859947679, 6821224.385468366], [167282.85714285713, 6821420.238095238], [167235.0, 6821430.0], [167265.53796103233, 6821483.618292585], [166947.04765552984, 6821784.847523488], [166720.31987415714, 6821775.924246227], [166235.69674469688, 6822241.87931986], [165934.52915906822, 6822079.799817318], [165911.88442211057, 6821905.326633166], [165679.2638544093, 6822102.583799018], [165423.29185031544, 6822158.237403855], [165223.8708631178, 6822108.915472963], [165124.66666666666, 6822119.666666667], [165028.29268292684, 6822023.292682927], [164829.50572649855, 6822175.708886004], [164815.0, 6822247.133283693], [163998.30829096286, 6823129.58053719], [163792.85178620127, 6823026.627806563], [163805.71543846332, 6823001.367150615], [163419.6545146307, 6822556.426685418], [163341.53802613975, 6822599.542039724], [163180.67637670063, 6822569.993789102], [163078.72477212505, 6822603.932886762], [162872.4742136763, 6822509.121809682], [162653.07692307694, 6822468.076923077], [162592.44541484717, 6822515.305676856], [162393.33333333334, 6822501.666666667], [162248.53606027988, 6822609.230355221], [162144.63927978635, 6822637.4066103045], [161895.76822711964, 6822882.543396485], [161826.2174817898, 6823161.207075963], [161645.77714427325, 6823342.256978973], [161562.80898876404, 6823267.921348315], [161490.0, 6823285.0], [161266.79030192975, 6822970.667908133], [160971.81136296896, 6822883.205454997], [160959.26763371166, 6822774.73558198], [160806.80737806426, 6822591.108377964], [160412.43243243243, 6822545.810810811], [160232.1669561144, 6822688.572761424], [160162.72727272726, 6822677.337662337], [160116.66826003825, 6822705.004780115], [160145.0, 6822760.0], [159971.1759431634, 6822771.255124691], [159841.0655737705, 6822924.918032787], [159771.92534466315, 6822925.010838464], [159635.0, 6823080.0], [159650.36697247706, 6823161.758409786], [159531.98256241117, 6823235.420569002], [159420.0, 6823155.0], [159301.30434782608, 6823135.217391305], [159260.0, 6823205.0], [159084.67146380994, 6823195.956785547], [158756.73403076752, 6823385.953928209], [158718.37016574584, 6823475.46961326], [158601.9285083186, 6823496.844012202], [158573.4, 6823588.0], [158618.70986920333, 6823633.745541022], [158516.53957800812, 6823634.453374332], [158514.84711211777, 6823750.481313704], [158377.36809690003, 6823777.6319031], [158296.42857142858, 6823858.571428572], [158336.72951870546, 6824128.23415214], [158552.46797385416, 6824354.995921588], [158546.4730999146, 6824410.388556789], [158631.45851142038, 6824504.988622307], [158627.8440081232, 6824573.971657521], [158751.9642857143, 6824783.035714285], [158635.0, 6824900.0], [158610.74017743874, 6825026.858048174], [158528.06397840928, 6825124.598348944], [158584.43570265872, 6825242.722011213], [158517.10595515018, 6825372.10595515], [158679.34782608695, 6825534.347826087], [158626.98421610988, 6825650.749890815], [158247.49005137023, 6826057.537393148], [157917.51924390567, 6826093.847710612], [157786.67985941586, 6826008.639392729], [157378.7747430343, 6826299.240397112], [157280.0, 6826305.0], [156995.2896710623, 6826597.929300996], [156855.0, 6826887.580645162], [156855.0, 6826987.142857143], [156781.4229437834, 6827014.580683301], [156694.18284996282, 6826960.430362051], [156580.0, 6827035.0], [156427.22619874487, 6826978.528233891], [156322.20362574054, 6826978.105847667], [155696.28811047922, 6826444.645677502], [155698.97111913358, 6826325.956678701], [155648.69638047065, 6826220.796272789], [155678.87947168696, 6826174.505945391], [155347.17741730215, 6825877.605460952], [155324.0, 6825899.0], [155239.92119495236, 6825793.362526991], [155329.7510011255, 6825692.161659015], [155235.0, 6825520.0], [155273.1181127542, 6825469.420196538], [155244.836861259, 6825398.880280872], [155275.0, 6825185.998955788], [155144.2457153856, 6825030.026256759], [155146.80368488174, 6824888.237334569], [154966.50743680075, 6824752.349697339], [154940.0, 6824675.0], [154672.96470588236, 6824566.05882353], [154562.5179401709, 6824615.016627856], [154475.2427402175, 6824558.400979183], [154061.17947062736, 6824952.682466841], [153948.3932853717, 6824889.604316547], [153910.0, 6824925.0], [153633.8414655395, 6824675.296095573], [153471.88416499528, 6824573.500874762], [153426.8792042776, 6824646.075720117], [153309.49074264755, 6824577.633097253], [153247.0679408379, 6824447.49916059], [153120.6811731315, 6824334.545884579], [152990.0, 6824405.0], [152808.32406471862, 6824259.991821811], [152677.78021868292, 6824315.803335164], [152626.22609219502, 6824579.0548049705], [152384.7992433376, 6824779.837076458], [152278.09075573212, 6824846.237759655], [152213.0, 6824712.0], [152161.25, 6824706.25], [152091.3911917246, 6824766.539108512], [151782.18418519443, 6824750.416064381], [151575.3332200404, 6824670.751515386], [151482.09492582743, 6824535.961711694], [151237.8509637461, 6824588.767535829], [151048.6855839793, 6824570.609580332], [150772.67070690743, 6824635.934637659], [150647.1180714055, 6824694.774777073], [150517.2738001314, 6824882.720017374], [150490.57487511373, 6824992.36659076], [150368.1541865854, 6825058.422906707], [150143.10841580582, 6825308.745261659], [150059.24676187313, 6825284.784789107], [149914.90366404055, 6825339.163638309], [149866.64025356577, 6825380.546751189], [149803.0982650025, 6825359.3660883345], [149713.27636316314, 6825441.684162582], [149593.7657585323, 6825331.10442416], [149547.14285714287, 6825340.714285715], [149490.0, 6825295.0], [149274.90679094542, 6825546.364846871], [149305.0, 6825590.0], [149096.13636363635, 6825798.863636363], [149137.1672067933, 6825876.02289043], [148925.0, 6826050.0], [148928.31606217616, 6826129.481865285], [148692.09151447949, 6826397.939703955], [148622.70970824885, 6826338.784241047], [148560.55555555556, 6826383.333333333], [148590.88585017837, 6826421.236623067], [148546.1879647242, 6826482.32734796], [148627.16378508764, 6826627.871523432], [148499.26859107963, 6826755.395468691], [148682.25090291057, 6826952.749097089], [148666.00580270792, 6827060.038684719], [148705.0, 6827133.333333333], [148653.18181818182, 6827111.818181818], [148523.04537521815, 6827177.068062827], [148547.29508196723, 6827213.442622951], [148456.21325244984, 6827278.492767149], [148465.0, 6827340.0], [148325.0, 6827520.0], [148315.0, 6827586.0], [148135.7594936709, 6827449.24050633], [148000.12285927028, 6827414.87714073], [147905.08593415626, 6827465.951652011], [147724.3533032997, 6827485.0], [147546.80814456358, 6827709.264580327], [147618.99196179604, 6827783.092181444], [147447.1647981899, 6827912.16479819], [147353.0386189563, 6827864.118452933], [147237.3367831924, 6827982.47275187], [147137.62411810685, 6828023.250587929], [147043.8427676554, 6827940.959470064], [146646.2475308213, 6828237.569703922], [146510.6303528427, 6828282.464883227], [146480.02232370205, 6828326.315199159], [146243.46153846153, 6828433.269230769], [146166.36363636365, 6828385.909090909], [145892.8579279267, 6828449.293074313], [145698.8515359408, 6828273.418585753], [145603.37953091683, 6828313.336886994], [145625.0, 6828380.0], [145239.0185801349, 6828819.417609053], [145283.04689674792, 6828925.031260869], [145148.23529411765, 6829058.529411765], [145163.20202680613, 6829149.718862373], [144653.03129221033, 6829692.410361127], [144531.66666666666, 6829883.333333333], [144568.93858913428, 6829988.851458391], [144377.39666944387, 6830062.1077173175], [143979.65746991846, 6830084.060252395], [143870.0, 6830045.0], [143733.7129986326, 6830163.906018191], [143487.2764752713, 6830596.022720626], [143333.0561105112, 6830726.501611498], [143216.97162517396, 6830920.114947552], [142763.93285686092, 6831292.534366456], [142450.3096744802, 6831305.16582353], [142317.48989381708, 6831369.199303599], [142189.6591747816, 6831304.743607308], [142082.6455026455, 6831372.883597883], [142014.54293797343, 6831352.083016457], [141930.4563679374, 6831434.037702831], [141864.5673076923, 6831389.567307692], [141799.14123013322, 6831405.247070234], [141787.48680984325, 6831323.967080333], [141636.66666666666, 6831181.666666667], [141475.60008149847, 6831197.302349077], [141233.0548628429, 6830976.832917706], [141509.8184635631, 6830604.366053854], [141495.33898305084, 6830560.677966102], [141564.46000643945, 6830509.770019778], [141545.77362714784, 6830458.905365103], [141632.36842105264, 6830352.631578947], [141545.0, 6830250.0], [141623.8377763633, 6830200.622274657], [141686.23654181996, 6829929.228708523], [141649.55835962144, 6829859.681913775], [141715.0, 6829800.0], [141666.44749290444, 6829677.47398297], [141721.66666666666, 6829581.111111111], [141709.79804479572, 6829513.855587176], [141562.36967840735, 6829412.856355283], [141454.0950811691, 6829492.618725208], [141175.23841093152, 6829271.356306608], [141034.58521433573, 6829410.063663613], [140909.11902109734, 6829406.78201276], [140730.0, 6829515.0], [140600.0, 6829435.0], [140618.06886056298, 6829398.782824378], [140532.1081036239, 6829317.67283945], [140470.0, 6829355.0], [140373.125, 6829268.194444444], [140226.71247727206, 6829229.2699248735], [140045.26763457104, 6829090.899232198], [139907.9834137516, 6829228.536762], [139848.93116912674, 6829387.837838343], [139537.18753273966, 6829777.593713987], [139457.36522433392, 6830009.841604134], [139471.75458260515, 6830190.407476903], [139361.69032642775, 6830442.3604914965], [139385.76923076922, 6830663.846153846], [139297.1158099488, 6830979.101254613], [138957.26103517492, 6831621.836906897], [138742.14602379626, 6831882.475498062], [138744.0186756718, 6831907.097402352], [138398.62650113303, 6832267.215780782], [138303.2242100864, 6832205.139652085], [138029.34806321948, 6832279.209163358], [137863.5294117647, 6832232.647058823], [137825.43219565472, 6832281.692015447], [137640.0, 6832215.0]]]}}]} \ No newline at end of file diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/analysis.txt b/docs/benchmarks/2026-09-29/bygdin/logs/analysis.txt new file mode 100644 index 00000000..ae3d06c2 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/analysis.txt @@ -0,0 +1,48 @@ +## catchment +run 1: windows [('1', '0.62'), ('2', '1.22'), ('3', '2.84')], flood sum 4.68, trace 0.13, reduce 0.04, wall 6.71, maxRSS 1069 MB, peak footprint 923 MB +run 2: windows [('1', '0.61'), ('2', '1.22'), ('3', '2.84')], flood sum 4.67, trace 0.14, reduce 0.04, wall 6.48, maxRSS 989 MB, peak footprint 920 MB +run 3: windows [('1', '0.61'), ('2', '1.22'), ('3', '2.89')], flood sum 4.72, trace 0.11, reduce 0.04, wall 6.53, maxRSS 982 MB, peak footprint 861 MB +median: flood 4.68 s, trace 0.13 s, reduce 0.04 s, wall 6.53 s, other (read, seed, write, start-up) 1.68 s, maxRSS 989 MB, peak footprint 920 MB + +NVE polygon: 305.5393 km2 by shapely, 1376 coords, valid True; published 305.54 km2 + +| tolerance | vertices | area km2 | vs fine m2 | vs NVE 305.54 | Hausdorff densify=0.05 | 1 m: fine->red, red->fine | valid | +| 0 | 5513 | 304.909550 | +0 | -0.206 % | 0.00 m | 0.00, 0.00 m | True | +| 10 | 1382 | 304.909550 | +0 | -0.206 % | 9.99 m | 9.99, 9.96 m | True | +| 20 | 740 | 304.909550 | -2.98e-07 | -0.206 % | 19.92 m | 19.92, 19.62 m | True | + properties: {'seed': [8.5425, 61.3512], 'seed_crs': 'EPSG:4326', 'nodes': 3049095, 'fine_vertices': 17812, 'fine_area_m2': 304909550.0, 'reduced_vertices': 740, 'reduced_area_m2': 304909549.9999997, 'outline_tolerance_m': 20.0, 'windows': [[1438, 3024], [2236, 3646], [3520, 4813]]} +| 50 | 295 | 304.909550 | -1.43e-06 | -0.206 % | 49.85 m | 49.85, 48.06 m | True | + +fine outline vs NVE boundary, 1 m samples: fine->NVE 485 m, NVE->fine 782 m + +## node overlap with NVE (DEM nodes at multiples of 10 m) +ours 3049096 nodes (catchment reports 3049095), NVE 3055360 nodes, both 3028611 +of NVE's nodes in ours: 99.12 %; of ours in NVE's: 99.33 % +ours only 20485 nodes (2.05 km2), NVE only 26749 nodes (2.67 km2) +largest ours-not-NVE parts: [(0.255, (163743, 6813036)), (0.134, (156227, 6826776)), (0.086, (148176, 6827409))] +largest NVE-not-ours parts: [(0.577, (149046, 6826265)), (0.439, (158666, 6826076)), (0.242, (167571, 6815768))] + +## meshes (median of 3 binary runs; quality from the ASCII run) +| run | triangles | vertices | start vertices | constraint edges | refine s | total s | wall s | maxRSS MB | worst angle | max degree | max error | within tol | Delaunay checked/ambiguous/violations | +| red_t10 | 52198 | 26576 | 740 | 952 | 0.034 | 0.356 | 0.62 | 646 | 0.3562° | 12 | 9.9998 | True | 77821/1122/0 | + runs (refine, total, wall, maxRSS): [(0.034, 0.356, 0.63, 643219456), (0.034, 0.355, 0.62, 645562368), (0.033, 0.356, 0.62, 647921664)] +| fine_t10 | 72292 | 38914 | 5513 | 5534 | 0.043 | 0.388 | 0.66 | 647 | 0.6548° | 13 | 10.0000 | True | 105671/1546/0 | + runs (refine, total, wall, maxRSS): [(0.043, 0.39, 0.66, 646496256), (0.043, 0.388, 0.66, 648019968), (0.042, 0.388, 0.66, 647200768)] +| red_t1 | 1129026 | 565511 | 740 | 1994 | 0.497 | 0.927 | 1.38 | 925 | 0.04612° | 18 | 1.0000 | True | 1692542/172872/0 | + runs (refine, total, wall, maxRSS): [(0.497, 0.933, 1.39, 923910144), (0.496, 0.927, 1.38, 925401088), (0.504, 0.915, 1.37, 929644544)] +| fine_t1 | 1137703 | 571651 | 5513 | 5597 | 0.483 | 0.917 | 1.38 | 942 | 0.08338° | 16 | 1.0000 | True | 1703756/172995/0 | + runs (refine, total, wall, maxRSS): [(0.481, 0.902, 1.36, 942112768), (0.483, 0.931, 1.39, 942194688), (0.486, 0.917, 1.38, 941096960)] +| feat_t10 | 83171 | 42110 | 8242 | 8882 | 0.046 | 1.972 | 2.24 | 776 | 0.004079° | 16 | 9.9997 | True | 116398/1095/0 | + runs (refine, total, wall, maxRSS): [(0.046, 2.031, 2.3, 777306112), (0.047, 1.972, 2.24, 774995968), (0.046, 1.966, 2.23, 776355840)] + +## land cover inside the reduced catchment (CORINE 2018, clipped by shapely) +187 candidate rows; covered 304.910 km2 of 304.910 +| 333 | 129.093 km2 | 42.34 % | +| 332 | 65.012 km2 | 21.32 % | +| 322 | 51.849 km2 | 17.00 % | +| 512 | 50.191 km2 | 16.46 % | +| 335 | 7.452 km2 | 2.44 % | +| 412 | 1.021 km2 | 0.33 % | +| 142 | 0.292 km2 | 0.10 % | +mesh cells with land_cover=1: 7835 (of 92053 cells: 8882 lines + 83171 triangles) +mesh cells with water=1: 2005 (of 92053 cells: 8882 lines + 83171 triangles) diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t0.err b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t0.err new file mode 100644 index 00000000..716f1fb9 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t0.err @@ -0,0 +1,25 @@ +window 1: x 140240-170470, y 6811200-6825570, 1438 x 3024 nodes, flood 0.61 s, grown on north, west, south, east +window 2: x 136250-172700, y 6807210-6829560, 2236 x 3646 nodes, flood 1.22 s, grown on north, west +window 3: x 128580-176700, y 6802360-6837550, 3520 x 4813 nodes, flood 2.84 s, contained +seed: lake of 39.936963 km2, 399371 nodes +catchment: 3049095 nodes, 304.909500 km2 of node area +fine outline: 17812 vertices, 304.909550 km2, 0 rings dropped (0 nodes), 1 holes filled (0.000050 km2), traced in 0.12 s +reduced outline: 5513 vertices, 304.909550 km2, difference 0 m2 (0 relative), tolerance 0 m, 0.00 s + 6.54 real 7.44 user 0.36 sys + 1003683840 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 96789 page reclaims + 109 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 0 voluntary context switches + 1394 involuntary context switches + 68121011463 instructions retired + 24830697181 cycles elapsed + 913196232 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t0.out b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t0.out new file mode 100644 index 00000000..837b602e --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t0.out @@ -0,0 +1 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t0.geojson diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t10.err b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t10.err new file mode 100644 index 00000000..2fec51ba --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t10.err @@ -0,0 +1,25 @@ +window 1: x 140240-170470, y 6811200-6825570, 1438 x 3024 nodes, flood 0.61 s, grown on north, west, south, east +window 2: x 136250-172700, y 6807210-6829560, 2236 x 3646 nodes, flood 1.22 s, grown on north, west +window 3: x 128580-176700, y 6802360-6837550, 3520 x 4813 nodes, flood 2.86 s, contained +seed: lake of 39.936963 km2, 399371 nodes +catchment: 3049095 nodes, 304.909500 km2 of node area +fine outline: 17812 vertices, 304.909550 km2, 0 rings dropped (0 nodes), 1 holes filled (0.000050 km2), traced in 0.12 s +reduced outline: 1382 vertices, 304.909550 km2, difference 0 m2 (0 relative), tolerance 10 m, 0.04 s + 6.48 real 7.41 user 0.36 sys + 982958080 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 86367 page reclaims + 99 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 0 voluntary context switches + 890 involuntary context switches + 68320843206 instructions retired + 24823885477 cycles elapsed + 914130120 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t10.out b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t10.out new file mode 100644 index 00000000..3d07a9d8 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t10.out @@ -0,0 +1 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t10.geojson diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20.err b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20.err new file mode 100644 index 00000000..f6d3aa2a --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20.err @@ -0,0 +1,25 @@ +window 1: x 140240-170470, y 6811200-6825570, 1438 x 3024 nodes, flood 0.62 s, grown on north, west, south, east +window 2: x 136250-172700, y 6807210-6829560, 2236 x 3646 nodes, flood 1.23 s, grown on north, west +window 3: x 128580-176700, y 6802360-6837550, 3520 x 4813 nodes, flood 2.84 s, contained +seed: lake of 39.936963 km2, 399371 nodes +catchment: 3049095 nodes, 304.909500 km2 of node area +fine outline: 17812 vertices, 304.909550 km2, 0 rings dropped (0 nodes), 1 holes filled (0.000050 km2), traced in 0.13 s +reduced outline: 740 vertices, 304.909550 km2, difference -2.98e-07 m2 (-9.8e-16 relative), tolerance 20 m, 0.04 s + 6.51 real 7.44 user 0.34 sys + 983056384 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 95166 page reclaims + 110 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 0 voluntary context switches + 903 involuntary context switches + 68359231685 instructions retired + 24848591477 cycles elapsed + 912409824 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20.out b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20.out new file mode 100644 index 00000000..5450d1d1 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20.out @@ -0,0 +1 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t20.geojson diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run1.err b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run1.err new file mode 100644 index 00000000..2a39c51b --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run1.err @@ -0,0 +1,25 @@ +window 1: x 140240-170470, y 6811200-6825570, 1438 x 3024 nodes, flood 0.62 s, grown on north, west, south, east +window 2: x 136250-172700, y 6807210-6829560, 2236 x 3646 nodes, flood 1.22 s, grown on north, west +window 3: x 128580-176700, y 6802360-6837550, 3520 x 4813 nodes, flood 2.84 s, contained +seed: lake of 39.936963 km2, 399371 nodes +catchment: 3049095 nodes, 304.909500 km2 of node area +fine outline: 17812 vertices, 304.909550 km2, 0 rings dropped (0 nodes), 1 holes filled (0.000050 km2), traced in 0.13 s +reduced outline: 740 vertices, 304.909550 km2, difference -2.98e-07 m2 (-9.8e-16 relative), tolerance 20 m, 0.04 s + 6.71 real 7.52 user 0.38 sys + 1069252608 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 100178 page reclaims + 153 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 659 voluntary context switches + 1464 involuntary context switches + 68481504221 instructions retired + 25139539390 cycles elapsed + 922502416 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run1.out b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run1.out new file mode 100644 index 00000000..c79cf167 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run1.out @@ -0,0 +1 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t20_run1.geojson diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run2.err b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run2.err new file mode 100644 index 00000000..62f5838a --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run2.err @@ -0,0 +1,25 @@ +window 1: x 140240-170470, y 6811200-6825570, 1438 x 3024 nodes, flood 0.61 s, grown on north, west, south, east +window 2: x 136250-172700, y 6807210-6829560, 2236 x 3646 nodes, flood 1.22 s, grown on north, west +window 3: x 128580-176700, y 6802360-6837550, 3520 x 4813 nodes, flood 2.84 s, contained +seed: lake of 39.936963 km2, 399371 nodes +catchment: 3049095 nodes, 304.909500 km2 of node area +fine outline: 17812 vertices, 304.909550 km2, 0 rings dropped (0 nodes), 1 holes filled (0.000050 km2), traced in 0.14 s +reduced outline: 740 vertices, 304.909550 km2, difference -2.98e-07 m2 (-9.8e-16 relative), tolerance 20 m, 0.04 s + 6.48 real 7.44 user 0.30 sys + 988905472 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 92259 page reclaims + 109 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 6 voluntary context switches + 1048 involuntary context switches + 68208970319 instructions retired + 24727903521 cycles elapsed + 920290528 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run2.out b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run2.out new file mode 100644 index 00000000..904554fb --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run2.out @@ -0,0 +1 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t20_run2.geojson diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run3.err b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run3.err new file mode 100644 index 00000000..7cc08914 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run3.err @@ -0,0 +1,25 @@ +window 1: x 140240-170470, y 6811200-6825570, 1438 x 3024 nodes, flood 0.61 s, grown on north, west, south, east +window 2: x 136250-172700, y 6807210-6829560, 2236 x 3646 nodes, flood 1.22 s, grown on north, west +window 3: x 128580-176700, y 6802360-6837550, 3520 x 4813 nodes, flood 2.89 s, contained +seed: lake of 39.936963 km2, 399371 nodes +catchment: 3049095 nodes, 304.909500 km2 of node area +fine outline: 17812 vertices, 304.909550 km2, 0 rings dropped (0 nodes), 1 holes filled (0.000050 km2), traced in 0.11 s +reduced outline: 740 vertices, 304.909550 km2, difference -2.98e-07 m2 (-9.8e-16 relative), tolerance 20 m, 0.04 s + 6.53 real 7.48 user 0.31 sys + 982482944 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 97529 page reclaims + 109 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 0 voluntary context switches + 942 involuntary context switches + 68267131103 instructions retired + 24858136046 cycles elapsed + 860800248 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run3.out b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run3.out new file mode 100644 index 00000000..6451577b --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t20_run3.out @@ -0,0 +1 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t20_run3.geojson diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t50.err b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t50.err new file mode 100644 index 00000000..f6a65ba1 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t50.err @@ -0,0 +1,25 @@ +window 1: x 140240-170470, y 6811200-6825570, 1438 x 3024 nodes, flood 0.64 s, grown on north, west, south, east +window 2: x 136250-172700, y 6807210-6829560, 2236 x 3646 nodes, flood 1.22 s, grown on north, west +window 3: x 128580-176700, y 6802360-6837550, 3520 x 4813 nodes, flood 2.83 s, contained +seed: lake of 39.936963 km2, 399371 nodes +catchment: 3049095 nodes, 304.909500 km2 of node area +fine outline: 17812 vertices, 304.909550 km2, 0 rings dropped (0 nodes), 1 holes filled (0.000050 km2), traced in 0.13 s +reduced outline: 295 vertices, 304.909550 km2, difference -1.43e-06 m2 (-4.7e-15 relative), tolerance 50 m, 0.04 s + 6.54 real 7.46 user 0.34 sys + 985776128 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 92107 page reclaims + 110 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 0 voluntary context switches + 1134 involuntary context switches + 68289847475 instructions retired + 24862349973 cycles elapsed + 917144800 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t50.out b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t50.out new file mode 100644 index 00000000..af39abb7 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/catchment_t50.out @@ -0,0 +1 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t50.geojson diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/commit.txt b/docs/benchmarks/2026-09-29/bygdin/logs/commit.txt new file mode 100644 index 00000000..f3f54b56 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/commit.txt @@ -0,0 +1,2 @@ +675792531373ec1f4297d2fd59281a4745376f00 +b6f5b91f43116a4ce978aeb2b27845766cf200ee7239eabb5bcfeb3d74be6ca0 .venv/lib/python3.14/site-packages/tin_engine/_core.cpython-314-darwin.so diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/exits.txt b/docs/benchmarks/2026-09-29/bygdin/logs/exits.txt new file mode 100644 index 00000000..fb84ac05 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/exits.txt @@ -0,0 +1,27 @@ +catchment_t20_run1 exit=0 +catchment_t20_run2 exit=0 +catchment_t20_run3 exit=0 +catchment_t0 exit=0 +catchment_t10 exit=0 +catchment_t20 exit=0 +catchment_t50 exit=0 +mesh_red_t10_run1 exit=0 +mesh_red_t1_run1 exit=0 +mesh_fine_t10_run1 exit=0 +mesh_fine_t1_run1 exit=0 +mesh_feat_t10_run1 exit=0 +mesh_red_t10_run2 exit=0 +mesh_red_t1_run2 exit=0 +mesh_fine_t10_run2 exit=0 +mesh_fine_t1_run2 exit=0 +mesh_feat_t10_run2 exit=0 +mesh_red_t10_run3 exit=0 +mesh_red_t1_run3 exit=0 +mesh_fine_t10_run3 exit=0 +mesh_fine_t1_run3 exit=0 +mesh_feat_t10_run3 exit=0 +mesh_red_t10_ascii exit=0 +mesh_red_t1_ascii exit=0 +mesh_fine_t10_ascii exit=0 +mesh_fine_t1_ascii exit=0 +mesh_feat_t10_ascii exit=0 diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_ascii.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_ascii.err new file mode 100644 index 00000000..7482a0b7 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_ascii.err @@ -0,0 +1,22 @@ +79 features kept, 342 dropped outside, 35 clipped, 0 empty skipped +15846 input vertices, 8242 noded vertices +15989 start quality nodes inserted, 2442 start quality skips, 534 constraint feet, 0 feet refused, 17 rounds, 17879 points inserted, 39582 flips, 83171 triangles, achieved max error 9.999732907715497 m, 0 valid DEM nodes not covered, 15614 start triangles, 8242 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 2.43 real 2.75 user 0.21 sys + 773390336 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 65759 page reclaims + 44 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 5 voluntary context switches + 572 involuntary context switches + 42096546004 instructions retired + 9407019502 cycles elapsed + 750945312 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_ascii.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_ascii.out new file mode 100644 index 00000000..eadeb36b --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_ascii.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_feat_t10_ascii.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_ascii.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_ascii.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_ascii.stats.md new file mode 100644 index 00000000..8e3f5f70 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_ascii.stats.md @@ -0,0 +1,66 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t20_run1.geojson --tolerance 10 --features ../rasputin_data/corine2018_dtm10_utm33.gpkg --features-layer corine2018 --features-map corine --ascii --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_feat_t10_ascii.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_ascii.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 740 (1 ring, 0 holes) | +| start vertices | 8242 | +| start triangles | 15614 | +| output vertices | 42110 | +| output triangles | 83171 | +| constraint edges | 8882 | +| vertices without data dropped | 0 | +| mesh_feat_t10_ascii.vtk | 4.7 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 36.87° | 0.07 % | 1.70 % | 0.00408° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 16 | 31 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 10 m | 9.999732907715497 m | 17 | 17879 | 0 | 39582 | 0 | 15989 | 2442 | 534 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.003 | 0.2 % | +| decode | 0.305 | 14.2 % | +| features read | 0.125 | 5.8 % | +| features clip | 1.430 | 66.4 % | +| start mesh: build | 0.000 | 0.0 % | +| start mesh: node | 0.017 | 0.8 % | +| start mesh: triangulate | 0.005 | 0.2 % | +| start mesh: constraint edges | 0.016 | 0.7 % | +| refine | 0.046 | 2.1 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.015 | 0.7 % | +| refine: scan (parallel) | 0.013 | 0.6 % | +| refine: split + flip (serial) | 0.009 | 0.4 % | +| refine: setup + output | 0.009 | 0.4 % | +| trim | 0.003 | 0.1 % | +| write: encode | 0.194 | 9.0 % | +| write: disk | 0.006 | 0.3 % | +| other | 0.005 | 0.2 % | +| **total** | **2.156** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.016 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run1.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run1.err new file mode 100644 index 00000000..61b2aaa3 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run1.err @@ -0,0 +1,22 @@ +79 features kept, 342 dropped outside, 35 clipped, 0 empty skipped +15846 input vertices, 8242 noded vertices +15989 start quality nodes inserted, 2442 start quality skips, 534 constraint feet, 0 feet refused, 17 rounds, 17879 points inserted, 39582 flips, 83171 triangles, achieved max error 9.999732907715497 m, 0 valid DEM nodes not covered, 15614 start triangles, 8242 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 2.30 real 2.55 user 0.22 sys + 777306112 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 65508 page reclaims + 57 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 832 voluntary context switches + 883 involuntary context switches + 39082214782 instructions retired + 8809983601 cycles elapsed + 754861088 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run1.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run1.out new file mode 100644 index 00000000..0b2f6070 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run1.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_feat_t10_run1.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run1.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run1.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run1.stats.md new file mode 100644 index 00000000..d6a3cae9 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run1.stats.md @@ -0,0 +1,66 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t20_run1.geojson --tolerance 10 --features ../rasputin_data/corine2018_dtm10_utm33.gpkg --features-layer corine2018 --features-map corine --binary --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_feat_t10_run1.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run1.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 740 (1 ring, 0 holes) | +| start vertices | 8242 | +| start triangles | 15614 | +| output vertices | 42110 | +| output triangles | 83171 | +| constraint edges | 8882 | +| vertices without data dropped | 0 | +| mesh_feat_t10_run1.vtk | 3.3 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 36.87° | 0.07 % | 1.70 % | 0.00408° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 16 | 31 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 10 m | 9.999732907715497 m | 17 | 17879 | 0 | 39582 | 0 | 15989 | 2442 | 534 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.003 | 0.2 % | +| decode | 0.316 | 15.6 % | +| features read | 0.206 | 10.1 % | +| features clip | 1.405 | 69.2 % | +| start mesh: build | 0.000 | 0.0 % | +| start mesh: node | 0.017 | 0.9 % | +| start mesh: triangulate | 0.005 | 0.3 % | +| start mesh: constraint edges | 0.016 | 0.8 % | +| refine | 0.046 | 2.3 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.015 | 0.7 % | +| refine: scan (parallel) | 0.013 | 0.6 % | +| refine: split + flip (serial) | 0.009 | 0.4 % | +| refine: setup + output | 0.009 | 0.4 % | +| trim | 0.003 | 0.1 % | +| write: encode | 0.007 | 0.3 % | +| write: disk | 0.001 | 0.1 % | +| other | 0.004 | 0.2 % | +| **total** | **2.031** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.015 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run2.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run2.err new file mode 100644 index 00000000..9efc60a9 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run2.err @@ -0,0 +1,22 @@ +79 features kept, 342 dropped outside, 35 clipped, 0 empty skipped +15846 input vertices, 8242 noded vertices +15989 start quality nodes inserted, 2442 start quality skips, 534 constraint feet, 0 feet refused, 17 rounds, 17879 points inserted, 39582 flips, 83171 triangles, achieved max error 9.999732907715497 m, 0 valid DEM nodes not covered, 15614 start triangles, 8242 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 2.24 real 2.56 user 0.21 sys + 774995968 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 66039 page reclaims + 44 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 16 voluntary context switches + 583 involuntary context switches + 39002546617 instructions retired + 8839202639 cycles elapsed + 752550944 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run2.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run2.out new file mode 100644 index 00000000..78bf8056 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run2.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_feat_t10_run2.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run2.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run2.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run2.stats.md new file mode 100644 index 00000000..6db15918 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run2.stats.md @@ -0,0 +1,66 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t20_run1.geojson --tolerance 10 --features ../rasputin_data/corine2018_dtm10_utm33.gpkg --features-layer corine2018 --features-map corine --binary --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_feat_t10_run2.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run2.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 740 (1 ring, 0 holes) | +| start vertices | 8242 | +| start triangles | 15614 | +| output vertices | 42110 | +| output triangles | 83171 | +| constraint edges | 8882 | +| vertices without data dropped | 0 | +| mesh_feat_t10_run2.vtk | 3.3 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 36.87° | 0.07 % | 1.70 % | 0.00408° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 16 | 31 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 10 m | 9.999732907715497 m | 17 | 17879 | 0 | 39582 | 0 | 15989 | 2442 | 534 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.003 | 0.2 % | +| decode | 0.308 | 15.6 % | +| features read | 0.131 | 6.6 % | +| features clip | 1.432 | 72.6 % | +| start mesh: build | 0.000 | 0.0 % | +| start mesh: node | 0.017 | 0.9 % | +| start mesh: triangulate | 0.005 | 0.3 % | +| start mesh: constraint edges | 0.016 | 0.8 % | +| refine | 0.047 | 2.4 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.015 | 0.8 % | +| refine: scan (parallel) | 0.013 | 0.7 % | +| refine: split + flip (serial) | 0.010 | 0.5 % | +| refine: setup + output | 0.009 | 0.4 % | +| trim | 0.003 | 0.2 % | +| write: encode | 0.005 | 0.3 % | +| write: disk | 0.001 | 0.0 % | +| other | 0.004 | 0.2 % | +| **total** | **1.972** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.015 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run3.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run3.err new file mode 100644 index 00000000..da3a2ee0 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run3.err @@ -0,0 +1,22 @@ +79 features kept, 342 dropped outside, 35 clipped, 0 empty skipped +15846 input vertices, 8242 noded vertices +15989 start quality nodes inserted, 2442 start quality skips, 534 constraint feet, 0 feet refused, 17 rounds, 17879 points inserted, 39582 flips, 83171 triangles, achieved max error 9.999732907715497 m, 0 valid DEM nodes not covered, 15614 start triangles, 8242 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 2.23 real 2.56 user 0.21 sys + 776355840 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 67674 page reclaims + 44 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 0 voluntary context switches + 564 involuntary context switches + 39056894174 instructions retired + 8846481892 cycles elapsed + 753927224 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run3.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run3.out new file mode 100644 index 00000000..d2dbbacc --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run3.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_feat_t10_run3.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run3.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run3.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run3.stats.md new file mode 100644 index 00000000..3315bac0 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run3.stats.md @@ -0,0 +1,66 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t20_run1.geojson --tolerance 10 --features ../rasputin_data/corine2018_dtm10_utm33.gpkg --features-layer corine2018 --features-map corine --binary --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_feat_t10_run3.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_feat_t10_run3.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 740 (1 ring, 0 holes) | +| start vertices | 8242 | +| start triangles | 15614 | +| output vertices | 42110 | +| output triangles | 83171 | +| constraint edges | 8882 | +| vertices without data dropped | 0 | +| mesh_feat_t10_run3.vtk | 3.3 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 36.87° | 0.07 % | 1.70 % | 0.00408° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 16 | 31 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 10 m | 9.999732907715497 m | 17 | 17879 | 0 | 39582 | 0 | 15989 | 2442 | 534 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.003 | 0.2 % | +| decode | 0.312 | 15.9 % | +| features read | 0.125 | 6.4 % | +| features clip | 1.427 | 72.6 % | +| start mesh: build | 0.000 | 0.0 % | +| start mesh: node | 0.017 | 0.9 % | +| start mesh: triangulate | 0.005 | 0.3 % | +| start mesh: constraint edges | 0.016 | 0.8 % | +| refine | 0.046 | 2.4 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.015 | 0.8 % | +| refine: scan (parallel) | 0.013 | 0.7 % | +| refine: split + flip (serial) | 0.009 | 0.5 % | +| refine: setup + output | 0.008 | 0.4 % | +| trim | 0.003 | 0.2 % | +| write: encode | 0.005 | 0.3 % | +| write: disk | 0.001 | 0.0 % | +| other | 0.004 | 0.2 % | +| **total** | **1.966** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.015 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_ascii.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_ascii.err new file mode 100644 index 00000000..5f481d65 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_ascii.err @@ -0,0 +1,20 @@ +10789 start quality nodes inserted, 1841 start quality skips, 21 constraint feet, 0 feet refused, 22 rounds, 22612 points inserted, 54390 flips, 72292 triangles, achieved max error 9.99996099528471 m, 0 valid DEM nodes not covered, 5511 start triangles, 5513 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 0.78 real 1.20 user 0.14 sys + 662913024 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 48750 page reclaims + 109 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 4 voluntary context switches + 645 involuntary context switches + 16629498139 instructions retired + 4178781410 cycles elapsed + 639992768 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_ascii.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_ascii.out new file mode 100644 index 00000000..29b4e3ca --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_ascii.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_fine_t10_ascii.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_ascii.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_ascii.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_ascii.stats.md new file mode 100644 index 00000000..29187989 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_ascii.stats.md @@ -0,0 +1,64 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t0.geojson --tolerance 10 --ascii --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_fine_t10_ascii.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_ascii.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 5513 (1 ring, 0 holes) | +| start vertices | 5513 | +| start triangles | 5511 | +| output vertices | 38914 | +| output triangles | 72292 | +| constraint edges | 5534 | +| vertices without data dropped | 0 | +| mesh_fine_t10_ascii.vtk | 3.8 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 36.87° | 0.01 % | 1.10 % | 0.655° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 13 | 3 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 10 m | 9.99996099528471 m | 22 | 22612 | 0 | 54390 | 0 | 10789 | 1841 | 21 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.006 | 1.3 % | +| decode | 0.310 | 61.3 % | +| start mesh: build | 0.000 | 0.0 % | +| start mesh: node | 0.006 | 1.2 % | +| start mesh: triangulate | 0.003 | 0.6 % | +| start mesh: constraint edges | 0.007 | 1.4 % | +| refine | 0.043 | 8.5 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.011 | 2.2 % | +| refine: scan (parallel) | 0.018 | 3.5 % | +| refine: split + flip (serial) | 0.009 | 1.9 % | +| refine: setup + output | 0.005 | 0.9 % | +| trim | 0.003 | 0.5 % | +| write: encode | 0.121 | 24.0 % | +| write: disk | 0.003 | 0.5 % | +| other | 0.003 | 0.6 % | +| **total** | **0.505** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.013 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run1.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run1.err new file mode 100644 index 00000000..11f93442 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run1.err @@ -0,0 +1,20 @@ +10789 start quality nodes inserted, 1841 start quality skips, 21 constraint feet, 0 feet refused, 22 rounds, 22612 points inserted, 54390 flips, 72292 triangles, achieved max error 9.99996099528471 m, 0 valid DEM nodes not covered, 5511 start triangles, 5513 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 0.66 real 1.08 user 0.14 sys + 646496256 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 47915 page reclaims + 66 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 4 voluntary context switches + 630 involuntary context switches + 14655662506 instructions retired + 3816662932 cycles elapsed + 623330216 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run1.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run1.out new file mode 100644 index 00000000..bb851e70 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run1.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_fine_t10_run1.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run1.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run1.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run1.stats.md new file mode 100644 index 00000000..2abd0be3 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run1.stats.md @@ -0,0 +1,64 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t0.geojson --tolerance 10 --binary --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_fine_t10_run1.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run1.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 5513 (1 ring, 0 holes) | +| start vertices | 5513 | +| start triangles | 5511 | +| output vertices | 38914 | +| output triangles | 72292 | +| constraint edges | 5534 | +| vertices without data dropped | 0 | +| mesh_fine_t10_run1.vtk | 2.8 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 36.87° | 0.01 % | 1.10 % | 0.655° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 13 | 3 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 10 m | 9.99996099528471 m | 22 | 22612 | 0 | 54390 | 0 | 10789 | 1841 | 21 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.007 | 1.8 % | +| decode | 0.312 | 80.0 % | +| start mesh: build | 0.000 | 0.1 % | +| start mesh: node | 0.006 | 1.6 % | +| start mesh: triangulate | 0.003 | 0.8 % | +| start mesh: constraint edges | 0.007 | 1.8 % | +| refine | 0.043 | 11.0 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.011 | 2.8 % | +| refine: scan (parallel) | 0.018 | 4.5 % | +| refine: split + flip (serial) | 0.009 | 2.3 % | +| refine: setup + output | 0.005 | 1.2 % | +| trim | 0.003 | 0.7 % | +| write: encode | 0.004 | 1.1 % | +| write: disk | 0.002 | 0.5 % | +| other | 0.003 | 0.8 % | +| **total** | **0.390** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.013 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run2.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run2.err new file mode 100644 index 00000000..14e4d9a9 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run2.err @@ -0,0 +1,20 @@ +10789 start quality nodes inserted, 1841 start quality skips, 21 constraint feet, 0 feet refused, 22 rounds, 22612 points inserted, 54390 flips, 72292 triangles, achieved max error 9.99996099528471 m, 0 valid DEM nodes not covered, 5511 start triangles, 5513 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 0.66 real 1.08 user 0.15 sys + 648019968 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 48218 page reclaims + 126 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 1 voluntary context switches + 643 involuntary context switches + 14673501610 instructions retired + 3810240204 cycles elapsed + 624853928 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run2.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run2.out new file mode 100644 index 00000000..7232208d --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run2.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_fine_t10_run2.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run2.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run2.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run2.stats.md new file mode 100644 index 00000000..54b04327 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run2.stats.md @@ -0,0 +1,64 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t0.geojson --tolerance 10 --binary --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_fine_t10_run2.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run2.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 5513 (1 ring, 0 holes) | +| start vertices | 5513 | +| start triangles | 5511 | +| output vertices | 38914 | +| output triangles | 72292 | +| constraint edges | 5534 | +| vertices without data dropped | 0 | +| mesh_fine_t10_run2.vtk | 2.8 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 36.87° | 0.01 % | 1.10 % | 0.655° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 13 | 3 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 10 m | 9.99996099528471 m | 22 | 22612 | 0 | 54390 | 0 | 10789 | 1841 | 21 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.007 | 1.7 % | +| decode | 0.310 | 79.9 % | +| start mesh: build | 0.000 | 0.1 % | +| start mesh: node | 0.006 | 1.6 % | +| start mesh: triangulate | 0.003 | 0.8 % | +| start mesh: constraint edges | 0.007 | 1.8 % | +| refine | 0.043 | 11.1 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.011 | 2.8 % | +| refine: scan (parallel) | 0.018 | 4.6 % | +| refine: split + flip (serial) | 0.009 | 2.4 % | +| refine: setup + output | 0.005 | 1.2 % | +| trim | 0.003 | 0.7 % | +| write: encode | 0.004 | 1.1 % | +| write: disk | 0.001 | 0.1 % | +| other | 0.004 | 1.1 % | +| **total** | **0.388** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.013 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run3.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run3.err new file mode 100644 index 00000000..01dbd7b0 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run3.err @@ -0,0 +1,20 @@ +10789 start quality nodes inserted, 1841 start quality skips, 21 constraint feet, 0 feet refused, 22 rounds, 22612 points inserted, 54390 flips, 72292 triangles, achieved max error 9.99996099528471 m, 0 valid DEM nodes not covered, 5511 start triangles, 5513 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 0.66 real 1.08 user 0.14 sys + 647200768 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 48746 page reclaims + 126 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 0 voluntary context switches + 677 involuntary context switches + 14667763218 instructions retired + 3818928580 cycles elapsed + 624051136 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run3.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run3.out new file mode 100644 index 00000000..eab674a8 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run3.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_fine_t10_run3.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run3.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run3.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run3.stats.md new file mode 100644 index 00000000..d5ebb3b7 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run3.stats.md @@ -0,0 +1,64 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t0.geojson --tolerance 10 --binary --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_fine_t10_run3.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t10_run3.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 5513 (1 ring, 0 holes) | +| start vertices | 5513 | +| start triangles | 5511 | +| output vertices | 38914 | +| output triangles | 72292 | +| constraint edges | 5534 | +| vertices without data dropped | 0 | +| mesh_fine_t10_run3.vtk | 2.8 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 36.87° | 0.01 % | 1.10 % | 0.655° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 13 | 3 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 10 m | 9.99996099528471 m | 22 | 22612 | 0 | 54390 | 0 | 10789 | 1841 | 21 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.006 | 1.6 % | +| decode | 0.313 | 80.5 % | +| start mesh: build | 0.000 | 0.1 % | +| start mesh: node | 0.006 | 1.6 % | +| start mesh: triangulate | 0.003 | 0.8 % | +| start mesh: constraint edges | 0.007 | 1.8 % | +| refine | 0.042 | 10.9 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.011 | 2.8 % | +| refine: scan (parallel) | 0.017 | 4.5 % | +| refine: split + flip (serial) | 0.009 | 2.3 % | +| refine: setup + output | 0.005 | 1.2 % | +| trim | 0.003 | 0.7 % | +| write: encode | 0.004 | 1.1 % | +| write: disk | 0.001 | 0.1 % | +| other | 0.003 | 0.8 % | +| **total** | **0.388** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.013 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_ascii.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_ascii.err new file mode 100644 index 00000000..0a277980 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_ascii.err @@ -0,0 +1,20 @@ +10789 start quality nodes inserted, 1841 start quality skips, 84 constraint feet, 0 feet refused, 33 rounds, 555349 points inserted, 1193738 flips, 1137703 triangles, achieved max error 1 m, 0 valid DEM nodes not covered, 5511 start triangles, 5513 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 3.22 real 4.17 user 0.30 sys + 1163919360 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 111236 page reclaims + 44 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 19 voluntary context switches + 1051 involuntary context switches + 53213996844 instructions retired + 13969867000 cycles elapsed + 1142572400 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_ascii.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_ascii.out new file mode 100644 index 00000000..06a01f5b --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_ascii.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_fine_t1_ascii.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_ascii.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_ascii.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_ascii.stats.md new file mode 100644 index 00000000..b5c900ec --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_ascii.stats.md @@ -0,0 +1,64 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t0.geojson --tolerance 1 --ascii --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_fine_t1_ascii.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_ascii.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 5513 (1 ring, 0 holes) | +| start vertices | 5513 | +| start triangles | 5511 | +| output vertices | 571651 | +| output triangles | 1137703 | +| constraint edges | 5597 | +| vertices without data dropped | 0 | +| mesh_fine_t1_ascii.vtk | 59.2 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 45.00° | 0.01 % | 0.27 % | 0.0834° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 16 | 80 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 1 m | 1 m | 33 | 555349 | 0 | 1193738 | 0 | 10789 | 1841 | 84 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.007 | 0.2 % | +| decode | 0.317 | 11.5 % | +| start mesh: build | 0.000 | 0.0 % | +| start mesh: node | 0.006 | 0.2 % | +| start mesh: triangulate | 0.003 | 0.1 % | +| start mesh: constraint edges | 0.007 | 0.2 % | +| refine | 0.498 | 18.0 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.011 | 0.4 % | +| refine: scan (parallel) | 0.102 | 3.7 % | +| refine: split + flip (serial) | 0.334 | 12.1 % | +| refine: setup + output | 0.051 | 1.8 % | +| trim | 0.050 | 1.8 % | +| write: encode | 1.850 | 66.8 % | +| write: disk | 0.029 | 1.0 % | +| other | 0.003 | 0.1 % | +| **total** | **2.770** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.196 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run1.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run1.err new file mode 100644 index 00000000..bef677d5 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run1.err @@ -0,0 +1,20 @@ +10789 start quality nodes inserted, 1841 start quality skips, 84 constraint feet, 0 feet refused, 33 rounds, 555349 points inserted, 1193738 flips, 1137703 triangles, achieved max error 1 m, 0 valid DEM nodes not covered, 5511 start triangles, 5513 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 1.36 real 2.43 user 0.19 sys + 942112768 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 68222 page reclaims + 44 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 10 voluntary context switches + 921 involuntary context switches + 23941825764 instructions retired + 8019935151 cycles elapsed + 920421576 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run1.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run1.out new file mode 100644 index 00000000..cbf9a9a8 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run1.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_fine_t1_run1.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run1.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run1.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run1.stats.md new file mode 100644 index 00000000..22f25387 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run1.stats.md @@ -0,0 +1,64 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t0.geojson --tolerance 1 --binary --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_fine_t1_run1.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run1.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 5513 (1 ring, 0 holes) | +| start vertices | 5513 | +| start triangles | 5511 | +| output vertices | 571651 | +| output triangles | 1137703 | +| constraint edges | 5597 | +| vertices without data dropped | 0 | +| mesh_fine_t1_run1.vtk | 41.1 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 45.00° | 0.01 % | 0.27 % | 0.0834° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 16 | 80 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 1 m | 1 m | 33 | 555349 | 0 | 1193738 | 0 | 10789 | 1841 | 84 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.006 | 0.7 % | +| decode | 0.311 | 34.4 % | +| start mesh: build | 0.000 | 0.0 % | +| start mesh: node | 0.006 | 0.7 % | +| start mesh: triangulate | 0.003 | 0.3 % | +| start mesh: constraint edges | 0.007 | 0.8 % | +| refine | 0.481 | 53.3 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.011 | 1.2 % | +| refine: scan (parallel) | 0.099 | 11.0 % | +| refine: split + flip (serial) | 0.324 | 36.0 % | +| refine: setup + output | 0.046 | 5.1 % | +| trim | 0.043 | 4.8 % | +| write: encode | 0.023 | 2.6 % | +| write: disk | 0.018 | 2.0 % | +| other | 0.003 | 0.4 % | +| **total** | **0.902** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.204 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run2.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run2.err new file mode 100644 index 00000000..63934c30 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run2.err @@ -0,0 +1,20 @@ +10789 start quality nodes inserted, 1841 start quality skips, 84 constraint feet, 0 feet refused, 33 rounds, 555349 points inserted, 1193738 flips, 1137703 triangles, achieved max error 1 m, 0 valid DEM nodes not covered, 5511 start triangles, 5513 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 1.39 real 2.43 user 0.19 sys + 942194688 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 65599 page reclaims + 44 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 17 voluntary context switches + 892 involuntary context switches + 23949464331 instructions retired + 8021673555 cycles elapsed + 920503496 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run2.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run2.out new file mode 100644 index 00000000..4d234678 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run2.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_fine_t1_run2.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run2.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run2.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run2.stats.md new file mode 100644 index 00000000..741664a2 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run2.stats.md @@ -0,0 +1,64 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t0.geojson --tolerance 1 --binary --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_fine_t1_run2.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run2.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 5513 (1 ring, 0 holes) | +| start vertices | 5513 | +| start triangles | 5511 | +| output vertices | 571651 | +| output triangles | 1137703 | +| constraint edges | 5597 | +| vertices without data dropped | 0 | +| mesh_fine_t1_run2.vtk | 41.1 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 45.00° | 0.01 % | 0.27 % | 0.0834° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 16 | 80 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 1 m | 1 m | 33 | 555349 | 0 | 1193738 | 0 | 10789 | 1841 | 84 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.006 | 0.7 % | +| decode | 0.312 | 33.5 % | +| start mesh: build | 0.000 | 0.0 % | +| start mesh: node | 0.006 | 0.7 % | +| start mesh: triangulate | 0.003 | 0.3 % | +| start mesh: constraint edges | 0.007 | 0.7 % | +| refine | 0.483 | 51.9 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.011 | 1.2 % | +| refine: scan (parallel) | 0.100 | 10.7 % | +| refine: split + flip (serial) | 0.326 | 35.1 % | +| refine: setup + output | 0.046 | 4.9 % | +| trim | 0.044 | 4.7 % | +| write: encode | 0.040 | 4.3 % | +| write: disk | 0.026 | 2.8 % | +| other | 0.003 | 0.4 % | +| **total** | **0.931** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.205 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run3.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run3.err new file mode 100644 index 00000000..01c6465a --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run3.err @@ -0,0 +1,20 @@ +10789 start quality nodes inserted, 1841 start quality skips, 84 constraint feet, 0 feet refused, 33 rounds, 555349 points inserted, 1193738 flips, 1137703 triangles, achieved max error 1 m, 0 valid DEM nodes not covered, 5511 start triangles, 5513 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 1.38 real 2.43 user 0.19 sys + 941096960 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 68102 page reclaims + 44 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 16 voluntary context switches + 920 involuntary context switches + 23952962455 instructions retired + 8052110280 cycles elapsed + 919422176 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run3.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run3.out new file mode 100644 index 00000000..da838706 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run3.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_fine_t1_run3.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run3.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run3.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run3.stats.md new file mode 100644 index 00000000..6bd4566b --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run3.stats.md @@ -0,0 +1,64 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t0.geojson --tolerance 1 --binary --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_fine_t1_run3.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_fine_t1_run3.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 5513 (1 ring, 0 holes) | +| start vertices | 5513 | +| start triangles | 5511 | +| output vertices | 571651 | +| output triangles | 1137703 | +| constraint edges | 5597 | +| vertices without data dropped | 0 | +| mesh_fine_t1_run3.vtk | 41.1 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 45.00° | 0.01 % | 0.27 % | 0.0834° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 16 | 80 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 1 m | 1 m | 33 | 555349 | 0 | 1193738 | 0 | 10789 | 1841 | 84 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.006 | 0.7 % | +| decode | 0.312 | 34.0 % | +| start mesh: build | 0.000 | 0.0 % | +| start mesh: node | 0.006 | 0.7 % | +| start mesh: triangulate | 0.003 | 0.3 % | +| start mesh: constraint edges | 0.007 | 0.8 % | +| refine | 0.486 | 53.0 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.011 | 1.2 % | +| refine: scan (parallel) | 0.100 | 10.9 % | +| refine: split + flip (serial) | 0.329 | 35.9 % | +| refine: setup + output | 0.046 | 5.0 % | +| trim | 0.044 | 4.8 % | +| write: encode | 0.022 | 2.5 % | +| write: disk | 0.027 | 3.0 % | +| other | 0.003 | 0.4 % | +| **total** | **0.917** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.204 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_ascii.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_ascii.err new file mode 100644 index 00000000..b1016c5f --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_ascii.err @@ -0,0 +1,20 @@ +1202 start quality nodes inserted, 145 start quality skips, 212 constraint feet, 0 feet refused, 28 rounds, 24634 points inserted, 60095 flips, 52198 triangles, achieved max error 9.999777071593144 m, 0 valid DEM nodes not covered, 738 start triangles, 740 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 0.70 real 1.14 user 0.16 sys + 651853824 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 52354 page reclaims + 44 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 4 voluntary context switches + 623 involuntary context switches + 15748668832 instructions retired + 4038961335 cycles elapsed + 628933568 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_ascii.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_ascii.out new file mode 100644 index 00000000..7825f177 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_ascii.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_red_t10_ascii.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_ascii.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_ascii.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_ascii.stats.md new file mode 100644 index 00000000..ff652294 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_ascii.stats.md @@ -0,0 +1,64 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t20_run1.geojson --tolerance 10 --ascii --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_red_t10_ascii.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_ascii.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 740 (1 ring, 0 holes) | +| start vertices | 740 | +| start triangles | 738 | +| output vertices | 26576 | +| output triangles | 52198 | +| constraint edges | 952 | +| vertices without data dropped | 0 | +| mesh_red_t10_ascii.vtk | 2.6 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 34.43° | 0.01 % | 1.23 % | 0.356° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 12 | 3 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 10 m | 9.999777071593144 m | 28 | 24634 | 0 | 60095 | 0 | 1202 | 145 | 212 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.003 | 0.7 % | +| decode | 0.309 | 70.0 % | +| start mesh: build | 0.000 | 0.0 % | +| start mesh: node | 0.001 | 0.2 % | +| start mesh: triangulate | 0.000 | 0.1 % | +| start mesh: constraint edges | 0.001 | 0.2 % | +| refine | 0.034 | 7.7 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.001 | 0.3 % | +| refine: scan (parallel) | 0.019 | 4.3 % | +| refine: split + flip (serial) | 0.011 | 2.5 % | +| refine: setup + output | 0.002 | 0.5 % | +| trim | 0.002 | 0.4 % | +| write: encode | 0.086 | 19.5 % | +| write: disk | 0.002 | 0.5 % | +| other | 0.003 | 0.7 % | +| **total** | **0.441** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.010 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run1.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run1.err new file mode 100644 index 00000000..6363c56f --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run1.err @@ -0,0 +1,20 @@ +1202 start quality nodes inserted, 145 start quality skips, 212 constraint feet, 0 feet refused, 28 rounds, 24634 points inserted, 60095 flips, 52198 triangles, achieved max error 9.999777071593144 m, 0 valid DEM nodes not covered, 738 start triangles, 740 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 0.63 real 1.06 user 0.15 sys + 643219456 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 46851 page reclaims + 109 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 5 voluntary context switches + 718 involuntary context switches + 14372415862 instructions retired + 3738694731 cycles elapsed + 620053416 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run1.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run1.out new file mode 100644 index 00000000..d8dc9061 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run1.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_red_t10_run1.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run1.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run1.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run1.stats.md new file mode 100644 index 00000000..9f73d74a --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run1.stats.md @@ -0,0 +1,64 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t20_run1.geojson --tolerance 10 --binary --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_red_t10_run1.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run1.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 740 (1 ring, 0 holes) | +| start vertices | 740 | +| start triangles | 738 | +| output vertices | 26576 | +| output triangles | 52198 | +| constraint edges | 952 | +| vertices without data dropped | 0 | +| mesh_red_t10_run1.vtk | 1.9 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 34.43° | 0.01 % | 1.23 % | 0.356° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 12 | 3 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 10 m | 9.999777071593144 m | 28 | 24634 | 0 | 60095 | 0 | 1202 | 145 | 212 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.004 | 1.0 % | +| decode | 0.306 | 85.9 % | +| start mesh: build | 0.000 | 0.0 % | +| start mesh: node | 0.001 | 0.2 % | +| start mesh: triangulate | 0.000 | 0.1 % | +| start mesh: constraint edges | 0.001 | 0.2 % | +| refine | 0.034 | 9.6 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.001 | 0.3 % | +| refine: scan (parallel) | 0.020 | 5.5 % | +| refine: split + flip (serial) | 0.011 | 3.1 % | +| refine: setup + output | 0.003 | 0.7 % | +| trim | 0.002 | 0.5 % | +| write: encode | 0.004 | 1.2 % | +| write: disk | 0.001 | 0.3 % | +| other | 0.003 | 0.9 % | +| **total** | **0.356** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.010 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run2.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run2.err new file mode 100644 index 00000000..0f8d348e --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run2.err @@ -0,0 +1,20 @@ +1202 start quality nodes inserted, 145 start quality skips, 212 constraint feet, 0 feet refused, 28 rounds, 24634 points inserted, 60095 flips, 52198 triangles, achieved max error 9.999777071593144 m, 0 valid DEM nodes not covered, 738 start triangles, 740 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 0.62 real 1.06 user 0.15 sys + 645562368 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 50541 page reclaims + 44 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 0 voluntary context switches + 625 involuntary context switches + 14377516726 instructions retired + 3753778848 cycles elapsed + 622396328 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run2.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run2.out new file mode 100644 index 00000000..7e542f1b --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run2.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_red_t10_run2.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run2.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run2.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run2.stats.md new file mode 100644 index 00000000..0ed3a567 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run2.stats.md @@ -0,0 +1,64 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t20_run1.geojson --tolerance 10 --binary --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_red_t10_run2.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run2.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 740 (1 ring, 0 holes) | +| start vertices | 740 | +| start triangles | 738 | +| output vertices | 26576 | +| output triangles | 52198 | +| constraint edges | 952 | +| vertices without data dropped | 0 | +| mesh_red_t10_run2.vtk | 1.9 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 34.43° | 0.01 % | 1.23 % | 0.356° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 12 | 3 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 10 m | 9.999777071593144 m | 28 | 24634 | 0 | 60095 | 0 | 1202 | 145 | 212 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.003 | 0.9 % | +| decode | 0.306 | 86.1 % | +| start mesh: build | 0.000 | 0.0 % | +| start mesh: node | 0.001 | 0.2 % | +| start mesh: triangulate | 0.000 | 0.1 % | +| start mesh: constraint edges | 0.001 | 0.2 % | +| refine | 0.034 | 9.5 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.001 | 0.3 % | +| refine: scan (parallel) | 0.019 | 5.4 % | +| refine: split + flip (serial) | 0.011 | 3.2 % | +| refine: setup + output | 0.002 | 0.7 % | +| trim | 0.002 | 0.6 % | +| write: encode | 0.004 | 1.2 % | +| write: disk | 0.001 | 0.1 % | +| other | 0.003 | 0.9 % | +| **total** | **0.355** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.009 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run3.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run3.err new file mode 100644 index 00000000..2871afdf --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run3.err @@ -0,0 +1,20 @@ +1202 start quality nodes inserted, 145 start quality skips, 212 constraint feet, 0 feet refused, 28 rounds, 24634 points inserted, 60095 flips, 52198 triangles, achieved max error 9.999777071593144 m, 0 valid DEM nodes not covered, 738 start triangles, 740 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 0.62 real 1.06 user 0.15 sys + 647921664 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 49962 page reclaims + 44 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 0 voluntary context switches + 672 involuntary context switches + 14395382812 instructions retired + 3755607788 cycles elapsed + 624755624 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run3.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run3.out new file mode 100644 index 00000000..f0d76148 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run3.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_red_t10_run3.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run3.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run3.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run3.stats.md new file mode 100644 index 00000000..bedf4b9b --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run3.stats.md @@ -0,0 +1,64 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t20_run1.geojson --tolerance 10 --binary --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_red_t10_run3.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t10_run3.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 740 (1 ring, 0 holes) | +| start vertices | 740 | +| start triangles | 738 | +| output vertices | 26576 | +| output triangles | 52198 | +| constraint edges | 952 | +| vertices without data dropped | 0 | +| mesh_red_t10_run3.vtk | 1.9 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 34.43° | 0.01 % | 1.23 % | 0.356° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 12 | 3 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 10 m | 9.999777071593144 m | 28 | 24634 | 0 | 60095 | 0 | 1202 | 145 | 212 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.003 | 0.9 % | +| decode | 0.307 | 86.4 % | +| start mesh: build | 0.000 | 0.0 % | +| start mesh: node | 0.001 | 0.2 % | +| start mesh: triangulate | 0.000 | 0.1 % | +| start mesh: constraint edges | 0.001 | 0.2 % | +| refine | 0.033 | 9.3 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.001 | 0.3 % | +| refine: scan (parallel) | 0.019 | 5.4 % | +| refine: split + flip (serial) | 0.011 | 3.0 % | +| refine: setup + output | 0.002 | 0.7 % | +| trim | 0.002 | 0.6 % | +| write: encode | 0.004 | 1.2 % | +| write: disk | 0.001 | 0.1 % | +| other | 0.003 | 0.9 % | +| **total** | **0.356** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.010 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_ascii.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_ascii.err new file mode 100644 index 00000000..446c3ee2 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_ascii.err @@ -0,0 +1,20 @@ +1202 start quality nodes inserted, 145 start quality skips, 1254 constraint feet, 0 feet refused, 36 rounds, 563569 points inserted, 1218749 flips, 1129026 triangles, achieved max error 1 m, 0 valid DEM nodes not covered, 738 start triangles, 740 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 3.19 real 4.18 user 0.29 sys + 1156923392 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 109434 page reclaims + 44 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 16 voluntary context switches + 995 involuntary context switches + 52887328192 instructions retired + 13971370462 cycles elapsed + 1135576432 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_ascii.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_ascii.out new file mode 100644 index 00000000..0530407f --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_ascii.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_red_t1_ascii.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_ascii.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_ascii.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_ascii.stats.md new file mode 100644 index 00000000..cd7164a3 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_ascii.stats.md @@ -0,0 +1,64 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t20_run1.geojson --tolerance 1 --ascii --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_red_t1_ascii.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_ascii.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 740 (1 ring, 0 holes) | +| start vertices | 740 | +| start triangles | 738 | +| output vertices | 565511 | +| output triangles | 1129026 | +| constraint edges | 1994 | +| vertices without data dropped | 0 | +| mesh_red_t1_ascii.vtk | 58.6 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 45.00° | 0.03 % | 0.36 % | 0.0461° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 18 | 102 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 1 m | 1 m | 36 | 563569 | 0 | 1218749 | 0 | 1202 | 145 | 1254 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.003 | 0.1 % | +| decode | 0.308 | 11.2 % | +| start mesh: build | 0.000 | 0.0 % | +| start mesh: node | 0.001 | 0.0 % | +| start mesh: triangulate | 0.000 | 0.0 % | +| start mesh: constraint edges | 0.001 | 0.0 % | +| refine | 0.508 | 18.5 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.001 | 0.0 % | +| refine: scan (parallel) | 0.108 | 4.0 % | +| refine: split + flip (serial) | 0.353 | 12.9 % | +| refine: setup + output | 0.046 | 1.7 % | +| trim | 0.049 | 1.8 % | +| write: encode | 1.833 | 66.9 % | +| write: disk | 0.034 | 1.2 % | +| other | 0.003 | 0.1 % | +| **total** | **2.741** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.193 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run1.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run1.err new file mode 100644 index 00000000..8b00e0a5 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run1.err @@ -0,0 +1,20 @@ +1202 start quality nodes inserted, 145 start quality skips, 1254 constraint feet, 0 feet refused, 36 rounds, 563569 points inserted, 1218749 flips, 1129026 triangles, achieved max error 1 m, 0 valid DEM nodes not covered, 738 start triangles, 740 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 1.39 real 2.42 user 0.21 sys + 923910144 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 66791 page reclaims + 44 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 13 voluntary context switches + 1458 involuntary context switches + 23881754846 instructions retired + 8078512563 cycles elapsed + 902202544 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run1.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run1.out new file mode 100644 index 00000000..5ea95199 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run1.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_red_t1_run1.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run1.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run1.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run1.stats.md new file mode 100644 index 00000000..7df50fe6 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run1.stats.md @@ -0,0 +1,64 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t20_run1.geojson --tolerance 1 --binary --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_red_t1_run1.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run1.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 740 (1 ring, 0 holes) | +| start vertices | 740 | +| start triangles | 738 | +| output vertices | 565511 | +| output triangles | 1129026 | +| constraint edges | 1994 | +| vertices without data dropped | 0 | +| mesh_red_t1_run1.vtk | 40.7 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 45.00° | 0.03 % | 0.36 % | 0.0461° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 18 | 102 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 1 m | 1 m | 36 | 563569 | 0 | 1218749 | 0 | 1202 | 145 | 1254 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.003 | 0.4 % | +| decode | 0.310 | 33.3 % | +| start mesh: build | 0.000 | 0.0 % | +| start mesh: node | 0.001 | 0.1 % | +| start mesh: triangulate | 0.000 | 0.0 % | +| start mesh: constraint edges | 0.001 | 0.1 % | +| refine | 0.497 | 53.3 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.001 | 0.1 % | +| refine: scan (parallel) | 0.105 | 11.3 % | +| refine: split + flip (serial) | 0.347 | 37.2 % | +| refine: setup + output | 0.044 | 4.7 % | +| trim | 0.043 | 4.6 % | +| write: encode | 0.044 | 4.7 % | +| write: disk | 0.030 | 3.2 % | +| other | 0.003 | 0.3 % | +| **total** | **0.933** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.197 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run2.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run2.err new file mode 100644 index 00000000..f40fa1de --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run2.err @@ -0,0 +1,20 @@ +1202 start quality nodes inserted, 145 start quality skips, 1254 constraint feet, 0 feet refused, 36 rounds, 563569 points inserted, 1218749 flips, 1129026 triangles, achieved max error 1 m, 0 valid DEM nodes not covered, 738 start triangles, 740 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 1.38 real 2.46 user 0.20 sys + 925401088 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 70065 page reclaims + 44 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 17 voluntary context switches + 932 involuntary context switches + 23890779174 instructions retired + 8158706283 cycles elapsed + 903709896 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run2.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run2.out new file mode 100644 index 00000000..bdb41d23 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run2.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_red_t1_run2.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run2.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run2.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run2.stats.md new file mode 100644 index 00000000..52b60283 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run2.stats.md @@ -0,0 +1,64 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t20_run1.geojson --tolerance 1 --binary --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_red_t1_run2.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run2.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 740 (1 ring, 0 holes) | +| start vertices | 740 | +| start triangles | 738 | +| output vertices | 565511 | +| output triangles | 1129026 | +| constraint edges | 1994 | +| vertices without data dropped | 0 | +| mesh_red_t1_run2.vtk | 40.7 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 45.00° | 0.03 % | 0.36 % | 0.0461° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 18 | 102 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 1 m | 1 m | 36 | 563569 | 0 | 1218749 | 0 | 1202 | 145 | 1254 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.003 | 0.4 % | +| decode | 0.309 | 33.3 % | +| start mesh: build | 0.000 | 0.0 % | +| start mesh: node | 0.001 | 0.1 % | +| start mesh: triangulate | 0.000 | 0.0 % | +| start mesh: constraint edges | 0.001 | 0.1 % | +| refine | 0.496 | 53.5 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.001 | 0.1 % | +| refine: scan (parallel) | 0.104 | 11.2 % | +| refine: split + flip (serial) | 0.347 | 37.4 % | +| refine: setup + output | 0.045 | 4.8 % | +| trim | 0.049 | 5.2 % | +| write: encode | 0.032 | 3.5 % | +| write: disk | 0.033 | 3.5 % | +| other | 0.003 | 0.3 % | +| **total** | **0.927** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.195 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run3.err b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run3.err new file mode 100644 index 00000000..75285fe3 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run3.err @@ -0,0 +1,20 @@ +1202 start quality nodes inserted, 145 start quality skips, 1254 constraint feet, 0 feet refused, 36 rounds, 563569 points inserted, 1218749 flips, 1129026 triangles, achieved max error 1 m, 0 valid DEM nodes not covered, 738 start triangles, 740 start vertices off-node, 0 vertices without data dropped +mosaic of 2 tiles, 2195 x 3314 nodes + 1.37 real 2.45 user 0.20 sys + 929644544 maximum resident set size + 0 average shared memory size + 0 average unshared data size + 0 average unshared stack size + 68173 page reclaims + 44 page faults + 0 swaps + 0 block input operations + 0 block output operations + 0 messages sent + 0 messages received + 0 signals received + 11 voluntary context switches + 967 involuntary context switches + 23896562231 instructions retired + 8123335720 cycles elapsed + 907936944 peak memory footprint diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run3.out b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run3.out new file mode 100644 index 00000000..11734301 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run3.out @@ -0,0 +1,2 @@ +/private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_red_t1_run3.vtk +/Users/skavhaug/projects/rasputin/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run3.stats.md diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run3.stats.md b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run3.stats.md new file mode 100644 index 00000000..7cdf21bc --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run3.stats.md @@ -0,0 +1,64 @@ +# rasputin mesh — statistics + +`rasputin mesh --dem ../rasputin_data/DTM10_UTM33_20260925 --domain /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/catchment_t20_run1.geojson --tolerance 1 --binary --out /private/tmp/claude-501/-Users-skavhaug-projects-rasputin/38487caf-f56e-46a3-b05b-867af1fb1619/scratchpad/run/mesh_red_t1_run3.vtk --stats docs/benchmarks/2026-09-29/bygdin/logs/mesh_red_t1_run3.stats.md` + +## Sizes + +| item | count | +|---|---| +| DEM nodes | 2195 × 3314 (10 m) | +| domain vertices | 740 (1 ring, 0 holes) | +| start vertices | 740 | +| start triangles | 738 | +| output vertices | 565511 | +| output triangles | 1129026 | +| constraint edges | 1994 | +| vertices without data dropped | 0 | +| mesh_red_t1_run3.vtk | 40.7 MB | + +## Quality (plan view, x/y) + +| metric | median | < 1° | < 10° | worst | +|---|---|---|---|---| +| minimum angle | 45.00° | 0.03 % | 0.36 % | 0.0461° | + +| metric | median | p99 | max | ≥ 12 | ≥ 20 | +|---|---|---|---|---|---| +| vertex degree (triangles) | 6 | 9 | 18 | 102 | 0 | + +## Refinement + +| tolerance | achieved max error | rounds | inserted | carved | flips | uncovered | quality inserted | quality skipped | feet | +|---|---|---|---|---|---|---|---|---|---| +| 1 m | 1 m | 36 | 563569 | 0 | 1218749 | 0 | 1202 | 145 | 1254 | + +## Timings + +Wall clock, `time.perf_counter_ns` (Python) and `std::chrono::steady_clock` +(inside `refine`), one run, no warm-up. Total is the `mesh` command body, from +argument checks to the last file written; interpreter start-up and imports are +not in it. Threads: 10 (hardware concurrency). + +| phase | seconds | share | +|---|---|---| +| domain read | 0.003 | 0.4 % | +| decode | 0.308 | 33.6 % | +| start mesh: build | 0.000 | 0.0 % | +| start mesh: node | 0.001 | 0.1 % | +| start mesh: triangulate | 0.000 | 0.0 % | +| start mesh: constraint edges | 0.001 | 0.1 % | +| refine | 0.504 | 55.0 % | +| refine: legalise start | 0.000 | 0.0 % | +| refine: start quality | 0.001 | 0.1 % | +| refine: scan (parallel) | 0.104 | 11.4 % | +| refine: split + flip (serial) | 0.353 | 38.6 % | +| refine: setup + output | 0.045 | 4.9 % | +| trim | 0.044 | 4.8 % | +| write: encode | 0.023 | 2.5 % | +| write: disk | 0.029 | 3.2 % | +| other | 0.003 | 0.3 % | +| **total** | **0.915** | **100 %** | + +Sub-rows sum to their parent and are not added to the total. + +Statistics computed in 0.201 s, not included above. diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/pmset_catchment_end.txt b/docs/benchmarks/2026-09-29/bygdin/logs/pmset_catchment_end.txt new file mode 100644 index 00000000..a692306c --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/pmset_catchment_end.txt @@ -0,0 +1,2 @@ +Now drawing from 'AC Power' + -InternalBattery-0 (id=7929955) 80%; AC attached; not charging present: true diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/pmset_catchment_start.txt b/docs/benchmarks/2026-09-29/bygdin/logs/pmset_catchment_start.txt new file mode 100644 index 00000000..a692306c --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/pmset_catchment_start.txt @@ -0,0 +1,2 @@ +Now drawing from 'AC Power' + -InternalBattery-0 (id=7929955) 80%; AC attached; not charging present: true diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/pmset_mesh_end.txt b/docs/benchmarks/2026-09-29/bygdin/logs/pmset_mesh_end.txt new file mode 100644 index 00000000..a692306c --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/pmset_mesh_end.txt @@ -0,0 +1,2 @@ +Now drawing from 'AC Power' + -InternalBattery-0 (id=7929955) 80%; AC attached; not charging present: true diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/pmset_mesh_start.txt b/docs/benchmarks/2026-09-29/bygdin/logs/pmset_mesh_start.txt new file mode 100644 index 00000000..a692306c --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/pmset_mesh_start.txt @@ -0,0 +1,2 @@ +Now drawing from 'AC Power' + -InternalBattery-0 (id=7929955) 80%; AC attached; not charging present: true diff --git a/docs/benchmarks/2026-09-29/bygdin/logs/pmset_quality_end.txt b/docs/benchmarks/2026-09-29/bygdin/logs/pmset_quality_end.txt new file mode 100644 index 00000000..a692306c --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/logs/pmset_quality_end.txt @@ -0,0 +1,2 @@ +Now drawing from 'AC Power' + -InternalBattery-0 (id=7929955) 80%; AC attached; not charging present: true diff --git a/docs/benchmarks/2026-09-29/bygdin/run.sh b/docs/benchmarks/2026-09-29/bygdin/run.sh new file mode 100644 index 00000000..36aed324 --- /dev/null +++ b/docs/benchmarks/2026-09-29/bygdin/run.sh @@ -0,0 +1,56 @@ +#!/bin/bash +# Increment 22 acceptance on Bygdin (@perf, 2026-09-29). Runs every timed +# block and writes raw logs to ./logs; meshes and GeoJSONs go to $SCRATCH. +# Usage: SCRATCH=/some/dir bash run.sh (from the repository root, .venv active) +set -u +HERE=docs/benchmarks/2026-09-29/bygdin +LOG=$HERE/logs +DEM=../rasputin_data/DTM10_UTM33_20260925 +GPKG=../rasputin_data/corine2018_dtm10_utm33.gpkg +mkdir -p "$LOG" "$SCRATCH" + +catchment() { # $1 tolerance ("" = default), $2 tag + local tol=() + [ -n "$1" ] && tol=(--outline-tolerance "$1") + /usr/bin/time -l rasputin catchment --dem $DEM --seed 8.5425 61.3512 \ + --lakes $GPKG --lakes-layer corine2018 ${tol[@]+"${tol[@]}"} \ + --out "$SCRATCH/$2.geojson" > "$LOG/$2.out" 2> "$LOG/$2.err" + echo "$2 exit=$?" | tee -a "$LOG/exits.txt" +} + +mesh() { # $1 domain geojson, $2 tolerance, $3 tag, $4 ascii|binary, rest: extra args + local dom=$1 tol=$2 tag=$3 fmt=$4; shift 4 + /usr/bin/time -l rasputin mesh --dem $DEM --domain "$dom" --tolerance "$tol" \ + "$@" --"$fmt" --out "$SCRATCH/$tag.vtk" --stats "$LOG/$tag.stats.md" \ + > "$LOG/$tag.out" 2> "$LOG/$tag.err" + echo "$tag exit=$?" | tee -a "$LOG/exits.txt" +} + +# Block 1: the catchment. +pmset -g batt > "$LOG/pmset_catchment_start.txt" +for i in 1 2 3; do catchment "" "catchment_t20_run$i"; done +for t in 0 10 20 50; do catchment "$t" "catchment_t$t"; done +pmset -g batt > "$LOG/pmset_catchment_end.txt" +cp "$SCRATCH/catchment_t20_run1.geojson" "$HERE/bygdin_reduced_t20.geojson" + +# Block 2: meshes. Three timed binary runs each, then one ASCII run for the +# quality check (tools/bench.py's quality(), which reads ASCII only). +RED=$SCRATCH/catchment_t20_run1.geojson +FINE=$SCRATCH/catchment_t0.geojson +pmset -g batt > "$LOG/pmset_mesh_start.txt" +for i in 1 2 3; do + mesh "$RED" 10 "mesh_red_t10_run$i" binary + mesh "$RED" 1 "mesh_red_t1_run$i" binary + mesh "$FINE" 10 "mesh_fine_t10_run$i" binary + mesh "$FINE" 1 "mesh_fine_t1_run$i" binary + mesh "$RED" 10 "mesh_feat_t10_run$i" binary \ + --features $GPKG --features-layer corine2018 --features-map corine +done +pmset -g batt > "$LOG/pmset_mesh_end.txt" +mesh "$RED" 10 mesh_red_t10_ascii ascii +mesh "$RED" 1 mesh_red_t1_ascii ascii +mesh "$FINE" 10 mesh_fine_t10_ascii ascii +mesh "$FINE" 1 mesh_fine_t1_ascii ascii +mesh "$RED" 10 mesh_feat_t10_ascii ascii \ + --features $GPKG --features-layer corine2018 --features-map corine +pmset -g batt > "$LOG/pmset_quality_end.txt" diff --git a/docs/increments/22-auto-catchment.md b/docs/increments/22-auto-catchment.md index a64dfddc..1e577768 100644 --- a/docs/increments/22-auto-catchment.md +++ b/docs/increments/22-auto-catchment.md @@ -1,9 +1,9 @@ # Increment 22 — auto-catchment: the catchment of a lake, from the DEM (Bygdin first) -Status: **PR 1 built, under review** (2026-09-29). Designed by `@architect` -on branch `increment22-autocatchment` off master `b4847d7`; PR 1 built at -`58f6904` (green), under review; PR 2 (the outline reduction) designed, in a -stacked PR ("As built" below covers PR 1 only). Ola was +Status: **built, under review** (2026-09-29). Designed by `@architect` on +branch `increment22-autocatchment` off master `b4847d7`; PR 1 built at +`58f6904`, PR 2 at `ac778e4`; `@reviewer`'s first pass asked for changes, and +the documentation half of them is in this file ("As built" below). Ola was asleep while this was written; every choice he would normally make is marked "Default (main session / @architect, 2026-09-29), for Ola to confirm", with the alternative, so the loop can run tonight. @@ -583,7 +583,8 @@ seed, for tolerance). - *Simple*: A-E and E-D do not meet any current edge except at A (with the edge ending at A) and at D (with the edge starting at D), and do not overlap those two. A uniform grid of current edges (bucket side the - tolerance, at least one cell) finds the candidates; it is updated on + larger of the tolerance and the mean fine edge, as built) finds the + candidates; it is updated on every collapse. - *Seed inside*: each keep-point has winding number zero around the closed loop A-B-C-D-E-A and lies on neither new edge. (The difference between @@ -593,7 +594,9 @@ seed, for tolerance). four vertices include A, E or D, and repeat until none is admissible or 4 vertices remain. Stale heap entries are skipped by a per-vertex version. -With tolerance 0 only the collinear pass runs. +With tolerance 0 only the collinear pass runs, so the result is the fine +ring less its collinear vertices: 5,513 vertices on Bygdin, not the traced +17,812 (`@reviewer`, at `ac778e4`). Same shape, same area. **Guarantees**, each with the check the tests make: @@ -604,12 +607,24 @@ With tolerance 0 only the collinear pass runs. vertex. Tested by shapely `is_valid` and, on small cases, a brute-force pairwise test with exact orientation. - *Seed inside*: the seed point is strictly inside (shapely `contains`). -- *Tolerance*: every fine vertex is within the tolerance of the reduced ring, - and every reduced vertex within the tolerance of the fine ring. So the - symmetric Hausdorff distance between the two is at most the tolerance plus - one cell: tested with shapely `hausdorff_distance(..., densify=0.05)`. +- *Tolerance, at vertices*: every fine vertex is within the tolerance of the + reduced ring, and every reduced vertex within the tolerance of the fine + ring. That is what the deviation bounds, and all it bounds. - *Deterministic*: serial, ordered by (deviation, id); the same input gives the same bits. +- **Measured, not guaranteed: the Hausdorff distance.** (Revised 2026-09-29 + at review; the first version claimed a symmetric Hausdorff distance of at + most the tolerance plus one cell. That does not follow: the deviation never + bounds the interior of a reduced edge, so a long new edge can pass further + from the fine ring than either of its ends.) `@reviewer`'s probe at + `ac778e4`: on 2,000 traced lattice rings the worst was 0.93 of (tolerance + plus one cell), none over; on 8,994 random star rings, not from a lattice, + 34 exceeded the tolerance, the worst by 1.34 times. Bygdin: 19.92 m at a + 20 m tolerance. So on traced outlines it has held, and it is not proved. + **Open, for Ola to decide**: enforce it, by also checking the interior of + A-E and E-D against the fine range (the distance from each fine segment in + the range to the new chain, and from the new edges' points to the fine + segments), at some cost per candidate; or keep it measured. - Not guaranteed: the fewest vertices (it is greedy), or that every catchment node is inside (a node within the tolerance of the boundary may fall either side). @@ -649,8 +664,9 @@ rasputin catchment --dem PATH [--dem PATH ...] --seed X Y [--seed-crs CRS] point and CRS, tolerance, node count, fine and reduced vertex counts, fine and reduced areas in m², the windows' sizes. Coordinates written with `repr` precision, so the area survives the round trip. -- `--outline-tolerance 0` writes the fine outline (collinear vertices only - removed), for comparison. +- `--outline-tolerance 0` writes the fine ring less its collinear vertices + (5,513 vertices on Bygdin, against 17,812 traced), same shape and area, + for comparison. - stderr, one line each: every window (box, nodes, flood seconds, and "grown: touches north" or "contained"); the seed (lake polygon with its area and seed-node count, or the pour node); the catchment (nodes, node @@ -666,27 +682,27 @@ request and the result are frozen; the CLI is the only place with paths. ### New and changed files -| File | What | Estimate | As built, PR 1 | -|---|---|---|---| -| `include/terrain/hydrology/upstream.hpp` | the flood, `UpstreamOutcome` | 110 | 106 | -| `include/terrain/vector_simplify/area_collapse.hpp` | the reduction, `ReduceOutcome`, edge grid | 260 | | -| `bindings/core.cpp` | `upstream`, `reduce_ring`, two outcome classes | 80 | 48 | -| `src_python/tin_engine/_core.pyi` | their stubs | 30 | 19 | -| `src_python/tin_engine/outline.py` | the tracer | 70 | 50 | -| `src_python/tin_engine/catchment.py` | request, seed, window loop, result | 170 | 244 | -| `src_python/tin_engine/dem_input.py` | repository helper split out | 10 | 1 | -| `src_python/tin_engine/feature_input.py` | `read_source` split out of `_Tally.source`, `read_lakes` | 30 | 38 net | -| `src_python/tin_engine/cli.py` | `catchment` command, report, writer | 100 | 113 | -| **Total** | | about 860 | **619 net** (674 added, 55 removed) | -| `project_structure.md` | the two C++ modules and two Python modules | docs | | - -About 860 lines estimated, over the 700 ceiling (CLAUDE.md §2), so two PRs. +| File | What | Estimate | As built, PR 1 | As built, PR 2 | +|---|---|---|---|---| +| `include/terrain/hydrology/upstream.hpp` | the flood, `UpstreamOutcome` | 110 | 106 | | +| `include/terrain/vector_simplify/area_collapse.hpp` | the reduction, `ReduceOutcome`, edge grid | 260 | | 286 | +| `bindings/core.cpp` | `upstream`, `reduce_ring`, two outcome classes | 80 | 48 | 46 | +| `src_python/tin_engine/_core.pyi` | their stubs | 30 | 19 | 22 | +| `src_python/tin_engine/outline.py` | the tracer | 70 | 50 | | +| `src_python/tin_engine/catchment.py` | request, seed, window loop, result | 170 | 244 | 24 | +| `src_python/tin_engine/dem_input.py` | repository helper split out | 10 | 1 | | +| `src_python/tin_engine/feature_input.py` | `read_source` split out of `_Tally.source`, `read_lakes` | 30 | 38 net | | +| `src_python/tin_engine/cli.py` | `catchment` command, report, writer | 100 | 113 | 28 | +| **Total** | | about 860 | **619 net** (674 added, 55 removed) | **406 net** | +| `project_structure.md` | the two C++ modules and two Python modules | docs | | | + "As built" is `@reviewer`'s count at review (2026-09-29), by CLAUDE.md §2's rule (blank lines, comments and docstrings not counted), over PR 1 = -`master..608e366`. The totals are `@reviewer`'s; the per-file numbers are -theirs too and were not recounted here. The PR 1 column, counted at -`58f6904`, sums to 619. PR 1 is under the 700 ceiling. PR 2's figures are the -estimates until it is built. +`master..608e366` and PR 2 = `039cf3c..ac778e4`. The totals are +`@reviewer`'s; the per-file numbers are theirs too and were not recounted +here. The PR 1 column, counted at `58f6904`, sums to 619. PR 2's figures +include `6757925`'s review fix (`catchment.py` +1, `cli.py` +2), taking it +from 403 to 406. Both PRs are under the 700 ceiling. ### The PR split @@ -700,7 +716,7 @@ estimates until it is built. Both go on `increment22-autocatchment`, PR 2 stacked on PR 1. As built: PR 1 is `master..608e366` (red `1e1b3bb`, green `58f6904`, the window test -`608e366`); PR 2 is designed, in a stacked PR. Neither touches +`608e366`), PR 2 is `039cf3c..ac778e4` (red `039cf3c`, green `ac778e4`). Neither touches refine or mesh code, so the 1 m benchmark and scaling sweep (README, rule 2) do not apply; the Bygdin run below is this increment's acceptance. @@ -766,11 +782,12 @@ for mutation testing tonight. must not cross itself; the same input twice gives equal bits. - `test_core_reduce.py`, on rings traced from random blobs and from the PR 1 synthetic catchments: the five guarantees above (area, simple, seed - inside, tolerance at vertices, Hausdorff within tolerance plus a cell), and - determinism. + inside, tolerance at vertices, and the Hausdorff distance, which on traced + lattice rings is measured within tolerance plus a cell but, per the + revision above, not guaranteed), and determinism. - `test_cli_catchment.py` gains: the default reduces; the reduced line reports vertices, area and the difference; `--outline-tolerance 0` gives - the fine ring. + the fine ring less its collinear vertices. ## Acceptance: Bygdin, end to end @@ -787,7 +804,8 @@ Run by `@perf` after PR 2 is green, recorded under NVE's polygon (fetched by the URL, not committed), both ways. 3. `rasputin mesh --dem ... --domain bygdin.geojson --tolerance 1` and `--tolerance 10`, each with `--stats`: triangles, vertices, time. And once - at `--tolerance 10` with the fine outline (`--outline-tolerance 0`), to + at `--tolerance 10` with the unreduced outline (`--outline-tolerance 0`, + the fine ring less its collinear vertices), to show what the reduction saves, which is Ola's first requirement. 4. A picture is optional; the `.vtk` opens in ParaView. diff --git a/docs/retrospectives/next.md b/docs/retrospectives/next.md index a525c502..2f1f7a27 100644 --- a/docs/retrospectives/next.md +++ b/docs/retrospectives/next.md @@ -76,3 +76,12 @@ with `/`) waits for a `@tester` red step. 2026-09-29, night: `@architect` built an uncommitted Python prototype of the 22 design to check its area against NVE (304.91 against 305.54 km²). Useful evidence, but it is implementation work in the design step. + +2026-09-29, night: `@developer` changed the window rule during the 22 PR 1 +green step (the design's rule never stopped early) without stopping to hand +back; the design amendment and the pinning test came after the code. The +fix was right and disclosed, but the order was code first. + +2026-09-29, night: `@developer` added user-visible behaviour (lake refusals +reported under `--lakes`) in a review-fix commit with no failing test first; +`@reviewer` caught it and `@tester` pinned it afterwards. diff --git a/include/terrain/vector_simplify/area_collapse.hpp b/include/terrain/vector_simplify/area_collapse.hpp new file mode 100644 index 00000000..f4d51d95 --- /dev/null +++ b/include/terrain/vector_simplify/area_collapse.hpp @@ -0,0 +1,341 @@ +#pragma once + +// Area-preserving segment collapse of a simple ring to a horizontal tolerance +// (docs/increments/22-auto-catchment.md, "The reduction"; after Kronenfeld, +// Stanislawski, Buttenfield and Brockmeyer 2020, APSC). Each collapse replaces +// the two vertices B, C of a chain A-B-C-D by one point E on the line where +// A-E-D encloses the area A-B-C-D did, so the ring's area is kept up to +// rounding. A collapse is made only if every fine vertex it stands for stays +// within the tolerance of the new edges, E within it of the fine ring, the +// ring stays simple (exact kernel) and every keep-point stays inside. +// +// Locality: every collapse reads four vertices, their fine ranges and a grid +// query; the heap is global and the loop serial, ordered by (deviation, id), +// so the output depends on nothing but the input. + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace terrain::vector_simplify { + +enum class ReduceStatus : std::uint8_t { Ok, InvalidTolerance, NotCounterClockwise, TooFewVertices }; + +struct ReduceCounts { + std::size_t collinear{}; // vertices dropped by the collinear pass + std::size_t collapses{}; // B, C -> E replacements made + std::size_t rejected_crossing{}, rejected_seed{}, rejected_tolerance{}; +}; + +struct ReduceOutcome { + std::vector ring; // open: the first vertex not repeated + ReduceStatus status{ReduceStatus::Ok}; + ReduceCounts counts{}; +}; + +namespace detail { + +[[nodiscard]] inline double segment_distance(const Point2& p, const Point2& a, const Point2& b) { + const Point2 d = b - a; + const double len2 = dot(d, d); + const double t = len2 > 0.0 ? std::clamp(dot(p - a, d) / len2, 0.0, 1.0) : 0.0; + const Point2 q = a + t * d; + return std::hypot(p.x - q.x, p.y - q.y); +} + +// The current ring: a doubly linked list over nodes. Edge u -> next[u] stands +// for `count[u]` fine vertices starting at fine index `start[u]`. +struct Ring { + std::vector p; + std::vector prev, next, start, count, id; + std::vector version; + std::vector alive; + + std::size_t add(Point2 q, std::size_t ident) { + p.push_back(q); + prev.push_back(0); + next.push_back(0); + start.push_back(0); + count.push_back(0); + id.push_back(ident); + version.push_back(0); + alive.push_back(true); + return p.size() - 1; + } +}; + +// Current edges by grid bucket, keyed by their start node. Entries are never +// removed: a stale one is filtered by the caller against the live ring. +class EdgeGrid { +public: + EdgeGrid(double side, Point2 origin) : side_{side}, origin_{origin} {} + + void insert(std::size_t u, const Point2& a, const Point2& b) { + each_cell(a, b, a, [&](std::uint64_t key) { cells_[key].push_back(u); }); + } + + template + void query(const Point2& a, const Point2& b, const Point2& c, F&& f) const { + each_cell(a, b, c, [&](std::uint64_t key) { + if (const auto it = cells_.find(key); it != cells_.end()) + for (const std::size_t u : it->second) + f(u); + }); + } + +private: + template + void each_cell(const Point2& a, const Point2& b, const Point2& c, F&& f) const { + const auto cell = [&](double v, double o) { + return static_cast(std::floor((v - o) / side_)); + }; + const auto x0 = cell(std::min({a.x, b.x, c.x}), origin_.x); + const auto x1 = cell(std::max({a.x, b.x, c.x}), origin_.x); + const auto y0 = cell(std::min({a.y, b.y, c.y}), origin_.y); + const auto y1 = cell(std::max({a.y, b.y, c.y}), origin_.y); + for (auto ix = x0; ix <= x1; ++ix) + for (auto iy = y0; iy <= y1; ++iy) + f((static_cast(ix) << 32) ^ static_cast(iy & 0xffffffff)); + } + + double side_; + Point2 origin_; + std::unordered_map> cells_; +}; + +struct Candidate { + Point2 e; + std::size_t split; // fine vertices [0, split) of the range go to A-E, the rest to E-D + double deviation; +}; + +// The deviation of placing E for the chain whose range is `fine` from `s`, +// `n` vertices long: the best split of the range between A-E and E-D, and E's +// distance to the fine segments in the range. +[[nodiscard]] inline Candidate deviation_of(std::span fine, std::size_t s, + std::size_t n, const Point2& a, const Point2& e, + const Point2& d) { + const std::size_t size = fine.size(); + const auto at = [&](std::size_t k) -> const Point2& { return fine[(s + k) % size]; }; + std::vector suffix(n + 1, 0.0); + for (std::size_t k = n; k-- > 0;) + suffix[k] = std::max(suffix[k + 1], segment_distance(at(k), e, d)); + Candidate best{e, 0, suffix[0]}; + double prefix = 0.0; + for (std::size_t m = 1; m <= n; ++m) { + prefix = std::max(prefix, segment_distance(at(m - 1), a, e)); + if (const double v = std::max(prefix, suffix[m]); v < best.deviation) + best = Candidate{e, m, v}; + } + double to_fine = std::numeric_limits::infinity(); + for (std::size_t k = 0; k < std::max(n, 1); ++k) + to_fine = std::min(to_fine, segment_distance(e, at(k), at(k + 1))); + best.deviation = std::max(best.deviation, to_fine); + return best; +} + +} // namespace detail + +template +[[nodiscard]] ReduceOutcome reduce_ring(std::span fine, double tolerance, + std::span keep) { + using pred::Orientation; + ReduceOutcome out; + if (!std::isfinite(tolerance) || tolerance < 0.0) { + out.status = ReduceStatus::InvalidTolerance; + return out; + } + const std::size_t n = fine.size(); + if (n < 4) { + out.status = ReduceStatus::TooFewVertices; + return out; + } + double twice = 0.0; + for (std::size_t i = 0; i < n; ++i) + twice += cross(fine[i] - fine[0], fine[(i + 1) % n] - fine[0]); + if (!(twice > 0.0)) { + out.status = ReduceStatus::NotCounterClockwise; + return out; + } + + // 1. The collinear pass: drop every vertex exactly between its neighbours. + std::vector kept; + for (std::size_t i = 0; i < n; ++i) { + const Point2& a = fine[(i + n - 1) % n]; + const Point2& b = fine[(i + 1) % n]; + const bool between = std::min(a.x, b.x) <= fine[i].x && fine[i].x <= std::max(a.x, b.x) + && std::min(a.y, b.y) <= fine[i].y && fine[i].y <= std::max(a.y, b.y); + if (K::orient2d(a, fine[i], b) != Orientation::Collinear || !between) + kept.push_back(i); + } + out.counts.collinear = n - kept.size(); + detail::Ring ring; + for (std::size_t k = 0; k < kept.size(); ++k) { + const std::size_t u = ring.add(fine[kept[k]], kept[k]); + ring.start[u] = kept[k]; + ring.count[u] = (kept[(k + 1) % kept.size()] + n - kept[k]) % n; + } + const std::size_t m0 = kept.size(); + for (std::size_t u = 0; u < m0; ++u) { + ring.next[u] = (u + 1) % m0; + ring.prev[u] = (u + m0 - 1) % m0; + } + std::size_t size = m0, next_id = n; + const auto emit = [&] { + std::size_t u = 0; + while (!ring.alive[u]) + ++u; + for (std::size_t k = 0; k < size; ++k, u = ring.next[u]) + out.ring.push_back(ring.p[u]); + return out; + }; + if (tolerance == 0.0 || size <= 4) + return emit(); + + // The grid of current edges: bucket side the tolerance, at least the mean + // fine edge, so a query meets few buckets. + double perimeter = 0.0; + Point2 low = fine[0]; + for (std::size_t i = 0; i < n; ++i) { + const Point2 d = fine[(i + 1) % n] - fine[i]; + perimeter += std::hypot(d.x, d.y); + low = Point2{std::min(low.x, fine[i].x), std::min(low.y, fine[i].y)}; + } + detail::EdgeGrid grid{std::max(tolerance, perimeter / static_cast(n)), low}; + for (std::size_t u = 0; u < m0; ++u) + grid.insert(u, ring.p[u], ring.p[ring.next[u]]); + + // 2.-4. Candidates: E on line A-B or on line C-D (ties to A-B), or the foot + // of B-C's midpoint when both are parallel to A-D. + const auto candidate = [&](std::size_t b) -> std::optional { + const std::size_t a = ring.prev[b], c = ring.next[b], d = ring.next[c]; + const Point2 A = ring.p[a], vb = ring.p[b] - A, vc = ring.p[c] - A, vd = ring.p[d] - A; + const double area = cross(vb, vc) + cross(vc, vd); // A-E-D must enclose this + const std::size_t s = ring.start[a], len = ring.count[a] + ring.count[b] + ring.count[c]; + std::optional best; + const auto consider = [&](Point2 e) { + if (!std::isfinite(e.x) || !std::isfinite(e.y)) + return; + const detail::Candidate cand = detail::deviation_of(fine, s, len, A, A + e, ring.p[d]); + if (!best || cand.deviation < best->deviation) + best = cand; + }; + if (const double bd = cross(vb, vd); bd != 0.0) + consider(vb * (area / bd)); + if (const double cd = cross(vc, vd); cd != 0.0) + consider(vc + (vd - vc) * (1.0 - area / cd)); + if (!best && dot(vd, vd) > 0.0) { + const Point2 mid = (vb + vc) * 0.5; + consider(mid + Point2{-vd.y, vd.x} * ((cross(mid, vd) - area) / dot(vd, vd))); + } + return best; + }; + + using Entry = std::tuple; // dev, id, node, version + std::priority_queue, std::greater<>> heap; + std::vector> pending(m0); + const auto evaluate = [&](std::size_t b) { + ++ring.version[b]; + pending[b] = candidate(b); + if (pending[b] && pending[b]->deviation <= tolerance) + heap.emplace(pending[b]->deviation, ring.id[b], b, ring.version[b]); + else if (pending[b]) + ++out.counts.rejected_tolerance; + }; + for (std::size_t u = 0; u < m0; ++u) + evaluate(u); + + std::vector stamp(m0, 0); + std::size_t round = 0; + while (!heap.empty() && size > 4) { + const auto [dev, ident, b, version] = heap.top(); + heap.pop(); + if (!ring.alive[b] || ring.version[b] != version) + continue; + const std::size_t a = ring.prev[b], c = ring.next[b], d = ring.next[c]; + const detail::Candidate cand = *pending[b]; + const Point2 A = ring.p[a], E = cand.e, D = ring.p[d]; + + // 5. Simple: A-E and E-D against every current edge near them. + const Segment2 ae{A, E}, ed{E, D}; + bool crosses = noding::classify(ae, ed) != noding::SegmentRelation::Touching; + ++round; + grid.query(A, E, D, [&](std::size_t u) { + if (crosses || !ring.alive[u] || stamp[u] == round || u == a || u == b || u == c) + return; + stamp[u] = round; + const std::size_t v = ring.next[u]; + const Segment2 edge{ring.p[u], ring.p[v]}; + const auto want = [&](bool shared) { + return shared ? noding::SegmentRelation::Touching : noding::SegmentRelation::Disjoint; + }; + crosses = noding::classify(ae, edge) != want(v == a) + || noding::classify(ed, edge) != want(u == d); + }); + if (crosses) { + ++out.counts.rejected_crossing; + continue; + } + // Keep-points: winding zero round A-B-C-D-E-A, and on neither new edge. + const std::array loop{A, ring.p[b], ring.p[c], D, E}; + bool lost = false; + for (const Point2& k : keep) { + int winding = 0; + for (std::size_t i = 0; i < 5; ++i) { + const Point2& p = loop[i]; + const Point2& q = loop[(i + 1) % 5]; + const Orientation o = K::orient2d(p, q, k); + winding += p.y <= k.y && q.y > k.y && o == Orientation::CounterClockwise; + winding -= p.y > k.y && q.y <= k.y && o == Orientation::Clockwise; + } + const Segment2 at{k, k}; + lost = lost || winding != 0 + || noding::classify(at, ae) != noding::SegmentRelation::Disjoint + || noding::classify(at, ed) != noding::SegmentRelation::Disjoint; + } + if (lost) { + ++out.counts.rejected_seed; + continue; + } + + // 6. Apply, then re-evaluate the candidates whose four vertices changed. + const std::size_t len = ring.count[a] + ring.count[b] + ring.count[c]; + const std::size_t e = ring.add(E, next_id++); + pending.emplace_back(); + stamp.push_back(0); + ring.alive[b] = ring.alive[c] = false; + ring.next[a] = e; + ring.prev[e] = a; + ring.next[e] = d; + ring.prev[d] = e; + ring.count[a] = cand.split; + ring.start[e] = (ring.start[a] + cand.split) % n; + ring.count[e] = len - cand.split; + grid.insert(a, A, E); + grid.insert(e, E, D); + --size; + ++out.counts.collapses; + if (size > 4) + for (const std::size_t u : {ring.prev[a], a, e, d}) + evaluate(u); + } + return emit(); +} + +} // namespace terrain::vector_simplify diff --git a/project_structure.md b/project_structure.md index 1a60a2df..96b21e8c 100644 --- a/project_structure.md +++ b/project_structure.md @@ -64,14 +64,17 @@ include/terrain/ # public C++ headers, header-only where possible upstream.hpp # upstream(z, seed) -> UpstreamOutcome: one # Priority-Flood labelling the nodes that drain # into the seed set, plus edge/NoData flags (22) + vector_simplify/ + area_collapse.hpp # reduce_ring: area-preserving segment collapse + # (Kronenfeld et al. 2020) to a horizontal + # tolerance, exact crossing tests, keep-points (22) src/ # C++ implementation, one directory per module # (only predicates/ and cdt/ exist; rest planned) predicates/ # exact orient2d/incircle; namespace terrain::pred parallel_util/ # (none: header-only, include/terrain/parallel_util/) - vector_simplify/ # Visvalingam-Whyatt, Douglas-Peucker, topology checks - # (no hydrology/ here: it is header-only, - # include/terrain/hydrology/) + # (no vector_simplify/ or hydrology/ here: both + # are header-only, include/terrain/...) # (no noding/ here, and none planned: the noder is # header-only, its driver a template on the kernel) cdt/ # thin wrapper over vendored Detria @@ -118,8 +121,9 @@ src_python/tin_engine/ # public Python API (distribution name: rasputin) # never imports _core (22) catchment.py # CatchmentRequest -> delineate(request, repo) -> # Catchment: seed, the window loop over 15a's - # plan, _core.upstream and the fine ring; takes a - # DemRepository and no path (22) + # plan, _core.upstream, the fine ring, and + # _core.reduce_ring; takes a DemRepository and + # no path (22) landcover.py # regions, label_triangles: a land-cover code per # triangle, components across unconstrained edges, # one point-in-polygon test per component (16c); @@ -191,9 +195,9 @@ docs/increments/ # per-increment design records; see its README ``` raster ────────────────┐ ▼ -predicates ───────────┬─→ vector_simplify hydrology (raster only) - │ - └─→ noding +predicates ───────────┬─→ noding ──→ vector_simplify hydrology (raster only) + │ (area_collapse uses noding's classify + │ and the predicates' kernel) │ ▼ cdt (thin wrapper over vendored Detria, @@ -352,7 +356,7 @@ Header-only. Two helpers in `chunks.hpp`, both over `std::jthread` created per c ### `vector_simplify` -Visvalingam-Whyatt for area-preserving polygon simplification; Douglas-Peucker for polyline length-preserving; multi-feature topology checks (no introduced crossings). Pre-noding step. +Header-only. `area_collapse.hpp` (increment 22): `reduce_ring`, Kronenfeld, Stanislawski, Buttenfield and Brockmeyer's area-preserving segment collapse (APSC, 2020): each collapse replaces two vertices by one on the line that keeps the area, so the area is kept up to rounding; collapses are taken least deviation first while the fine ring's vertices stay within a horizontal tolerance; each new edge is tested for crossings with `noding::classify` over a uniform grid, and keep-points (the seed) must stay inside. Serial and deterministic. Visvalingam-Whyatt and Douglas-Peucker are not used: neither keeps area. See `docs/increments/22-auto-catchment.md`. ### `hydrology` diff --git a/src_python/tin_engine/_core.pyi b/src_python/tin_engine/_core.pyi index d2663579..9635cbb0 100644 --- a/src_python/tin_engine/_core.pyi +++ b/src_python/tin_engine/_core.pyi @@ -471,3 +471,34 @@ class UpstreamOutcome: def upstream(view: RasterView, seed: npt.ArrayLike) -> UpstreamOutcome: """Every node draining into a seed of the ``(rows, cols)`` mask (Priority-Flood); another shape is a ``ValueError``. Releases the GIL.""" + +class ReduceStatus(Enum): + """Why :func:`reduce_ring` refused, or ``Ok``.""" + + Ok = 0 + InvalidTolerance = 1 + NotCounterClockwise = 2 + TooFewVertices = 3 + +@final +class ReduceOutcome: + """What :func:`reduce_ring` returned: the ring, open, a status and counts.""" + + @property + def ring(self) -> npt.NDArray[np.float64]: ... + @property + def status(self) -> ReduceStatus: ... + @property + def collinear(self) -> int: ... + @property + def collapses(self) -> int: ... + @property + def rejected_crossing(self) -> int: ... + @property + def rejected_seed(self) -> int: ... + @property + def rejected_tolerance(self) -> int: ... + +def reduce_ring(ring: npt.ArrayLike, tolerance: float, keep: npt.ArrayLike) -> ReduceOutcome: + """Reduce an open counter-clockwise ``(N, 2)`` ring to ``tolerance``, keeping + its area and the ``(K, 2)`` keep-points inside. Releases the GIL.""" diff --git a/src_python/tin_engine/catchment.py b/src_python/tin_engine/catchment.py index deada36d..c33fffcf 100644 --- a/src_python/tin_engine/catchment.py +++ b/src_python/tin_engine/catchment.py @@ -1,11 +1,12 @@ -"""The catchment of a lake, from the DEM (increment 22, PR 1: the fine outline). +"""The catchment of a lake, from the DEM, as a reduced outline (increment 22). `docs/increments/22-auto-catchment.md`, "Data flow", "The seed", "The window" and "The fine outline". :func:`delineate` moves the seed point and the lake into the DEM's CRS, plans and assembles a window round them (15a), floods it (`_core.upstream`), grows the window until the catchment lies clearly inside it or is cut by the data's edge or NoData, and traces the outline of -the in-nodes, holes filled and other pieces dropped. +the in-nodes, holes filled and other pieces dropped. The fine outline is then +reduced to the request's tolerance by `_core.reduce_ring`, keeping its area. No paths: the DEM comes through a `DemRepository`, and the lakes arrive as shapely geometries the CLI read. Blocking (the flood releases the GIL); an @@ -16,7 +17,7 @@ import math import time -from dataclasses import dataclass +from dataclasses import dataclass, replace from typing import Any, Self import numpy as np @@ -25,7 +26,7 @@ from pydantic import BaseModel, ConfigDict, model_validator from shapely.geometry import Point, Polygon -from tin_engine._core import UpstreamOutcome, upstream +from tin_engine._core import ReduceStatus, UpstreamOutcome, reduce_ring, upstream from tin_engine.crs import crs_label, reprojector from tin_engine.io.models import RasterMeta from tin_engine.io.repository import DemRepository @@ -50,6 +51,11 @@ class CatchmentError(ValueError): """A catchment that cannot be delineated, in words for the person asking.""" +class LakeError(CatchmentError): + """The lakes cannot seed it: the point in no lake or in two, or a lake + that does not move into the DEM's CRS.""" + + class CatchmentRequest(BaseModel): """The seed point in `seed_crs`, and the lakes (shapely polygons or multipolygons in `lakes_crs`) of which the one containing it is the seed. @@ -61,11 +67,17 @@ class CatchmentRequest(BaseModel): seed_crs: str = "EPSG:4326" lakes: tuple[Any, ...] | None = None lakes_crs: str | None = None + #: Metres the reduced outline may stray from the fine one; None is twice + #: the DEM's cell, 0 keeps the fine outline less its collinear vertices. + outline_tolerance: float | None = None @model_validator(mode="after") def _lakes_have_a_crs(self) -> Self: if self.lakes is not None and self.lakes_crs is None: raise ValueError("lakes need their CRS") + t = self.outline_tolerance + if t is not None and not (math.isfinite(t) and t >= 0.0): + raise ValueError(f"the outline tolerance must be finite and at least 0, got {t}") return self @@ -86,7 +98,11 @@ class Catchment: """The fine outline (holes filled) in the DEM's CRS `crs`, the seed point there, the lake's area (None without one), node counts, the outline's area and what was left out of it, every window, and the last window's - mask of in-nodes with its `meta`.""" + mask of in-nodes with its `meta`. Then the reduced outline `reduced` (the + CLI's `reduced_vertices` and `reduced_area_m2` are its vertex count and + area), the `tolerance` in metres it was reduced to (`outline_tolerance_m`; + 0 keeps the fine outline less its collinear vertices), and the seconds the + trace and the reduction took.""" fine: Polygon crs: str @@ -103,6 +119,9 @@ class Catchment: mask: npt.NDArray[np.uint8] meta: RasterMeta trace_seconds: float + reduced: Polygon + tolerance: float + reduce_seconds: float def delineate(request: CatchmentRequest, repository: DemRepository) -> Catchment: @@ -154,7 +173,9 @@ def delineate(request: CatchmentRequest, repository: DemRepository) -> Catchment if out.touches_edge: sides = _edge_sides(m, out) raise CatchmentError(f"the catchment is cut by the data's {' and '.join(sides)} edge") - return _outline(dem_crs, lake, (float(x), float(y)), out, m, seed, tuple(windows)) + fine = _outline(dem_crs, lake, (float(x), float(y)), out, m, seed, tuple(windows)) + tolerance = request.outline_tolerance + return _reduced(fine, 2.0 * max(m.delta_x, m.delta_y) if tolerance is None else tolerance) def _lake(request: CatchmentRequest, dem_crs: str) -> Polygon | None: @@ -166,13 +187,13 @@ def _lake(request: CatchmentRequest, dem_crs: str) -> Polygon | None: found = [p for g in request.lakes for p in shapely.get_parts(g) if p.contains(point)] where = f"({px!r}, {py!r}) in {request.lakes_crs}" if not found: - raise CatchmentError(f"the seed point {where} is in no lake") + raise LakeError(f"the seed point {where} is in no lake") if len(found) > 1: - raise CatchmentError(f"the seed point {where} is in {len(found)} lakes; give one") + raise LakeError(f"the seed point {where} is in {len(found)} lakes; give one") move = reprojector(request.lakes_crs, dem_crs) lake = shapely.transform(found[0], move) if not np.isfinite(shapely.get_coordinates(lake)).all(): - raise CatchmentError(f"the lake at {where} has a vertex with no image in {dem_crs}") + raise LakeError(f"the lake at {where} has a vertex with no image in {dem_crs}") assert isinstance(lake, Polygon) return lake @@ -307,6 +328,24 @@ def _outline( mask=mask, meta=m, trace_seconds=time.perf_counter() - t0, + reduced=fine, + tolerance=0.0, + reduce_seconds=0.0, + ) + + +def _reduced(fine: Catchment, tolerance: float) -> Catchment: + """The fine outline reduced to `tolerance` by `_core.reduce_ring`, in + metres from its lower-left corner, the seed point kept inside.""" + origin = np.array(fine.fine.bounds[:2]) + ring = np.asarray(fine.fine.exterior.coords)[:-1] - origin + t0 = time.perf_counter() + out = reduce_ring(ring, tolerance, np.array([fine.seed]) - origin) + if out.status != ReduceStatus.Ok: + raise CatchmentError(f"the outline could not be reduced: {out.status.name}") + reduced = Polygon(np.asarray(out.ring) + origin) + return replace( + fine, reduced=reduced, tolerance=tolerance, reduce_seconds=time.perf_counter() - t0 ) diff --git a/src_python/tin_engine/cli.py b/src_python/tin_engine/cli.py index 197443d3..16612231 100644 --- a/src_python/tin_engine/cli.py +++ b/src_python/tin_engine/cli.py @@ -69,7 +69,7 @@ sample, triangulate, ) -from tin_engine.catchment import CatchmentRequest, delineate +from tin_engine.catchment import CatchmentRequest, LakeError, delineate from tin_engine.chains import start_chains from tin_engine.crs import crs_label, parse_crs, transform_description from tin_engine.dem_input import DemInput, DemRequest, open_dem, repository_for @@ -1377,17 +1377,32 @@ def catchment( lakes_layer: Annotated[ str | None, typer.Option("--lakes-layer", help="The GeoPackage's features table.") ] = None, + outline_tolerance: Annotated[ + float | None, + typer.Option( + "--outline-tolerance", + metavar="METRES", + help="Reduce the outline, keeping its area, to within this of the fine one; " + "0 writes the fine outline without its collinear vertices. Default: twice the " + "DEM's cell.", + ), + ] = None, out_parent: Annotated[ Path | None, typer.Option("--out-parent", help="Refuse any output path resolving outside this."), ] = None, ) -> None: """Write the catchment of a lake, from the DEM, as a GeoJSON polygon in the - DEM's CRS that ``mesh --domain`` reads (increment 22, PR 1: the fine - outline, drawn between DEM nodes, holes filled). A catchment cut by the - data's edge or by NoData is refused, and nothing is written.""" + DEM's CRS that ``mesh --domain`` reads (increment 22). The fine outline is + drawn between DEM nodes, holes filled, then reduced to ``--outline-tolerance`` + keeping its area, with the seed inside. A catchment cut by the data's edge + or by NoData is refused, and nothing is written.""" if lakes is None and lakes_layer is not None: raise typer.BadParameter("applies only with --lakes", param_hint="--lakes-layer") + if outline_tolerance is not None and not ( + math.isfinite(outline_tolerance) and outline_tolerance >= 0.0 + ): + raise typer.BadParameter("must be finite and at least 0", param_hint="--outline-tolerance") if out.suffix.lower() not in CATCHMENT_SUFFIXES: raise typer.BadParameter(f"use {' or '.join(CATCHMENT_SUFFIXES)}", param_hint="--out") target = _destination(out, out_parent, out.stem) @@ -1402,18 +1417,20 @@ def catchment( seed_crs=seed_crs, lakes=None if found is None else found[0], lakes_crs=None if found is None else found[1], + outline_tolerance=outline_tolerance, ) result = delineate(request, repository) except OSError as exc: raise typer.BadParameter(f"cannot read {exc.filename}: {exc}", param_hint="--dem") from exc except ValueError as exc: - raise typer.BadParameter(_words(exc), param_hint="--dem") from exc + hint = "--lakes" if isinstance(exc, LakeError) else "--dem" + raise typer.BadParameter(_words(exc), param_hint=hint) from exc for k, w in enumerate(result.windows, 1): b = w.bounds typer.echo( f"window {k}: x {b.x_min:.0f}-{b.x_max:.0f}, y {b.y_min:.0f}-{b.y_max:.0f}, " f"{w.rows} x {w.cols} nodes, flood {w.seconds:.2f} s, " - + (f"grown: touches {', '.join(w.grown)}" if w.grown else "contained"), + + (f"grown on {', '.join(w.grown)}" if w.grown else "contained"), err=True, ) cell = result.meta.delta_x * result.meta.delta_y @@ -1439,12 +1456,24 @@ def catchment( f"traced in {result.trace_seconds:.2f} s", err=True, ) + reduced = result.reduced + kept = len(reduced.exterior.coords) - 1 + change = reduced.area - result.fine_area + typer.echo( + f"reduced outline: {kept} vertices, {reduced.area / 1e6:.6f} km2, difference " + f"{change:.3g} m2 ({change / result.fine_area:.2g} relative), tolerance " + f"{result.tolerance:g} m, {result.reduce_seconds:.2f} s", + err=True, + ) properties = { "seed": list(seed), "seed_crs": seed_crs, "nodes": result.nodes, "fine_vertices": vertices, "fine_area_m2": result.fine_area, + "reduced_vertices": kept, + "reduced_area_m2": reduced.area, + "outline_tolerance_m": result.tolerance, "windows": [[w.rows, w.cols] for w in result.windows], } doc = { @@ -1456,7 +1485,7 @@ def catchment( "properties": properties, "geometry": { "type": "Polygon", - "coordinates": [[list(xy) for xy in result.fine.exterior.coords]], + "coordinates": [[list(xy) for xy in reduced.exterior.coords]], }, } ], diff --git a/tests/cpp/CMakeLists.txt b/tests/cpp/CMakeLists.txt index 57e27d1a..8b7415d3 100644 --- a/tests/cpp/CMakeLists.txt +++ b/tests/cpp/CMakeLists.txt @@ -317,3 +317,9 @@ add_terrain_backend_test(prop_noding_verifier_sweep property/prop_noding_verifie # "The red suites"). Not named invariant-critical; the descent oracle and the # invariants on random DEMs are the defence. It starts no threads. add_terrain_test(test_hydrology_upstream unit/test_hydrology_upstream.cpp) + +# The outline reduction, increment 22 PR 2 (docs/increments/22-auto-catchment.md, +# "The reduction" and "The red suites"): area-preserving segment collapse with +# the exact kernel. A backend test because it names DefaultKernel. Not named +# invariant-critical; it starts no threads. +add_terrain_backend_test(test_area_collapse unit/test_area_collapse.cpp) diff --git a/tests/cpp/unit/test_area_collapse.cpp b/tests/cpp/unit/test_area_collapse.cpp new file mode 100644 index 00000000..948d2949 --- /dev/null +++ b/tests/cpp/unit/test_area_collapse.cpp @@ -0,0 +1,390 @@ +// Increment 22, PR 2 (docs/increments/22-auto-catchment.md, "The reduction" +// and "The red suites", PR 2 C++): the fine outline reduced by +// area-preserving segment collapse (Kronenfeld et al. 2020) to a horizontal +// tolerance, simple and with the seed kept inside. +// +// The design names this file tests/cpp/unit/area_collapse.cpp; it carries the +// test_ prefix every other suite in tests/cpp/unit/ has. +// +// Interface assumed (the design fixes the header, the function, the statuses +// and which counts exist; the spelling is chosen here and stated in the +// handback): +// +// #include +// namespace terrain::vector_simplify { +// enum class ReduceStatus : std::uint8_t { +// Ok, InvalidTolerance, NotCounterClockwise, TooFewVertices }; +// struct ReduceCounts { +// std::size_t collinear; // vertices dropped by the collinear pass +// std::size_t collapses; // B, C -> E replacements made +// std::size_t rejected_crossing, rejected_seed, rejected_tolerance; +// }; +// struct ReduceOutcome { +// std::vector ring; // open: the first vertex not repeated +// ReduceStatus status; +// ReduceCounts counts; +// }; +// template +// ReduceOutcome reduce_ring(std::span ring, double tolerance, +// std::span keep); +// } +// +// The input ring is open too. On a status other than Ok the ring's content is +// not pinned. +// +// THE CHECKS ARE THE DESIGN'S GUARANTEES, computed here from the output and +// the input alone: area to 1e-9 relative; simple by a brute-force pairwise +// classify (non-adjacent edges Disjoint, adjacent ones +// Touching); keep-points strictly inside by an exact winding number; every +// input vertex within the tolerance of the output ring and every output +// vertex within it of the input ring; the same input twice gives equal bits. +// Plus the bookkeeping identity: input size - collinear - collapses = output +// size (each collapse replaces two vertices by one). + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using terrain::Point2; +using terrain::Segment2; +using terrain::noding::classify; +using terrain::noding::SegmentRelation; +using terrain::pred::DefaultKernel; +using terrain::pred::Orientation; +using terrain::vector_simplify::reduce_ring; +using terrain::vector_simplify::ReduceOutcome; +using terrain::vector_simplify::ReduceStatus; + +namespace { + +using Ring = std::vector; + +ReduceOutcome reduce(const Ring& ring, double tolerance, const Ring& keep = {}) { + return reduce_ring(std::span{ring}, tolerance, + std::span{keep}); +} + +double area(const Ring& r) { + double twice = 0.0; + for (std::size_t i = 0; i < r.size(); ++i) { + const Point2& a = r[i]; + const Point2& b = r[(i + 1) % r.size()]; + twice += a.x * b.y - b.x * a.y; + } + return 0.5 * twice; +} + +Segment2 edge(const Ring& r, std::size_t i) { return Segment2{r[i], r[(i + 1) % r.size()]}; } + +// Brute force, exact: adjacent edges share only their vertex, others nothing. +bool simple(const Ring& r) { + const std::size_t n = r.size(); + if (n < 3) + return false; + for (std::size_t i = 0; i < n; ++i) + for (std::size_t j = i + 1; j < n; ++j) { + const bool adjacent = j == i + 1 || (i == 0 && j == n - 1); + const SegmentRelation rel = classify(edge(r, i), edge(r, j)); + if (adjacent ? rel != SegmentRelation::Touching : rel != SegmentRelation::Disjoint) + return false; + } + return true; +} + +// Strictly inside: on no edge, and an exact winding number of 1. +bool strictly_inside(const Point2& p, const Ring& r) { + int winding = 0; + for (std::size_t i = 0; i < r.size(); ++i) { + const Point2& a = r[i]; + const Point2& b = r[(i + 1) % r.size()]; + if (classify(Segment2{p, p}, Segment2{a, b}) != SegmentRelation::Disjoint) + return false; + const Orientation o = DefaultKernel::orient2d(a, b, p); + if (a.y <= p.y && b.y > p.y && o == Orientation::CounterClockwise) + ++winding; + if (a.y > p.y && b.y <= p.y && o == Orientation::Clockwise) + --winding; + } + return winding == 1; +} + +double distance(const Point2& p, const Point2& a, const Point2& b) { + const double dx = b.x - a.x, dy = b.y - a.y; + const double len2 = dx * dx + dy * dy; + double t = len2 > 0.0 ? ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2 : 0.0; + t = std::clamp(t, 0.0, 1.0); + return std::hypot(p.x - (a.x + t * dx), p.y - (a.y + t * dy)); +} + +double distance(const Point2& p, const Ring& r) { + double best = std::numeric_limits::infinity(); + for (std::size_t i = 0; i < r.size(); ++i) + best = std::min(best, distance(p, r[i], r[(i + 1) % r.size()])); + return best; +} + +// Every guarantee of the design, on one call. +void check_guarantees(const Ring& fine, double tolerance, const Ring& keep, const ReduceOutcome& out) { + REQUIRE(out.status == ReduceStatus::Ok); + const Ring& red = out.ring; + REQUIRE(red.size() >= 3); + CHECK(fine.size() - out.counts.collinear - out.counts.collapses == red.size()); + const double a0 = area(fine); + CHECK(std::abs(area(red) - a0) <= 1e-9 * a0); + CHECK(area(red) > 0.0); // still counter-clockwise + CHECK(simple(red)); + for (const Point2& k : keep) + CHECK(strictly_inside(k, red)); + const double slack = 1e-9 * std::max(1.0, tolerance); + for (const Point2& p : fine) + CHECK(distance(p, red) <= tolerance + slack); + for (const Point2& p : red) + CHECK(distance(p, fine) <= tolerance + slack); + for (const Point2& p : red) + CHECK((std::isfinite(p.x) && std::isfinite(p.y))); +} + +bool same_bits(const Ring& a, const Ring& b) { + return a.size() == b.size() + && std::equal(a.begin(), a.end(), b.begin(), [](const Point2& p, const Point2& q) { + return std::bit_cast(p.x) == std::bit_cast(q.x) + && std::bit_cast(p.y) == std::bit_cast(q.y); + }); +} + +// The fine outline of a w x h block of nodes (cell `cell`, first node at +// `origin`), as the tracer draws it: a vertex at the midpoint of every lattice +// edge leaving the block, counter-clockwise. 2 (w + h) vertices, of which all +// but the 8 corner-cutting ones are collinear; area (w h - 1/2) cells. +Ring block(std::size_t w, std::size_t h, double cell = 10.0, Point2 origin = {1000.0, 2000.0}) { + Ring r; + const auto at = [&](double i, double j) { return Point2{origin.x + i * cell, origin.y + j * cell}; }; + const double W = static_cast(w), H = static_cast(h); + for (std::size_t i = 0; i < w; ++i) + r.push_back(at(static_cast(i), -0.5)); + for (std::size_t j = 0; j < h; ++j) + r.push_back(at(W - 0.5, static_cast(j))); + for (std::size_t i = w; i-- > 0;) + r.push_back(at(static_cast(i), H - 0.5)); + for (std::size_t j = h; j-- > 0;) + r.push_back(at(-0.5, static_cast(j))); + return r; +} + +// The same up to where the cycle starts. +bool same_cycle(const Ring& a, const Ring& b) { + if (a.size() != b.size()) + return false; + for (std::size_t s = 0; s < a.size(); ++s) { + bool all = true; + for (std::size_t i = 0; i < a.size() && all; ++i) + all = a[(s + i) % a.size()] == b[i]; + if (all) + return true; + } + return a.empty(); +} + +Ring corners_of(const Ring& r) { + Ring out; + for (std::size_t i = 0; i < r.size(); ++i) { + const Point2& a = r[(i + r.size() - 1) % r.size()]; + const Point2& b = r[(i + 1) % r.size()]; + if (DefaultKernel::orient2d(a, r[i], b) != Orientation::Collinear) + out.push_back(r[i]); + } + return out; +} + +// A random star polygon round `centre`: sorted distinct angles, radii on a +// half-metre grid, so it is simple and the centre is strictly inside. +Ring star(std::mt19937& rng, std::size_t n, Point2 centre = {5000.0, 7000.0}) { + std::uniform_int_distribution radius(100, 300); + std::vector ticks(3600); + std::iota(ticks.begin(), ticks.end(), 0); + std::shuffle(ticks.begin(), ticks.end(), rng); + ticks.resize(n); + std::sort(ticks.begin(), ticks.end()); + Ring r; + for (int t : ticks) { + const double angle = 2.0 * std::numbers::pi * t / 3600.0; + const double rho = 0.5 * radius(rng); + r.push_back(Point2{std::round(centre.x + rho * std::cos(angle)), + std::round(centre.y + rho * std::sin(angle))}); + } + // Rounding may make three consecutive vertices collinear or two equal on + // a small star; the fixture keeps only simple, counter-clockwise ones. + return r; +} + +} // namespace + +TEST_CASE("status: an invalid tolerance, a clockwise ring, fewer than 4 vertices", + "[vector_simplify][area_collapse][status]") { + const Ring ring = block(5, 4); + CHECK(reduce(ring, -1.0).status == ReduceStatus::InvalidTolerance); + CHECK(reduce(ring, std::numeric_limits::quiet_NaN()).status + == ReduceStatus::InvalidTolerance); + CHECK(reduce(ring, std::numeric_limits::infinity()).status + == ReduceStatus::InvalidTolerance); + CHECK(reduce(ring, -std::numeric_limits::infinity()).status + == ReduceStatus::InvalidTolerance); + + Ring clockwise(ring.rbegin(), ring.rend()); + CHECK(reduce(clockwise, 10.0).status == ReduceStatus::NotCounterClockwise); + + const Ring triangle{{0.0, 0.0}, {10.0, 0.0}, {0.0, 10.0}}; + CHECK(reduce(triangle, 10.0).status == ReduceStatus::TooFewVertices); + CHECK(reduce(Ring{{0.0, 0.0}, {10.0, 0.0}}, 10.0).status == ReduceStatus::TooFewVertices); + CHECK(reduce(Ring{}, 10.0).status == ReduceStatus::TooFewVertices); + + const Ring square{{0.0, 0.0}, {10.0, 0.0}, {10.0, 10.0}, {0.0, 10.0}}; + CHECK(reduce(square, 10.0).status == ReduceStatus::Ok); +} + +TEST_CASE("tolerance 0 drops only the exactly collinear vertices", "[vector_simplify][area_collapse][zero]") { + const Ring ring = block(20, 10); + REQUIRE(ring.size() == 60); + const ReduceOutcome out = reduce(ring, 0.0); + REQUIRE(out.status == ReduceStatus::Ok); + CHECK(out.counts.collinear == 52); + CHECK(out.counts.collapses == 0); + CHECK(same_cycle(out.ring, corners_of(ring))); + CHECK(out.ring.size() == 8); + CHECK(area(out.ring) == area(ring)); + + // A ring with no collinear vertex comes back as it went in. + const Ring square{{0.0, 0.0}, {10.0, 0.0}, {10.0, 10.0}, {5.0, 12.5}, {0.0, 10.0}}; + const ReduceOutcome same = reduce(square, 0.0); + REQUIRE(same.status == ReduceStatus::Ok); + CHECK(same_cycle(same.ring, square)); + CHECK(same.counts.collinear == 0); +} + +TEST_CASE("a traced rectangle of nodes reduces to at most 8 vertices at one cell, area unchanged", + "[vector_simplify][area_collapse][rectangle]") { + constexpr std::array, 4> shapes{{{20, 10}, {3, 17}, {2, 2}, {40, 40}}}; + for (const auto& [w, h] : shapes) { + const Ring ring = block(w, h); + const ReduceOutcome out = reduce(ring, 10.0); + INFO(w << " x " << h); + check_guarantees(ring, 10.0, {}, out); + CHECK(out.ring.size() <= 8); + CHECK(out.ring.size() >= 4); + } +} + +TEST_CASE("a keep-point half a cell inside a tab a collapse would cut off stays inside", + "[vector_simplify][area_collapse][keep]") { + // A 200 m square with a tab one cell (10 m) wide and 1.5 cells tall on its + // top side, densified at 10 m so there is plenty to collapse. At a + // tolerance of 5 cells the tab is well within reach of a collapse; the + // keep-point is half a cell inside the tab on every side. + Ring ring; + for (int x = 0; x < 200; x += 10) + ring.push_back({static_cast(x), 0.0}); + for (int y = 0; y < 200; y += 10) + ring.push_back({200.0, static_cast(y)}); + for (int x = 200; x > 110; x -= 10) + ring.push_back({static_cast(x), 200.0}); + ring.push_back({110.0, 200.0}); + ring.push_back({110.0, 215.0}); + ring.push_back({100.0, 215.0}); + ring.push_back({100.0, 200.0}); + for (int x = 90; x > 0; x -= 10) + ring.push_back({static_cast(x), 200.0}); + for (int y = 200; y > 0; y -= 10) + ring.push_back({0.0, static_cast(y)}); + REQUIRE(simple(ring)); + REQUIRE(area(ring) > 0.0); + + const Ring keep{{105.0, 210.0}}; + REQUIRE(strictly_inside(keep[0], ring)); + const ReduceOutcome out = reduce(ring, 50.0, keep); + check_guarantees(ring, 50.0, keep, out); +} + +TEST_CASE("a thin corridor at a tolerance of five cells does not cross itself", + "[vector_simplify][area_collapse][corridor]") { + // Two 100 m squares joined by a corridor 300 m long whose sides are 1.5 + // cells (15 m) apart, each side a zigzag of 2.5 m so every vertex is a + // candidate. A collapse on one side that reached the other would cross. + Ring ring{{0.0, 0.0}, {100.0, 0.0}}; + for (int k = 0; k <= 30; ++k) + ring.push_back({100.0 + 10.0 * k, 42.5 + 2.5 * (k % 2)}); + ring.push_back({400.0, 0.0}); + ring.push_back({500.0, 0.0}); + ring.push_back({500.0, 100.0}); + ring.push_back({400.0, 100.0}); + for (int k = 30; k >= 0; --k) + ring.push_back({100.0 + 10.0 * k, 57.5 - 2.5 * (k % 2)}); + ring.push_back({100.0, 100.0}); + ring.push_back({0.0, 100.0}); + REQUIRE(simple(ring)); + REQUIRE(area(ring) > 0.0); + + const Ring keep{{50.0, 50.0}, {450.0, 50.0}}; + const ReduceOutcome out = reduce(ring, 50.0, keep); + check_guarantees(ring, 50.0, keep, out); + CHECK(out.ring.size() < ring.size()); +} + +TEST_CASE("the same input twice gives equal bits", "[vector_simplify][area_collapse][determinism]") { + std::mt19937 rng{22201}; + for (int trial = 0; trial < 20; ++trial) { + const Ring ring = star(rng, 40); + if (!simple(ring) || !(area(ring) > 0.0)) + continue; + const Ring keep{{5000.0, 7000.0}}; + const ReduceOutcome a = reduce(ring, 20.0, keep); + const ReduceOutcome b = reduce(ring, 20.0, keep); + CHECK(a.status == b.status); + CHECK(same_bits(a.ring, b.ring)); + CHECK(a.counts.collapses == b.counts.collapses); + } + const Ring rect = block(30, 7); + CHECK(same_bits(reduce(rect, 10.0).ring, reduce(rect, 10.0).ring)); +} + +TEST_CASE("random star polygons keep every guarantee", "[vector_simplify][area_collapse][property]") { + std::mt19937 rng{22202}; + std::uniform_int_distribution size(8, 80); + const double tolerances[] = {0.0, 2.0, 10.0, 30.0, 100.0}; + int checked = 0, reduced = 0; + for (int trial = 0; trial < 300; ++trial) { + const Ring ring = star(rng, size(rng)); + if (!simple(ring) || !(area(ring) > 0.0)) + continue; + const Ring keep{{5000.0, 7000.0}}; + if (!strictly_inside(keep[0], ring)) + continue; + const double tolerance = tolerances[trial % 5]; + const ReduceOutcome out = reduce(ring, tolerance, keep); + INFO("trial " << trial << ", " << ring.size() << " vertices, tolerance " << tolerance); + check_guarantees(ring, tolerance, keep, out); + if (tolerance == 0.0) + CHECK(out.counts.collapses == 0); + ++checked; + reduced += out.counts.collapses > 0 ? 1 : 0; + } + // The fixture must exercise the loop, not only the collinear pass. + CHECK(checked > 200); + CHECK(reduced > 100); +} diff --git a/tests/python/test_cli_catchment.py b/tests/python/test_cli_catchment.py index 2b31c738..8bdf8ae4 100644 --- a/tests/python/test_cli_catchment.py +++ b/tests/python/test_cli_catchment.py @@ -21,6 +21,16 @@ `vertices` and `km`. - Every refusal is a non-zero exit and no file. +PR 2 (the reduction): the default output is the reduced polygon, whose area is +the fine outline's to 1e-9 relative (the design's area guarantee), so the +PR 1 area checks above compare at 1e-9 (amended in the PR 2 red step; at +1e-12 they held only for the fine ring). `--outline-tolerance 0` writes the +fine ring with only its exactly collinear vertices removed. The properties +gain `reduced_vertices`, `reduced_area_m2` and `outline_tolerance_m` (names +chosen here, beside PR 1's `fine_vertices` and `fine_area_m2`). stderr gains +a line with the word `reduced`, its vertices, its area in km and the +difference in m2. + The Bygdin test runs against Ola's data in ../rasputin_data and is skipped when it is absent: the area must be within 2 % of NVE's 305.54 km^2 (delfelt 1187), as the design's acceptance asks. @@ -35,10 +45,23 @@ import numpy as np import pytest -from shapely.geometry import Point +import shapely +from shapely.geometry import Point, Polygon from typer.testing import CliRunner -from catchment_fixtures import EPSG, D, bowl, filled, full_flood, lake_box, lat, seeds_in, tile_of +from catchment_fixtures import ( + EPSG, + X0, + Y0, + D, + bowl, + filled, + full_flood, + lake_box, + lat, + seeds_in, + tile_of, +) from gpkg_fixtures import DTM10, OLA_NORWAY, Layer, Row, write_gpkg from mosaic_fixtures import quadrants from test_catchment import moved, to_4326 @@ -105,7 +128,7 @@ def refused(tmp_path: Path, *args: str, out: str = "c.geojson") -> str: code, output = invoke("catchment", *args, "--out", str(target)) assert code != 0, output # Refused by the command, not by Typer for want of one. - assert "No such command" not in output, output + assert "No such command" not in output and "No such option" not in output, output assert not target.exists() return output @@ -142,7 +165,7 @@ def test_read_domain_reads_it_back_with_the_fine_area( assert parse_crs(domain.crs) == parse_crs(EPSG) assert domain.polygon.is_valid assert len(domain.polygon.interiors) == 0 - assert domain.polygon.area == pytest.approx(expected_area(), rel=1e-12) + assert domain.polygon.area == pytest.approx(expected_area(), rel=1e-9) def test_mesh_accepts_it_as_a_domain(tmp_path: Path, dem_dir: Path, lakes: Path) -> None: @@ -219,7 +242,7 @@ def test_without_lakes_the_seed_is_a_pour_point(tmp_path: Path, dem_dir: Path) - seed = np.zeros((400, 200), dtype=np.uint8) seed[200, 100] = 1 area = square_area(filled(full_flood(tile, seed))) * D * D - assert read_domain(out).polygon.area == pytest.approx(area, rel=1e-12) + assert read_domain(out).polygon.area == pytest.approx(area, rel=1e-9) # --------------------------------------------------------------------------- @@ -239,8 +262,45 @@ def test_lakes_layer_without_lakes_is_refused(tmp_path: Path, dem_dir: Path) -> assert "--lakes" in output -def test_a_seed_in_no_lake_is_refused(tmp_path: Path, dem_dir: Path, lakes: Path) -> None: - refused(tmp_path, "--dem", str(dem_dir), *utm_seed(*lat(60, 60)), "--lakes", str(lakes)) +#: Typer's line for a BadParameter with param_hint "--lakes" (6757925, +#: LakeError), as `plain` flattens it. The whole phrase, because the old hint's +#: line, "Invalid value for --dem:", also contains "--lakes" wherever the +#: message quotes the flag. +LAKES_HINT = "Invalid value for --lakes:" +DEM_HINT = "Invalid value for --dem:" + + +def test_a_seed_in_no_lake_is_refused_under_lakes( + tmp_path: Path, dem_dir: Path, lakes: Path +) -> None: + output = refused( + tmp_path, "--dem", str(dem_dir), *utm_seed(*lat(60, 60)), "--lakes", str(lakes) + ) + assert LAKES_HINT in output, output + assert DEM_HINT not in output, output + + +def test_a_seed_in_two_lakes_is_refused_under_lakes(tmp_path: Path, dem_dir: Path) -> None: + doc = { + "type": "FeatureCollection", + "crs": {"type": "name", "properties": {"name": EPSG}}, + "features": [ + { + "type": "Feature", + "properties": {"name": name}, + "geometry": {"type": "Polygon", "coordinates": [list(lake.exterior.coords)]}, + } + for name, lake in ( + ("one", lake_box()), + ("two", lake_box().buffer(D, join_style="mitre")), + ) + ], + } + path = tmp_path / "two.geojson" + path.write_text(json.dumps(doc)) + output = refused(tmp_path, "--dem", str(dem_dir), *utm_seed(), "--lakes", str(path)) + assert LAKES_HINT in output, output + assert DEM_HINT not in output, output def test_a_catchment_cut_by_the_data_edge_writes_nothing(tmp_path: Path) -> None: @@ -281,3 +341,123 @@ def test_bygdin_is_within_two_percent_of_nve(tmp_path: Path) -> None: # The seed is mid-lake, so the outline contains it. ((x, y),) = reprojector("EPSG:4326", EPSG)([[float(v) for v in BYGDIN_SEED]]) assert domain.polygon.contains(Point(x, y)) + + +# --------------------------------------------------------------------------- +# PR 2: the reduction +# --------------------------------------------------------------------------- + + +def feature(out: Path) -> dict[str, Any]: + (f,) = json.loads(out.read_text())["features"] + return dict(f) + + +def vertices(polygon: Any) -> int: + return len(polygon.exterior.coords) - 1 + + +def fine_corners() -> set[tuple[float, float]]: + """The fine outline's vertices less the exactly collinear ones: the + outer ring round the seed of the traced whole-raster catchment. Every + coordinate is a multiple of 50 m, so the collinearity test is exact.""" + from tin_engine.outline import trace + + tile = tile_of(bowl()) + mask = filled(full_flood(tile, seeds_in(tile, lake_box()))) + rings = [np.asarray(r, dtype=np.float64) for r in trace(mask)] + world = [np.column_stack([X0 + r[:, 1] * D, Y0 - r[:, 0] * D]) for r in rings] + (ring,) = [w for w in world if Polygon(w).contains(Point(SEED))] + if np.array_equal(ring[0], ring[-1]): + ring = ring[:-1] + a, b, c = np.roll(ring, 1, axis=0), ring, np.roll(ring, -1, axis=0) + cross = (b[:, 0] - a[:, 0]) * (c[:, 1] - a[:, 1]) - (b[:, 1] - a[:, 1]) * (c[:, 0] - a[:, 0]) + return {(float(x), float(y)) for x, y in ring[cross != 0]} + + +def test_outline_tolerance_0_writes_the_fine_ring_less_its_collinear_vertices( + tmp_path: Path, dem_dir: Path, lakes: Path +) -> None: + out = tmp_path / "fine.geojson" + run(dem_dir, out, *utm_seed(), "--lakes", str(lakes), "--outline-tolerance", "0") + polygon = read_domain(out).polygon + assert {(float(x), float(y)) for x, y in polygon.exterior.coords} == fine_corners() + assert vertices(polygon) == len(fine_corners()) + assert polygon.area == pytest.approx(expected_area(), rel=1e-12) + + +def test_the_default_reduces_to_twice_the_cell_keeping_the_area( + tmp_path: Path, dem_dir: Path, lakes: Path +) -> None: + fine_out, out = tmp_path / "fine.geojson", tmp_path / "c.geojson" + run(dem_dir, fine_out, *utm_seed(), "--lakes", str(lakes), "--outline-tolerance", "0") + run(dem_dir, out, *utm_seed(), "--lakes", str(lakes)) + fine, reduced = read_domain(fine_out).polygon, read_domain(out).polygon + assert vertices(reduced) < vertices(fine) + assert abs(reduced.area - fine.area) <= 1e-9 * fine.area + assert reduced.is_valid + assert reduced.contains(Point(SEED)) + tolerance = 2 * D + # Measured, not guaranteed (698b19f); expected here because the fine ring + # is a traced lattice outline. + assert shapely.hausdorff_distance(fine.exterior, reduced.exterior, densify=0.05) <= ( + tolerance + D + ) + + props = feature(out)["properties"] + assert props["outline_tolerance_m"] == pytest.approx(tolerance) + assert props["reduced_vertices"] == vertices(reduced) + assert props["fine_vertices"] > props["reduced_vertices"] + assert props["reduced_area_m2"] == pytest.approx(reduced.area, rel=1e-12) + assert props["fine_area_m2"] == pytest.approx(fine.area, rel=1e-9) + + +def test_an_explicit_tolerance_is_kept(tmp_path: Path, dem_dir: Path, lakes: Path) -> None: + fine_out, out = tmp_path / "fine.geojson", tmp_path / "c.geojson" + run(dem_dir, fine_out, *utm_seed(), "--lakes", str(lakes), "--outline-tolerance", "0") + run(dem_dir, out, *utm_seed(), "--lakes", str(lakes), "--outline-tolerance", "50") + fine, reduced = read_domain(fine_out).polygon, read_domain(out).polygon + distance = shapely.distance(shapely.points(np.asarray(fine.exterior.coords)), reduced.exterior) + assert distance.max() <= 50.0 + 1e-6 + assert feature(out)["properties"]["outline_tolerance_m"] == pytest.approx(50.0) + + +def test_stderr_reports_the_reduced_outline(tmp_path: Path, dem_dir: Path, lakes: Path) -> None: + output = run(dem_dir, tmp_path / "c.geojson", *utm_seed(), "--lakes", str(lakes)) + lines = [line for line in output.splitlines() if re.search(r"(?i)\breduced\b", line)] + assert lines, output + line = " ".join(lines) + assert re.search(r"\bvertices\b", line), line + assert re.search(r"km", line), line + assert re.search(r"\d\s*m(2|\u00b2)(?![a-z])", line.replace("km", "")), line + + +@pytest.mark.parametrize("value", ["-1", "nan", "inf"]) +def test_a_bad_outline_tolerance_is_refused( + tmp_path: Path, dem_dir: Path, lakes: Path, value: str +) -> None: + refused( + tmp_path, "--dem", str(dem_dir), *utm_seed(), "--lakes", str(lakes), + "--outline-tolerance", value, + ) # fmt: skip + + +@pytest.mark.skipif( + not (DTM10.is_dir() and OLA_NORWAY.is_file()), reason="Ola's DTM10 and CORINE are not here" +) +def test_bygdin_reduced_keeps_the_area_and_meshes_at_10_m(tmp_path: Path) -> None: + lakes_args = ("--lakes", str(OLA_NORWAY), "--lakes-layer", "corine2018") + fine_out, out = tmp_path / "fine.geojson", tmp_path / "bygdin.geojson" + run(DTM10, fine_out, "--seed", *BYGDIN_SEED, *lakes_args, "--outline-tolerance", "0") + run(DTM10, out, "--seed", *BYGDIN_SEED, *lakes_args) + fine, reduced = read_domain(fine_out).polygon, read_domain(out).polygon + assert abs(reduced.area - fine.area) <= 1e-9 * fine.area + # PR 1 measured 17,812 fine vertices; the design's gauge is about 900. + assert vertices(reduced) <= 17_812 // 5, vertices(reduced) + assert feature(out)["properties"]["outline_tolerance_m"] == pytest.approx(20.0) + vtk = tmp_path / "bygdin.vtk" + code, output = invoke( + "mesh", "--dem", str(DTM10), "--domain", str(out), "--tolerance", "10", "--out", str(vtk) + ) + assert code == 0, output + assert vtk.is_file() and vtk.stat().st_size > 0 diff --git a/tests/python/test_core_reduce.py b/tests/python/test_core_reduce.py new file mode 100644 index 00000000..deeac74e --- /dev/null +++ b/tests/python/test_core_reduce.py @@ -0,0 +1,191 @@ +"""`_core.reduce_ring`, the outline reduction through the binding (increment 22, PR 2). + +`docs/increments/22-auto-catchment.md`, "The reduction" and "The red suites" +(PR 2, `test_core_reduce.py`): on rings traced from random blobs and from the +PR 1 synthetic catchments, the four guarantees, the Hausdorff distance +measured, and determinism. + +Interface assumed (names chosen here, stated in the handback; they mirror the +C++ `ReduceOutcome` in `tests/cpp/unit/test_area_collapse.cpp`): + +- `_core.reduce_ring(ring, tolerance, keep) -> ReduceOutcome`. `ring` is an + `(N, 2)` float64 array-like of `(x, y)`, open (the first vertex not + repeated), counter-clockwise; `keep` an `(K, 2)` array-like of points that + must stay strictly inside. Any other shape is a `ValueError`. Releases the + GIL. +- `ReduceOutcome.ring`: an `(M, 2)` float64 array, open. `.status`: a + `_core.ReduceStatus` (`Ok`, `InvalidTolerance`, `NotCounterClockwise`, + `TooFewVertices`). Counts: `collinear`, `collapses`, `rejected_crossing`, + `rejected_seed`, `rejected_tolerance`. + +The four guarantees, as the design words its tests: area `|A_r - A_f| <= +1e-9 A_f`; simple (shapely `is_valid`, `is_simple`); the seed strictly inside +(shapely `contains`); every fine vertex within the tolerance of the reduced +ring and every reduced vertex within it of the fine ring. + +MEASURED, NOT GUARANTEED: the Hausdorff distance (`densify=0.05`) at most the +tolerance plus one cell. The deviation bounds vertices only, so a long new +edge may pass further from the fine ring than its ends (the design, revised +at review in 698b19f: on random non-lattice star rings it exceeded the +tolerance by up to 1.34 times). On traced lattice outlines it has held, and +every ring this file draws is one: outlines traced from random blobs and the +PR 1 catchments. `check` asserts it for that reason only; a non-lattice ring +must not be passed to it. +""" + +from __future__ import annotations + +from typing import Any + +import numpy as np +import pytest +import shapely +from shapely.geometry import LinearRing, Point, Polygon + +from catchment_fixtures import EPSG, MemoryRepository, bowl, lake_box, lat, tile_of +from mosaic_fixtures import quadrants +from test_catchment import plus_with_a_hole + +CELL = 10.0 + + +@pytest.fixture(scope="module") +def core() -> Any: + import tin_engine._core as core + + assert hasattr(core, "reduce_ring"), "tin_engine._core has no reduce_ring" + return core + + +def open_ring(coords: Any) -> np.ndarray: + a = np.asarray(coords, dtype=np.float64) + return a[:-1] if len(a) > 1 and np.array_equal(a[0], a[-1]) else a + + +def check( + core: Any, fine: np.ndarray, tolerance: float, keep: np.ndarray, cell: float = CELL +) -> np.ndarray: + """Reduce `fine`, a TRACED LATTICE RING, and check the four guarantees and + the measured Hausdorff bound; the reduced ring.""" + out = core.reduce_ring(fine, tolerance, keep) + assert out.status == core.ReduceStatus.Ok + red = np.asarray(out.ring, dtype=np.float64) + assert red.ndim == 2 and red.shape[1] == 2 + assert np.isfinite(red).all() + assert len(fine) - out.collinear - out.collapses == len(red) + f, r = Polygon(fine), Polygon(red) + assert r.exterior.is_ccw + assert abs(r.area - f.area) <= 1e-9 * f.area, (r.area, f.area) + assert LinearRing(red).is_simple and r.is_valid + for k in keep: + assert r.contains(Point(k)) + slack = 1e-9 * max(1.0, tolerance) + assert shapely.distance(shapely.points(fine), r.exterior).max() <= tolerance + slack + assert shapely.distance(shapely.points(red), f.exterior).max() <= tolerance + slack + # Measured, not guaranteed (see the module docstring): holds on traced + # lattice rings, which is all this file passes here. + assert shapely.hausdorff_distance(f.exterior, r.exterior, densify=0.05) <= tolerance + cell + # Deterministic: the same input again gives the same bits. + again = core.reduce_ring(fine, tolerance, keep) + assert np.asarray(again.ring).tobytes() == np.asarray(out.ring).tobytes() + return red + + +def blob_ring(seed: int) -> tuple[np.ndarray, np.ndarray]: + """The outer ring round a random blob of 10 m nodes (a union of disks), + in metres from its window's lower-left corner, and one in-node inside it.""" + from tin_engine.outline import trace + + rng = np.random.default_rng(22_300 + seed) + n = 48 + r, c = np.indices((n, n)) + mask = np.zeros((n, n), dtype=bool) + for _ in range(int(rng.integers(3, 9))): + cr, cc = rng.integers(12, 36, size=2) + mask |= np.hypot(r - cr, c - cc) <= rng.uniform(2.0, 10.0) + mask[24, 24] = True + rings = [open_ring(ring) for ring in trace(mask)] + world = [np.column_stack([ring[:, 1] * CELL, (n - 1 - ring[:, 0]) * CELL]) for ring in rings] + outer = max(world, key=lambda w: Polygon(w).area if LinearRing(w).is_ccw else -1.0) + keep = np.array([[24 * CELL, (n - 1 - 24) * CELL]]) + if not Polygon(outer).contains(Point(keep[0])): + keep = np.empty((0, 2)) + return outer, keep + + +@pytest.mark.parametrize("tolerance", [0.0, 5.0, 10.0, 20.0, 50.0]) +@pytest.mark.parametrize("seed", range(12)) +def test_random_blobs_keep_every_guarantee(core: Any, seed: int, tolerance: float) -> None: + fine, keep = blob_ring(seed) + red = check(core, fine, tolerance, keep) + if tolerance == 0.0: + # Only the collinear pass: every reduced vertex is a fine vertex. + assert {tuple(p) for p in red.tolist()} <= {tuple(p) for p in fine.tolist()} + + +def test_blobs_are_reduced_substantially_at_two_cells(core: Any) -> None: + kept = [] + for seed in range(12): + fine, keep = blob_ring(seed) + zero = core.reduce_ring(fine, 0.0, keep) + two = core.reduce_ring(fine, 2 * CELL, keep) + kept.append(len(two.ring) / len(zero.ring)) + # A reducer that stopped after the collinear pass would keep all of them. + assert np.median(kept) < 0.8 + + +def catchment_ring(fine: Polygon) -> tuple[np.ndarray, np.ndarray]: + """A PR 1 fine outline shifted to its lower-left corner, as the design has + Python do before the reduction.""" + x0, y0 = fine.bounds[0], fine.bounds[1] + return open_ring(np.asarray(fine.exterior.coords)) - (x0, y0), np.array([x0, y0]) + + +@pytest.mark.parametrize("tolerance", [0.0, 100.0, 200.0, 500.0]) +def test_the_pr1_bowl_catchment_keeps_every_guarantee(core: Any, tolerance: float) -> None: + from tin_engine.catchment import CatchmentRequest, delineate + + z = bowl() + result = delineate( + CatchmentRequest(seed=lat(100, 200), seed_crs=EPSG, lakes=(lake_box(),), lakes_crs=EPSG), + MemoryRepository(quadrants(tile_of(z), row_cut=200, col_cut=100, overlap=1)), + ) + fine, origin = catchment_ring(result.fine) + seed = np.array([lat(100, 200)]) - origin + check(core, fine, tolerance, seed, cell=100.0) + + +def test_the_pr1_plus_catchment_keeps_every_guarantee(core: Any) -> None: + from tin_engine.catchment import CatchmentRequest, delineate + + z, lake = plus_with_a_hole() + result = delineate( + CatchmentRequest(seed=lat(35, 34), seed_crs=EPSG, lakes=(lake,), lakes_crs=EPSG), + MemoryRepository({"t.tif": tile_of(z)}), + ) + fine, origin = catchment_ring(result.fine) + check(core, fine, 200.0, np.array([lat(35, 34)]) - origin, cell=100.0) + + +def test_the_statuses_cross_the_binding(core: Any) -> None: + square = np.array([[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]]) + none = np.empty((0, 2)) + status = core.ReduceStatus + assert core.reduce_ring(square, 1.0, none).status == status.Ok + assert core.reduce_ring(square, -1.0, none).status == status.InvalidTolerance + assert core.reduce_ring(square, float("nan"), none).status == status.InvalidTolerance + assert core.reduce_ring(square, float("inf"), none).status == status.InvalidTolerance + assert core.reduce_ring(square[::-1].copy(), 1.0, none).status == status.NotCounterClockwise + assert core.reduce_ring(square[:3], 1.0, none).status == status.TooFewVertices + + +@pytest.mark.parametrize("shape", [(4,), (4, 3), (2, 4, 2)]) +def test_a_ring_of_another_shape_is_a_value_error(core: Any, shape: tuple[int, ...]) -> None: + with pytest.raises(ValueError, match=r"(?i)shape|\(N, ?2\)"): + core.reduce_ring(np.zeros(shape), 1.0, np.empty((0, 2))) + + +def test_a_keep_of_another_shape_is_a_value_error(core: Any) -> None: + square = np.array([[0.0, 0.0], [10.0, 0.0], [10.0, 10.0], [0.0, 10.0]]) + with pytest.raises(ValueError, match=r"(?i)shape|\(K, ?2\)|\(N, ?2\)"): + core.reduce_ring(square, 1.0, np.zeros((3,)))