Skip to content

Increment 35: the land-cover clean-up step made faster, output byte-identical - #229

Merged
skavhaug merged 11 commits into
masterfrom
worktree-cleanup-speed
Oct 11, 2026
Merged

skavhaug merged 11 commits into
masterfrom
worktree-cleanup-speed

Conversation

@skavhaug

Copy link
Copy Markdown
Member

Increment 35 (docs/increments/35-cleanup-speed.md), Ola's pick of 2026-10-11 ("land-cover clean-up speed-up, then GeoPackage"). 23 net production lines, Python only (count_loc.py 3e6f87eb HEAD: crs.py +14, feature_input.py +9), against the design's estimate of about 20.

  • From @perf's profile at master (docs/benchmarks/2026-10-11/cleanup-profile/): four exact changes. The cover polygons are reprojected in one shapely.transform call, with point arrays over 500,000 split across up to 8 threads, one Transformer each; _untouched widened to lines whose ends touch the outline (relate_pattern(domain, line, "T**F**FF*")); covers skipped where _untouched held; one outline STRtree per clean-up.
  • Every output byte-identical: all 7 quick-check meshes' SHA-256 equal, both catchments' features: lines identical.
  • @perf's acceptance (docs/benchmarks/2026-10-11/cleanup-speed/): Lagan features clip 11.16 → 6.34 s (0.569 of base, limit 0.65), run 19.3 → 14.5 s; Numedalslågen 6.09 → 6.00 s, its clean-up stays near 6 s (GEOS overlay time, Ola's default); other cases within -0.7 to +0.8 %. Quick-check baseline refreshed.
  • Design review 1 round, code review 1 round, both APPROVED; mutation round M1–M4 all killed (record in 7621a99e and section 11 of the increment file).
  • Two questions decided on defaults while Ola was away (section 9); two hotspot shares for his ruling (section 11).

🤖 Generated with Claude Code

skavhaug and others added 11 commits October 11, 2026 15:11
…gen and Lagan, for increment 35's design (@Perf)

One --stats run and one cProfile run per case on battery; the step is 6.9 s
(67 % of the run) on Numedalslagen and 7.8 s (38 %) on Lagan. Largest
sub-steps: the outline rule (2.8 s / 2.5 s, mostly GEOS union_all and
make_valid), the two clips (1.4 s / 1.2 s), coverage_clean (1.2 s / 0.9 s);
on Lagan the per-line covers and intersection in _add (1.5 s) and, before
the step, the pyproj reprojection of every polygon (4.1 s).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… its measured probes (@architect)

Four exact Python changes, about 20 net lines, every output byte-identical:
the cover polygons reprojected in one call split over up to 8 threads;
_untouched widened to lines whose ends touch the outline (DE-9IM T**F**FF*);
covers skipped where it holds; one outline STRtree per clean-up. Measured on
monkeypatched runs: Lagan features clip 11.74 to 6.74 s, Numedalslagen 6.29
to 6.00 s, meshes and features: lines identical. Skipping the region and
domain clips left out: GEOS's overlay rewrites ring order (ring_order.py,
stage_check.py). ROADMAP row 35 numbered and set to designed.

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

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

docs/increments/35-cleanup-speed.md section 5, tests 1 to 6, Python only.
15 tests fail at this commit, 275 others in the two files pass:
  .venv/bin/python -m pytest tests/python/test_crs.py tests/python/test_feature_input.py -q --no-cov

tests/python/test_crs.py, TestChunkedReprojector (test 1, invariant-critical):
- 10, 500 000 (= 2 * CHUNK_POINTS), 500 001 and 1.2 million points drawn
  uniformly in EPSG:3035 over southern Sweden, to EPSG:3006, np.array_equal
  against one Transformer.transform call (kills M4: a point dropped or
  doubled changes the shape or a value). Red where crs.CHUNK_POINTS is read;
  the 10- and 1.2-million-point rows pass today (one call is its own oracle).
- CHUNK_POINTS == 250_000 and WORKERS == min(8, os.cpu_count() or 1).

