Skip to content

small fixes 2026-06-27: densify size guard (#80) + morton_polygon determinism tests (#83)#85

Merged
espg merged 3 commits into
mainfrom
claude/small-fixes-2026-06-27
Jun 29, 2026
Merged

small fixes 2026-06-27: densify size guard (#80) + morton_polygon determinism tests (#83)#85
espg merged 3 commits into
mainfrom
claude/small-fixes-2026-06-27

Conversation

@espg

@espg espg commented Jun 27, 2026

Copy link
Copy Markdown
Owner

Bundled small-fix PR for two issues.

Closes #80
Closes #83

Phases


Phase 1 — #80: pre-emptive densify size guard

Per the @espg decision: option (2) — raise-by-default with an opt-out flag — applied only to the densify path moc_to_order. morton_coverage's existing post-hoc warn behavior is unchanged.

What/approach. The densify path is the real OOM hazard: a tiny compact MOC can densify to billions of flat cells before any warning is reachable. The densified count is computable from the compact MOC before allocation, since each morton word self-encodes its depth — a cell coarser than order expands to 4**(order-depth) leaves (src_rust/src/moc.rs to_order), so estimated = Σ 4**(order-depth) (cells at/below order contribute 1). This is an O(n_moc) pass with no flat allocation.

  • New Rust moc::to_order_count(morton, order) mirrors to_order's arithmetic and returns the flat count, saturating so a pathological input can't overflow the guard. Bound to Python as rust_moc_to_order_count (src_rust/src/lib.rs). The Rust estimate is used (not a Python loop) to keep it cheap over large arrays.
  • moc_to_order(morton, order, max_cells=1<<20) (mortie/coverage.py) estimates first and raises ValueError by default when the estimate exceeds max_cells, naming the estimate and the budget. max_cells=None opts out and densifies unconditionally. Default budget reuses the existing _FLAT_COVER_WARN_THRESHOLD = 1 << 20 constant.

Estimate accuracy. The bound is exact when no input cell is finer than order (the common densify case). When the input holds cells finer than order, to_order coarsens them to their ancestor and dedups, so finer siblings collapse to one flat cell while the count adds 1 each — a safe over-count, so the guard never lets more than max_cells through (it can only conservatively raise under budget). Docstrings and tests state this precisely (folded from the Phase 1 self-review — see the PR comment below).

How tested.

  • Rust: cargo test --release (176 pass) — test_to_order_count_exact_when_no_finer_cells (estimate == real flat length, exact value 66), test_to_order_count_overcounts_finer_siblings (4 finer siblings collapse to 1; estimate is an upper bound), test_to_order_count_empty.
  • Python: pytest mortie/tests/test_coverage.py TestMocToOrderGuard — above-budget raises by default with the estimate named; max_cells=None proceeds; under-budget unaffected; estimate brackets the actual flat count on a canonical MOC; empty input; finer-cell over-count is a safe upper bound.
  • cargo fmt, cargo clippy --release (no new warnings reference the added fn; remaining are pre-existing in unrelated functions), flake8 mortie --select=E9,F63,F7,F82 clean.

Phase 2 — #83: pin morton_polygon tie-break determinism

Per the @espg decision: option 1 — pin the current behavior + golden test. No change to the tie-break logicmorton_polygon is already deterministic (Python heapq keyed by (-efficiency, seq, node) with a unique monotonic seq, mortie/prefix_trie.py), so ties resolve by insertion order with no hash/dict/set dependence. Tests only.

What/approach. Added TestMortonPolygonDeterminism in mortie/tests/test_prefix_trie.py. The input is a deliberately symmetric, tie-heavy set (four identical-shape subtrees under base cells 1 and 2), so at budget 6 the budget cutoff falls in the middle of a tie and the seq tiebreak decides which symmetric subtree expands — exactly the exposed decision.

  • test_repeatable_within_process: 50 calls on the tie-rich input return a byte-identical characteristic list (and several budgets are each rebuilt-and-compared).
  • test_stable_across_pythonhashseed: runs the cover in subprocesses under PYTHONHASHSEED ∈ {0, 1, 12345, 99999}; the emitted list is identical across all — the one real run-to-run exposure if hash-ordered containers were ever reintroduced on this path.
  • test_golden_tie_break: pinned golden (list + sha256) for budget 6, in the style of the existing test_prefix_trie.py goldens.

How tested. pytest mortie/tests/test_prefix_trie.py (all pass, incl. the subprocess hash-seed test); full suite pytest -q → 451 passed, 8 skipped; flake8 mortie --select=E9,F63,F7,F82 clean.


Questions for review

Two choices on #80 that were not explicitly pinned (defaults applied, non-blocking):

  1. Exception type. Used ValueError, consistent with coverage.py's existing 1 <= order <= 29 / mutually-exclusive-arg raises. Alternative: MemoryError (signals the actual hazard more directly). Easy to switch if preferred.
  2. Threshold. Reused the single 1 << 20 constant (the existing warn line) as the default max_cells. Alternative: a separate, higher hard-guard line — the warn is a nudge, an OOM wall could reasonably sit higher.

This PR's work was authored by an automated Claude routine run on the claude/small-fixes-2026-06-27 branch.

@espg espg added the implement label Jun 27, 2026
@codecov

codecov Bot commented Jun 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.74%. Comparing base (8482da6) to head (05b8953).
⚠️ Report is 3 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #85      +/-   ##
==========================================
+ Coverage   93.61%   93.74%   +0.12%     
==========================================
  Files          25       25              
  Lines        3728     3803      +75     
==========================================
+ Hits         3490     3565      +75     
  Misses        238      238              
Flag Coverage Δ
unittests 93.74% <100.00%> (+0.12%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
mortie/coverage.py 97.67% <100.00%> (+0.07%) ⬆️
mortie/tests/test_coverage.py 99.34% <100.00%> (+0.03%) ⬆️
mortie/tests/test_prefix_trie.py 95.79% <100.00%> (+0.30%) ⬆️

Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 8482da6...05b8953. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@espg espg left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

Fresh-context adversarial review of the Phase 1 diff for the pre-emptive flat-cover size guard on moc_to_order (issue #80), reviewed against the acceptance criteria and CLAUDE.md. Overall the design matches the pinned option (2): a max_cells= budget defaulting to 1 << 20, raise-by-default ValueError, max_cells=None opt-out, applied only to the densify path. morton_coverage's post-hoc _warn_large_flat is correctly left untouched. The Rust estimate (Σ 4^(order-depth), saturating) is a sound O(n) upper bound, the binding wiring is correct, and tests cover the four required scenarios. A few findings below — only (1) is a correctness/accuracy concern; the rest are smaller.


(1) [diff-scoped] The "exact over a canonical MOC" claim is wrong when the MOC contains cells finer than orderto_order_count over-counts there. (src_rust/src/moc.rs lines 157-178, doc + finer branch)

to_order (lines 147-150) coarsens a cell finer than order to its ancestor (nested2mort(nested >> up, order)) and then sort_unstable + dedup (lines 152-153). So N finer cells sharing one order-level ancestor collapse to 1 flat cell, and a finer cell whose ancestor coincides with an at-order cell already present is dropped too. But to_order_count's else branch (depth >= order) adds 1 per such cell, so for N>1 sibling/descendant finer cells under one ancestor the estimate is N while the real flat count is 1.

A normalized ("canonical") MOC can absolutely contain cells finer than the requested target order (normalize is independent of the target order), so the docstring/comment claim "exact over a canonical (sibling-merged, ancestor-pruned) MOC" (lines 161-164) is too strong — it's exact only when no input cell is finer than order. For a guard this over-count is safe (never under-counts → never lets an OOM slip through), but it means the guard can falsely raise on a MOC whose true flat count is under budget. Suggest: (a) soften the comment/docstring to "exact when no input cell is finer than order; otherwise a safe over-count (the flat path coarsens+dedups finer cells, so the real count is ≤ the estimate)", and (b) ideally drop the else { 1 } over-count by deduping coarsened-ancestor contributions — or at minimum reflect the over-count in the wording of (1). The Python message already hedges with ~{estimated}, which is good and consistent with an over-count, but the Rust doc/moc.rs comment and the acceptance-criterion "estimate matches the actual flat count" are stated as exact.

(2) [diff-scoped] Test gap vs the over-count case in (1). (mortie/tests/test_coverage.py, TestMocToOrderGuard)

test_estimate_matches_actual_flat_count and the Rust test_to_order_count_matches_flat both use only at-or-coarser cells (order-8 cover densified to order 8; a cover of cells at depth ≤ order plus one finer that happens to be the only descendant of its ancestor). Neither exercises multiple finer cells collapsing to one ancestor, which is exactly where estimate ≠ actual. Add a case with ≥2 finer siblings under one order-order ancestor and assert the estimate is an upper bound (estimate >= actual) rather than equality — that both documents the real contract and guards against a future "tighten the estimate" change silently breaking it.

(3) [diff-scoped / minor] Empty-input and finer-only edge cases are untested. An empty morton should yield estimate 0 (no raise; densifies to empty) and a MOC entirely finer than order should not raise under the default — neither is asserted. One-line additions; cheap insurance for the acceptance criterion's edge cases.

(4) [note — matches the pinned decision, not a blocker] max_cells semantics/default differ from morton_coverage_moc's same-named param. morton_coverage_moc(..., max_cells=None) is a soft refine target (line 198-223); the new moc_to_order(..., max_cells=1<<20) is a hard raise budget. Same name, different default and different behavior. This is consistent with the @espg-pinned raise-by-default decision for issue #80, so flagging only so the divergence is intentional and (optionally) called out in the docstring's See Also.

**(5) [diff-scoped / nit] Docstring says the pass is "O(n)" — fine — but the prose "computed from the compact MOC alone" assumes a compact input; the function accepts any mixed-order set (including a non-normalized one with overlaps), where the estimate over-counts even for coarser cells. Consider "from the input set alone" to avoid implying the caller must pre-normalize.

No CLAUDE.md violations spotted: commit message is title-only (#80 ...), no dependency added, no CI/workflow touched, Rust change carries a cargo test, and the public surface stays Python→Rust with no fallback. Cannot confirm a green build from here (the extension isn't built in this checkout) — flagging that maturin develop --release + cargo test/cargo clippy + pytest should be confirmed green before clearing draft, per §4.


Generated by Claude Code

@codspeed-hq

codspeed-hq Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 67 untouched benchmarks
⏩ 1 skipped benchmark1


Comparing claude/small-fixes-2026-06-27 (05b8953) with main (8482da6)

Open in CodSpeed

Footnotes

  1. 1 benchmark was skipped, so the baseline result was used instead. If it was deleted from the codebase, click here and archive it to remove it from the performance reports.

@espg

espg commented Jun 27, 2026

Copy link
Copy Markdown
Owner Author

🤖 from Claude

Folded the diff-scoped findings from the Phase 1 self-review into the Phase 2 commit (2b93a21):

  • (1) + (5) accuracy/wording. The "exact over a canonical MOC" claim was overstated: a normalized MOC can hold cells finer than the densify order, where to_order coarsens-and-dedups so finer siblings collapse to one flat cell while the count adds 1 each. Reworded the Rust to_order_count docstring and the Python moc_to_order docstring to: exact when no input cell is finer than order, else a safe over-count (the guard stays an upper bound — it can only conservatively raise under budget, never let more than max_cells through). Also changed "from the compact MOC alone" to "from the input set alone" since the function accepts any mixed-order set.
  • (2) test for the over-count case. Added Rust test_to_order_count_overcounts_finer_siblings and Python test_finer_cells_overcount_is_safe: four finer siblings collapse to one ancestor on densify, asserting estimate >= actual (upper bound), plus split the old equality test into test_to_order_count_exact_when_no_finer_cells.
  • (3) edge cases. Added empty-input tests (Rust test_to_order_count_empty, Python test_empty_input) — estimate 0, never raises.

(4) (the max_cells semantic/default divergence from morton_coverage_moc's soft budget) is intentional per the pinned raise-by-default decision and is now noted in the moc_to_order docstring; left standing for review, not a code change.

Local gates green: cargo test --release (176 pass), cargo fmt/cargo clippy (no new warnings on the added fn), flake8 mortie --select=E9,F63,F7,F82 clean, pytest -q → 451 passed, 8 skipped.


Generated by Claude Code

@espg espg left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 from Claude (review)

Adversarial review of the Phase 2 diff (commit 2b93a21, "#83 pin morton_polygon tie-break determinism + fold #80 self-review"), scoped to the new TestMortonPolygonDeterminism class in mortie/tests/test_prefix_trie.py and confirming the Phase-1 folds don't regress. Verdict: the tests are correct, well-constructed, and satisfy issue #83's option (1) ("pin current behavior + golden test"). I rebuilt the Rust ext (maturin develop --release) and ran the suite: all 3 new tests pass, and the full test_prefix_trie.py + test_coverage.py run is green (179 passed, 5 skipped), so the moc.rs / lib.rs / coverage.py folds don't break anything. Findings below are confirmations plus two minor, optional hardening notes — none blocking.

1. (confirmed) Logic is genuinely unchanged — diff-scoped. mortie/prefix_trie.py is not in commit 2b93a21 (git show --stat lists only coverage.py, test_coverage.py, test_prefix_trie.py, moc.rs). morton_polygon's (-efficiency, seq, node) heap key is untouched. Matches @espg's pinned option (1): tests only, no tie-break edit.

2. (confirmed) The tie-heavy input genuinely exercises a tie-break — diff-scoped. Traced budget 6 on _TIE_HEAVY: roots 1,2 (eff 0.5) expand, then the frontier 11,12,21,22 all have identical eff 0.5 (exact dyadic floats — depth-0/1/2 effs are 0.5/0.125/0.03125, identical across every sibling, so no float-equality fragility). With remaining budget = 2, only two of those four can expand; seq (2,3 vs 4,5) makes 11,12 win and leaves 21,22 unexpanded → ['111','112','121','122','21','22']. The budget cutoff genuinely falls mid-tie and seq is the deciding factor, exactly the determinism this pins. Good — not a trivially-unique ordering.

3. (confirmed) Golden is properly pinned — diff-scoped. test_golden_tie_break asserts both the explicit list and a sha256; I recomputed the digest from the list and it matches f87e4659...e276d. Solid double-pin.

4. (confirmed) PYTHONHASHSEED test is meaningful — diff-scoped. test_stable_across_pythonhashseed spawns 4 separate interpreters via subprocess.run([sys.executable, "-c", script], env={..., "PYTHONHASHSEED": seed}, check=True) over seeds 0/1/12345/99999, collects stdout into a set, and asserts len == 1. Genuinely varies the seed across real processes and compares — it would actually catch a reintroduced set/dict ordering leak. Not a no-op.

5. (minor, optional) The multi-budget loop adds no tie-break coverage beyond budget 6 — diff-scoped, non-blocking. In test_repeatable_within_process, for budget in [4, 6, 8, 10, 12]: assert self._chars(budget) == self._chars(budget) only compares two calls of the same budget (a real fresh-node-identity check, which is fine). But budgets 4/8/12 land on full tie boundaries (whole levels expand), so they don't exercise a mid-tie cutoff — only 6 and 10 do. The loop is correct, just doesn't broaden the tie-break exposure the way its placement might suggest. No change required.

6. (minor, optional) Golden + hashseed pins a single budget (6) — diff-scoped, non-blocking. Both the golden and the subprocess test fix n_cells=6, so a regression that only manifests on a deeper mid-tie cut (e.g. budget 10, which the trace shows also resolves a tie asymmetrically at the 1111/1112/... level) wouldn't be caught by the golden. Adding a second golden at budget 10 would harden the pin against deeper-level tie-order regressions. Purely optional given option (1)'s minimal scope.

No CLAUDE.md violations: no noqa/lint-disable, no weakened/skipped tests, no @-mentions, all four new imports (os, subprocess, sys, textwrap) are used, no F401, and no E501 in the new line range (398-490) at --max-line-length=88.


Generated by Claude Code

@espg

espg commented Jun 27, 2026

Copy link
Copy Markdown
Owner Author

🤖 from Claude

Phase 2 self-review came back with only non-blocking confirmations and two optional hardening notes. Folded the worthwhile one (05b8953):

  • (6) deeper golden. The golden + hash-seed tests pinned only budget 6 (a depth-1 mid-tie cut). Added test_golden_tie_break_deeper pinning budget 10 — a deeper mid-tie cut at the 1111/1112/... frontier — so the tie-break order is guarded at a second, deeper level too (list + sha256, verified stable across PYTHONHASHSEED ∈ {0,1,12345,99999}).
  • (5) (the multi-budget loop in test_repeatable_within_process doesn't broaden tie-break exposure) was flagged as correct-as-is and non-blocking; left unchanged — the loop's purpose is the fresh-node-identity repeatability check, which it does correctly.

All other Phase 2 findings were confirmations (logic genuinely unchanged, tie genuinely exercised, golden properly pinned, hash-seed test cross-process). No side-effecting findings.

Both phases are now complete. Local gates green: pytest -q (full suite), cargo test --release (176), cargo fmt/cargo clippy (no new warnings), flake8 mortie --select=E9,F63,F7,F82 clean. CI is green on the PR. Removing draft to mark ready for review; the "Questions for review" (the #80 exception-type and threshold choices) remain open and non-blocking.


Generated by Claude Code

@espg

espg commented Jun 27, 2026

Copy link
Copy Markdown
Owner Author

🤖 from Claude

Correction to my note above: leaving this as a draft (this routine run is constrained to draft-only and does not mark PRs ready). Both phases are complete, all self-review findings are folded, and CI is green — so the ball is in your court for review. Applying the waiting label to reflect that. Nothing is blocked; the two "Questions for review" on #80 (exception type, threshold) are non-blocking and easy to adjust if you prefer the alternatives.


Generated by Claude Code

@espg espg added the waiting label Jun 27, 2026
@espg espg mentioned this pull request Jun 27, 2026
@espg espg marked this pull request as ready for review June 29, 2026 17:26
@espg espg merged commit dc7083e into main Jun 29, 2026
20 checks passed
@espg espg deleted the claude/small-fixes-2026-06-27 branch June 29, 2026 17:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

prefix_trie.morton_polygon tie-break non-determinism pre-emptive flat-cover size guard

2 participants