Component-overlap histories: the set is stable, not just the count - #29
Component-overlap histories: the set is stable, not just the count#29aurascoper wants to merge 4 commits into
Conversation
…count "Consolidates to 10 and holds for 85 frames" was a claim about a COUNT. Ten regions every frame is equally consistent with ten stable regions and with regions being born and dying every frame while the total happens to stay 10. A per-frame component count cannot tell those apart. component_overlap.py intersects each component's voxel set with every component's voxel set in the next frame, inherits lineage by largest shared volume, and records births, deaths, merges and splits. Result: 21 lineages ever exist under point-sharing (24 under face-sharing), 10 are alive at MCS 100, all 10 were born before MCS 15, and there are zero births, zero deaths and zero splits across every remaining frame under both conventions. The set is stable, and consolidation is monotone by merge -- no region ever dies. Both adjacency rules are now reported side by side, because they disagree. The count stabilises permanently at MCS 15 under point-sharing and MCS 17 under face-sharing; an earlier revision gave only the point-sharing column and wrote "15 -> 100: 10, flat", which is false under face-sharing. That is the third convention-dependent number in this bundle, after the slice-plane coordinate and the threshold predicate. vti_read.py reads the .vti in numpy alone -- the exporter writes uncompressed appended binary, so no VTK is needed, and an audit of the renderer's output should not need the renderer's toolchain. Validated before use: summing occupied_above_threshold reproduces metrics.json on 101/101 frames, zero mismatches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1GLLCekStea7UHjRF51aw
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1582a7ef24
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| counts.append(k) | ||
| cur_sizes = {i: int((lab == i).sum()) for i in range(1, k + 1)} | ||
|
|
||
| if prev_lab is None or k == 0: |
There was a problem hiding this comment.
Count deaths when a frame becomes empty
When a nonempty frame is followed by a frame with k == 0, this branch initializes an empty current-lineage map but hard-codes deaths to zero, so a complete disappearance of every region is reported as no deaths. Handle only the first frame here, and let empty later frames compare against lineage_of_prev; add a nonempty-to-empty negative control so the advertised death accounting is exercised.
AGENTS.md reference: AGENTS.md:L17-L21
Useful? React with 👍 / 👎.
…isible
`deaths` counted only predecessors with no overlapping successor. In a merge both
predecessors overlap, so neither was a death -- but `c in taken_cur` retires all
but the largest claim, and those lineages were counted nowhere. Measured against
the shipped evidence: 11 of 11 retirements (26-conn) and 14 of 14 (6-conn) were
reported as zero deaths, and README prose read that as "no region ever dies".
The conclusion survives and is now measured rather than inferred: nothing ever
disappears (0 across the whole record under both rules), and every lineage that
stops does so by merge. Component counts are unchanged -- counts_by_mcs is
byte-identical to the superseded receipt, which is retained as historical evidence.
Two further accounting defects, both latent on this dataset but real:
- the seeding branch set frames_seen=1 before the common increment, so a lineage
present for one frame reported two; it now seeds 0 like the birth path.
- `prev_lab is None or k == 0` routed every EMPTY later frame through the seeding
branch, which hard-codes deaths=0. Only the genuinely first frame takes it now.
Events are labelled by destination frame, so the old `mcs >= 15` filter included the
14->15 transition -- the one that PRODUCES the flat state rather than one inside it.
Reported now as `transition_into_frame_15` and `churn_within_frames_16_to_100`.
Added closure assertions that must hold every transition:
carried + retired_by_merge + disappeared == previous count
carried + births == current count
test_component_overlap.py adds 26 fixtures on synthetic frames -- single-frame
lifespan, all-vanish, two- and three-into-one merge, split, competing overlap, and a
transition mixing disappearance with merge. Against the pre-repair implementation they
report frames_seen == 1 -> 2, deaths == 1 -> 0, then a KeyError for a retirement
counter that did not exist.
The tracker measures persistence under largest-overlap greedy inheritance, not
material identity, and lineage integers are not comparable across adjacency rules.
Both are now stated in the receipt.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V1GLLCekStea7UHjRF51aw
…nt-overlap-histories
The footer hard-coded 7+2+5+4+3+2+3 = 26 for 22 check() calls across 7 scenarios; the first block's count was mistyped as 7 when it has 2. A harness that asserts its own size from memory is the same class of error the fixtures exist to catch. It counts now, and README.md states 22. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V1GLLCekStea7UHjRF51aw
Stacked on #28.
"Consolidates to 10 and holds for 85 frames" was a claim about a count. Ten regions every frame is equally consistent with ten stable regions and with regions being born and dying every frame while the total happens to stay 10. A per-frame component count cannot tell those apart.
component_overlap.pyintersects each component's voxel set with every component's voxel set in the next frame, inherits lineage by largest shared volume, and records births, deaths, merges and splits.The set is stable, not merely the count, and consolidation is monotone by merge — no region ever dies and none ever splits after MCS 15. The final state is adjacency-independent; only the frame at which it is reached is not.
A correction to the existing README
Both adjacency rules are now reported side by side. The count stabilises permanently at MCS 15 under point-sharing and MCS 17 under face-sharing; the previous revision gave only the point-sharing column and wrote "15 → 100: 10, flat", which is false under face-sharing (13 at MCS 15, 12 at 16). That is the third convention-dependent number in this bundle, after the slice-plane coordinate and the threshold predicate.
Portable read-only audit
vti_read.pyreads the.vtiin numpy alone — the exporter writes uncompressed appended binary, so no VTK is needed. An audit of the renderer's output should not require the renderer's toolchain. Validated before being trusted for anything new: summingoccupied_above_thresholdreproducesmetrics.jsonon 101/101 frames, zero mismatches.🤖 Generated with Claude Code
https://claude.ai/code/session_01V1GLLCekStea7UHjRF51aw