tests/python/test_feature_input.py:
- UNTOUCHED_CASES (test 3, invariant-critical) gains ends-on-the-outline,
  one-end-on-the-outline, end-on-a-hole (the design review's case) and
  end-on-a-domain-vertex, all True, and
  ends-on-the-outline-through-a-domain-vertex, False. Measured here (GEOS
  3.14.1): the pattern T**F**FF* agrees with every row, and GEOS gives the
  line back unchanged on every True row. test_r2_every_true_row_is_what_geos_
  gives_back runs that oracle over each True row; the random test_r2 stays.
  Red: the four new True rows (today contains_properly refuses them).
- TestOneTransformPerSource (test 2): three EPSG:3035 squares under the
  corine map with the CLI's clean-up defaults; fi.reprojector wrapped to count
  apply calls: expected [15] (one call, every cover vertex), today [5, 5, 5].
  A vertex at x = 1e300 is refused with today's text naming its fid (passes).
- TestClippedCount (test 4, invariant-critical): _Tally._add called directly
  with two chains; BaseGeometry.covers and shapely.covers both wrapped (the
  method does not go through the function for a scalar). One untouched plus
  one crossing chain: clipped 1 and one covers call (today 2); two untouched:
  clipped 0 and no call (today 2). Kills M3 (clipped from the flags alone).
- TestOneOutlineTreePerCleanup (test 5): the CORINE extract over the
  quarter-circle domain, shapely.STRtree subclassed to record the
  feature_input.py frames under _clean; one tree not under snap_to_outline
  (today 8, one per _outline_lines call) and none in _outline_lines. The
  lines of every _outline_lines call equal a tree-free cut (full distance
  matrix of ring points against outline segments, then _chains): 86 lines,
  passes today and after.
- TestTheSetIsUnchanged (test 6): the same run as built and with 30b's
  _untouched and the one-call reprojector (both verbatim) monkeypatched:
  fid, mask, every line's WKB, clipped, outside, empty, cover_vertices equal.
  Passes today; the guard for green.

Not written: no real-catchment test. Section 5 says "No real catchment in
CI" and section 7 gives the byte-identical meshes to @Perf's quick check.

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

docs/increments/35-cleanup-speed.md section 3, Python only; the 15 tests of
c413019 pass, nothing under tests/ touched.

crs.py (3.1 b): CHUNK_POINTS = 250_000 and WORKERS = min(8, os.cpu_count() or 1).
reprojector's apply splits an array of 2 * CHUNK_POINTS points or more with
np.array_split into min(WORKERS, n // CHUNK_POINTS) chunks (uneven split kept
in order) and maps them over a ThreadPoolExecutor opened and closed inside the
call; each worker thread builds its own Transformer in a threading.local, the
calling thread's is built eagerly as before so a bad CRS still refuses at
construction. Smaller arrays take the one-call path unchanged.

feature_input.py:
- 3.1 (a) _take appends cover polygons unmoved; after the loop one
  shapely.transform over an object array of them, then _finite per polygon
  with today's FeatureError text naming the fid; timed into self.seconds.
- 3.2 _untouched: relate_pattern(domain, line, "T**F**FF*") replaces
  contains_properly; the other three clauses and their order stay.
- 3.3 _add evaluates _untouched once per chain and asks covers only for the
  chains it did not pass (all() short-circuits on the first uncovered one).
- 3.4 _outline_tree(outline) -> (segments, STRtree), built once in _clean;
  _outline_lines(polygon, tree) takes the pair. The design wrote
  _outline_lines(polygon, outline, tree); the outline argument would have been
  unused once the tree carries its segments, so it is dropped (only caller:
  _clean).

Verified at this head, this worktree's .venv (shapely 2.2.0 / GEOS 3.14.1,
pyproj 3.8.0, _core as master):
  pytest -q --no-cov: 7143 passed, 17 skipped, 0 failed (559 s)
  mypy strict, ruff check, ruff format --check: clean
  check_prohibited_deps, check_detria_boundary, check_citations: OK
  rasputin mesh, the quick check's two catchment cases, --out X.vtk --binary,
  whole-file sha256 as docs/increments/35-probes/mesh_check.py hashes:
    numedalslagen 349d0b5632a28af6024210556a0bd0b6f35e5ba99378c18c742d4b666acfdcfe
      features: 22 kept (15 cut at the domain outline), 4638 outside the domain, 0 empty
    lagan         6c110f9eb1fb6a741bd4a19a954a91b7a7424d43f7ba04c354d25f9a42495fa0
      features: 23 kept (0 cut at the domain outline), 4193 outside the domain, 0 empty
  both equal to the design's recorded digests (probes README, item 4) and
  features: lines. No C++ built; no sanitizer run (Python only).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…dded for M3 (@tester)

docs/increments/35-cleanup-speed.md section 5, the invariant-critical tests 1, 3
and 4: tests/python/test_crs.py::TestChunkedReprojector,
tests/python/test_feature_input.py::TestUntouched and ::TestClippedCount.
Each mutant planted in a scratch copy of e805d37 made by tools/scratch_copy.py
in the session scratchpad (removed afterwards), the three classes run (36 tests),
the file restored from `git show e805d37:<path>` and touched, the unmutated
run green (36 passed) after each. This machine has 10 cores, so WORKERS = 8 and
the chunked path runs; on a one-core runner M4 cannot be killed (one chunk).

