test(parity): accumulated arrays vs pycocotools - #15
Merged
Merged
Conversation
- Add `test_accumulated_arrays_match_pycocotools_bit_for_bit` to `scripts/test_parity.py`: compares `eval["precision"]`, `eval["recall"]`, and `eval["scores"]` against pycocotools as `uint64` views, so a single tie broken the wrong way fails with the first differing `(t, r, k, a, m)` instead of averaging into a headline metric. - Add `_tie_heavy_dataset()`: 12 images x 3 categories, 14 detections per cell, scores quantized to two decimals so equal scores span images, one hand-built cross-image TP/FP tie at 0.70, and a category with no ground truth. Cells exceed the middle `maxDets` cap so per-image truncation fires. - Add `_Lcg`, a fixed 64-bit LCG, so the ties do not depend on a seed anyone can change. - Add `_accumulated_arrays()`: runs both tools with `maxDets=[1, 10, 100]` and, in a second case, re-assigns the caps to `[10, 1]` between `evaluate()` and `accumulate()`. - Checked to fail on three injected violations in `accumulate()`: an unstable sort, per-image truncation at `max_det + 1`, and reversed cell concatenation order. --- Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Borda
force-pushed
the
test/accumulate-array-parity
branch
from
September 16, 2026 09:54
3c23031 to
9e524ac
Compare
derekallman
added a commit
that referenced
this pull request
Sep 25, 2026
… perf PRs Post-merge cleanup of #11, #12, #13, and #15. No behavior change: every Rust suite, the fast pytest suite, and real-data parity on val2017 pass bit-identically before and after. - Extract one `tie_heavy_datasets()` builder and one `Lcg` into the integration-test helpers; both accumulate tests call it instead of carrying their own copy, and it now matches the seed and cell size of `_tie_heavy_dataset` in scripts/test_parity.py, with cross-references in both directions. - Borrow the shared score order for the unfiltered `maxDets` slot instead of copying it once per work item. - Drop the dead `nd` counter and early return from `precision_recall_curve_of_order_into`; the fall-through already returns the same value. Remove unused derives on `Tally`. - Replace comments that narrated the pre-merge code or a rejected experiment with the constraint they were standing in for, and fix a comment that wrongly claimed `rand` is not a dependency. - Test nits: compare slice metrics straight from the `BTreeMap`, turn a five-parameter closure into a nested fn, hoist a loop-invariant assert. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0122uhTHqxEPRhN3cEZoSaaU
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
A fast parity test that compares the accumulated
precision,recall, andscoresarrays against pycocotools bit for bit, on a dataset built so that ranking order is observable. Test only; no source change.Branch
test/accumulate-array-parityis cut frommain(1bfe6ad) and is independent of theperf/*branches. It passes onmainand onperf/A1.Motivation
Every existing test in
scripts/test_parity.pycompares the 12 headline metrics. Each of those averages 1,010 precision points, so one score tie broken in the wrong order moves AP by about 1e-4 — inside the tolerance the val2017 parity run accepts — and can cancel against another tie in the same average. The fast suite also had no dataset in which scores collide across images whilemaxDetstruncates each image's list, which is exactly the situation whereaccumulate()'s stable sort and per-image truncation decide the curve.That leaves the three open
accumulate()performance PRs (perf/A1,perf/A2,perf/A3) anchored to pycocotools only throughjust fuzz(7 minutes, four-decimal scores so ties are rare) andjust parityon val2017 (data/is local-only). Their own Rust tests use a fresh hotcoco run as the oracle, which proves the optimized path equals the unoptimized path, not that either equals the reference. This test closes that gap in 0.13 s, in CI (ci.ymlrunsscripts/test_parity.py).Change
scripts/test_parity.py: newtest_accumulated_arrays_match_pycocotools_bit_for_bit, with three helpers._Lcg: a fixed 64-bit linear congruential generator, so the ties never depend on a seed anyone can change._tie_heavy_dataset(): 12 images × 3 categories, 14 detections per (image, category) cell, scores quantized to two decimals (98 distinct values over 506 detections), one hand-built cross-image tie (a TP in image 1 and an FP in image 2, both scoring exactly 0.70), and a third category with no ground truth. Every cell holds more detections than the middlemaxDetscap, so per-image truncation fires._accumulated_arrays(): runsevaluate()+accumulate()through both tools and returns the twoevaldicts. Accepts a secondmaxDetslist to assign between the two calls — pycocotools'accumulate(p)idiom, which leaves cells holding more detections than the current cap.uint64views formaxDets=[1, 10, 100]and forevaluate([1, 10, 100])→accumulate([10, 1]). A failure names the first differing(t, r, k, a, m)and both values.Correctness evidence
The test was checked to fail on three violations injected into
main'saccumulate(), each rebuilt and run:inds.sort_by→inds.sort_unstable_by.min(max_det)→.min(max_det + 1)for eval_img in &evals→evals.iter().rev()(tie order flipped)A fourth attempt — swapping the comparator arguments and appending
.reverse()— is a semantic no-op and passed; it is not counted.With the injections reverted:
pytest scripts/test_parity.py crates/hotcoco-pyo3/tests→ 108 passed, 24 skipped (107 before).ruff checkandruff format --checkclean. Pre-commit hook (fmt, clippy, cargo test, ruff) green on the commit.Not in this PR
None-gate divergence from pycocotools (len(gt)==0 and len(dt)==0; affectseval["scores"]at recall threshold 0 for non-"all" area ranges) is not exercised.scoresmatching exactly here says nothing about that case.accumulate()and are not separately covered here;just fuzzcovers them at tolerance.