Kill record (mutant | where planted | killing test, fixture row | result):

M1  relate pattern "T**F**FF*" -> "T*****FF*" (plain covers)
    src_python/tin_engine/feature_input.py, _untouched
    TestUntouched::test_r1_case_by_case: along-then-inside,
    domain-vertex-on-the-line, touching-a-hole,
    ends-on-the-outline-through-a-domain-vertex,
    closed-ring-touching-the-outline-at-its-start,
    closed-ring-touching-the-outline-mid-ring (mutant True, False expected);
    and test_r2_where_it_is_true_geos_gives_the_line_back_unchanged.
    along-the-outline does not kill it, as the design review corrected.
    KILLED (7 of 36 failed)

M2  `and bool(shapely.is_simple(line))` dropped
    src_python/tin_engine/feature_input.py, _untouched
    TestUntouched::test_r1_case_by_case: self-touching-at-a-vertex,
    self-crossing, doubling-back; and test_r2_where_it_is_true_geos_gives_
    the_line_back_unchanged.
    KILLED (4 of 36 failed)

M3  `self.clipped += not all(u or dom.covers(g) ...)` -> `not all(whole)`
    src_python/tin_engine/feature_input.py, _Tally._add
    TestClippedCount::test_one_untouched_and_one_crossing_chain_is_one_
    clipped_feature, by the covers spy only (`len(covers_calls) == 1`, got 0):
    the count stays 1 there, since not all([True, False]) is True either way.
    KILLED (1 of 36 failed), but by the spy, not the count. Over the whole
    test_feature_input.py the count alone is caught only by test 6
    (TestTheSetIsUnchanged::test_as_built_equals_the_old_rules, the CORINE
    fixture), which is not invariant-critical. Added
    TestClippedCount::test_a_covered_chain_the_rule_refused_is_not_clipped:
    a chain along BOX's east side, covers True, _untouched False, GEOS gives
    it back whole, clipped 0 and covers asked once; the mutant counts 1.
    Re-planted with the new test copied in: KILLED by the count (1 == 0).

M4  chunk boundary off by one in apply, two variants
    src_python/tin_engine/crs.py, reprojector.apply
    M4-dropped: np.array_split replaced by floor-division slices, the
      remainder dropped. TestChunkedReprojector::test_one_point_above_the_
      threshold_still_equals_one_call (n = 2 * CHUNK_POINTS + 1).
      KILLED (1 of 36 failed)
    M4-doubled: the first chunk one point longer, the boundary point doubled.
      TestChunkedReprojector::test_chunked_equals_one_call_bit_for_bit
      [exactly-two-chunks], [1.2-million-points], and test_one_point_above_
      the_threshold_still_equals_one_call. KILLED (3 of 36 failed)

Red-step scaffolding: none found in c413019's added lines (no xfail, skip,
TODO or red note); nothing to remove.

Verified at this head, this worktree's .venv: pytest -q --no-cov
tests/python/test_feature_input.py tests/python/test_crs.py: 291 passed;
ruff check and ruff format --check on the test file: clean. tests/ only.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d, with the Status line and ROADMAP row, by the main session

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…9 of base, every mesh unchanged; baseline refreshed (@Perf)

docs/benchmarks/2026-10-11/cleanup-speed/README.md: head 357808b against
the branch's base, master 3e6f87e, back to back in one venv (numpy 2.5.4),
AC power, medians of 3 per case. Verdict ACCEPTED against section 7 of
docs/increments/35-cleanup-speed.md: all 7 meshes byte-identical to the
base's; Lagan features clip 11.156 -> 6.342 s (ratio 0.569, limit 0.65);
Lagan total 19.296 -> 14.542 s (0.754, limit 0.85); Numedalslagen features
clip 6.092 -> 6.000 s (0.985, limit 1.0); the other cases -0.7 % to +0.8 %.
Tool verdict FASTER, exit 0.

The first run against the stored baseline 39e3ae3 was NO BASELINE (numpy
2.5.4 vs 2.5.3); increment 32 (merged as 3e6f87e after that baseline)
changed the features meshes, so the base commit was measured with --tree
as docs/increments/README.md asks. docs/benchmarks/quick/baseline-ac.json
is now the head record (perf-quick-check.md rule (a), FASTER at head).

Hotspots: Lagan features clip 43.6 % of the run (ruled 40.7 %; was 57.8 %
on the base, where the tool raised it). Numedalslagen features clip 68 %
is unruled, as on every quick check so far. For Ola.

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

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant