test(vetting): run the CellProfiler oracle — five CellProfiler rows proved, one disproved - #425
test(vetting): run the CellProfiler oracle — five CellProfiler rows proved, one disproved#425darkclad wants to merge 5 commits into
Conversation
…s proved, one disproved The six morphology rows reading oracle=cellprofiler carried no evidence: no version, no config, no generator, so nothing in the tree distinguished a CellProfiler number from a Nyxus one and the vetted verdict rested on the tracker alone (not_covered.md section C). gen_morphology_cellprofiler.py closes that by running the real cellprofiler.modules.MeasureObjectIntensity (4.2.8) on the shape2d fixture. Five reproduce and are now vetted for real, with tool, version, module, recipe and generator recorded: MASS_DISPLACEMENT and the mean, max, min and integrated edge intensities. The agreement is exact rather than approximate because both tools select the same edge pixels -- CellProfiler's edge is find_boundaries(mode="inner"), so an object pixel is an edge pixel unless all four of its N/S/E/W neighbours share its label, which on this fixture is 18 of the 26 ROI pixels summing to 753 against the ROI's 1048. The residual is 5.2e-8 relative and is entirely CellProfiler storing the image as float32. The assertion band tightens from 0.1% to 1e-6 relative to match that measurement. The old band was inherited from the shared snapshot helper and was loose enough to pass the estimator gap described below, which is the kind of tolerance that makes a test unable to fail on the thing it claims. Mutation-checked: a 8.8e-6 perturbation of the mean fails. EDGE_STDDEV_INTENSITY does not reproduce and is demoted to regression. Over the identical 18 pixels Nyxus divides the variance by n-1 (Moments4::std, a helper shared across features) and CellProfiler by n, so the two differ by exactly sqrt(n/(n-1)) = 1.0289915 at n=18: 16.769194 against 16.296728. That is a definitional gap, not a tolerance one. Its row now reads regression with candidate_oracle=cellprofiler and flag=estimator-divergence, and its snapshot moves to test_2d_morphology_regression.h so the _cellprofiler file holds only assertions CellProfiler actually backs. The generator checks the ratio as an identity rather than printing it, so the relationship cannot drift unnoticed in either direction. The CellProfiler comparison needs oracle-side settings morphology.shape2d_native does not carry -- the image fed as raw/255, the fixture padded with background -- so it gets its own recipe id rather than sharing that one, the same split radial.cellprofiler_8bin already uses. The six rows also gain the test_name and benchmark SPEC 3 asks for, which they could not before: the assertion they name now exists as one gtest case per status. coverage_report.md is regenerated from the registry, which is where the one-feature drop comes from (morphology 77 -> 76, headline 606 -> 605). It is the single demotion above and nothing else. Which estimator Nyxus means to report is a src/nyx question and is left open here.
… the registry coverage_report.md is generated from oracle_coverage.csv and says so in its own header, but nothing verified that it still matched, so "re-run --write after editing the registry" was remembered rather than enforced. It has been forgotten before: PolusAI#422 demoted ten GLCM matlab rows without regenerating, and for as long as that stood the report in the tree claimed glcm 118/118 vetted against a registry holding 108, with the headline overstating coverage by ten features. That instance has since been regenerated on main; what has not been closed is the hole that let it happen, and every family PR since has had to remember on its own. --check now compares the rendered text to the file on disk. A registry with no report beside it stays clean, since the self-tests validate ad-hoc registries in tmp_path. The self-test covers both directions: freshly written passes, registry-moves-on fails, and absent report passes. Mutation-checked against the real tree -- editing one figure in the report makes --check exit 1. SPEC 3.1 records the rule and why a stale report is a correctness problem rather than a tidiness one.
37b2442 to
5e0672a
Compare
|
|
||
| # tests/test_data.h: shape2d_morphology_intensity and shape2d_morphology_mask, as rows y=0..7 | ||
| # of columns x=0..7. A single irregular concave ROI with one interior hole at (x=3, y=3). | ||
| INTENSITY = [ |
There was a problem hiding this comment.
Blocker: This is a second copy of the committed fixture. The generator never reads tests/test_data.h, so a fixture edit can change the gtest input while this oracle stays green. Please parse the named arrays from test_data.h (or move them to one shared machine-readable fixture); as written, this does not actually run CellProfiler on the checked-in fixture it claims to consume.
| OBJECTS = "objs" | ||
|
|
||
| # goldens CellProfiler reproduces -- pinned in tests/test_2d_morphology_cellprofiler.h | ||
| GOLDENS = { |
There was a problem hiding this comment.
Blocker: These are a second copy of the C++ goldens, and the script never opens test_2d_morphology_cellprofiler.h. The registry statement that this generator “re-verifies every pin in the header it feeds” is therefore false. Please read the reference table from the header and require forward and reverse key equality; apply the same binding to the Nyxus stddev pin so an edited, missing, or extra pin must fail.
| ASSERT_TRUE(morphology_2d_cellprofiler_ref_vals.count(feature_name) > 0) << feature_name; | ||
| ASSERT_TRUE(agrees_gt(fvals[static_cast<int>(feature)][0], morphology_2d_cellprofiler_ref_vals.at(feature_name), 1000.0)); | ||
| // 1e6 -> a 1e-6 relative band, set from the measured 5.2e-8 float32 residual rather than left | ||
| // at the 0.1% the shared snapshot helper used; a band that loose would pass the sqrt(n/(n-1)) |
There was a problem hiding this comment.
Blocker: This explanation is mathematically wrong. sqrt(18/17) - 1 = 2.899%, while the old band was 0.1%; the CellProfiler value would have missed by about 29 times that band. The old test passed because its golden was the Nyxus snapshot, not because the tolerance absorbed the estimator gap. Tightening to 1e-6 is justified by the measured residual, but please correct this comment and the same claim in the PR body/registry notes.
| - `PERIMETER` is **not** comparable on this fixture (Nyxus chain-code walk 26.935 vs skimage 12.657 | ||
| on a 26-pixel object with a hole); it is vetted at `morphology.perimeter_circles` instead. | ||
|
|
||
| ## morphology.cellprofiler_edge_intensity |
There was a problem hiding this comment.
Blocker: This recipe describes the CellProfiler side but not the exact Nyxus production cell. The test uses regular in-RAM ContourFeature::calculate() with SINGLEROI=false; the whole-slide and osized_calculate() paths are separate reachable paths. Please narrow this recipe to the cell actually vetted and record the remaining path dispositions in the SPEC-required morphology matrix (tests/vetting/matrix/morphology.md, currently absent).
There was a problem hiding this comment.
Thanks — the fixture/header binding, tolerance explanation, registry source, and missing-report enforcement are fixed on c12810b. This thread still has one substantive SPEC §5 gap:
tests/vetting/config_recipes.md:221-226names onlyContourFeature::calculate(), but the same oracle test also assertsMASS_DISPLACEMENT, which is produced byBasicMorphologyFeatures::calculate()(with its ownosized_calculate()path). It does not inherit a contour-builder cell.tests/vetting/matrix/morphology.md:24-25labels two reachable production cellsNOT MEASURED. SPEC §5 permitsVALID→ oracle,VALID-BUT-PRODUCTION-ONLY→ regression, orINVALID→ dropped with a reason; it explicitly requires every cell to have a disposition. The file itself says neither reachable path is asserted.
Please give the five contour statistics and MASS_DISPLACEMENT their actual production-path rows, measure/classify the reachable cells, and add the corresponding registry-backed assertions. The current matrix is useful analysis, but “not measured” does not yet satisfy the matrix requirement.
There was a problem hiding this comment.
Correction: I overstated the second half of my follow-up. This PR now scopes its vetted claim to the in-RAM SINGLEROI=false assertion and explicitly records the other paths as unmeasured. Completing those paths is follow-up matrix work; it does not invalidate the five CellProfiler comparisons, and I am not asking for new path tests or registry rows in this PR.
Only two non-blocking wording corrections remain: name BasicMorphologyFeatures::calculate() as the producer of MASS_DISPLACEMENT, and change the title from “five edge-intensity rows” to “five CellProfiler rows” (the proved set is four edge-intensity features plus mass displacement).
| 2D,WEIGHTED_CENTROID_Y,morphology,vetted,matlab,agreed,morphology.shape2d_native,rel=1e-3,test_2d_morphology_matlab.h;test_2d_morphology_regression.h,,,,audit,"vetted vs GNU Octave 11.3.0 + image 2.20.0 regionprops on the shape2d fixture, recipe morphology.shape2d_native; agreement 0 to ~1e-15 on every row (gen_morphology_matlab.m verifies all 33 pins). MATLAB applies the same +1/12 pixel finite-size second-moment correction Nyxus does, which is why the ellipse triple vets here and not against skimage.",, | ||
| 2D,WEIGHTED_CENTROID_X,morphology,vetted,matlab,agreed,morphology.shape2d_native,rel=1e-3,test_2d_morphology_matlab.h;test_2d_morphology_regression.h,,,,audit,"vetted vs GNU Octave 11.3.0 + image 2.20.0 regionprops on the shape2d fixture, recipe morphology.shape2d_native; agreement 0 to ~1e-15 on every row (gen_morphology_matlab.m verifies all 33 pins). MATLAB applies the same +1/12 pixel finite-size second-moment correction Nyxus does, which is why the ellipse triple vets here and not against skimage.",, | ||
| 2D,MASS_DISPLACEMENT,morphology,vetted,cellprofiler,agreed,morphology.shape2d_native,,test_2d_morphology_cellprofiler.h,,,,tracker,,, | ||
| 2D,MASS_DISPLACEMENT,morphology,vetted,cellprofiler,agreed,morphology.cellprofiler_edge_intensity,rel=1e-6,test_2d_morphology_cellprofiler.h,test_2d_morphology_cellprofiler.h,,,generator,"CellProfiler 4.2.8 MeasureObjectIntensity on the shape2d fixture, recipe morphology.cellprofiler_edge_intensity; the edge set is find_boundaries(mode=inner), 18 of the 26 ROI pixels summing to 753 against the ROI's 1048 -- the same pixels Nyxus walks, which is what makes this an exact comparison. Measured agreement 5.2e-8 relative, the residual being CellProfiler's float32 image storage rather than a disagreement; asserted at 1e-6 relative in test_2d_morphology_cellprofiler.h, which is above that residual and far under the sqrt(n/(n-1)) gap that disqualified EDGE_STDDEV_INTENSITY. Generator tests/vetting/oracles/gen_morphology_cellprofiler.py re-verifies every pin in the header it feeds.",TEST_NYXUS.TEST_2D_MORPHOLOGY_EDGE_INTENSITY_CELLPROFILER,bench_shape8_concave_holed |
There was a problem hiding this comment.
Blocker: source=generator is not a SPEC §3 source value; the defined values are in-tree, tracker, and audit. These rows name a runnable gtest assertion, so the source should be in-tree, with the offline generator cited in provenance/notes. This row also says the generator re-verifies the header, which is not true until the generator actually reads it.
| """[] if coverage_report.md matches what the registry renders to, one error if it does not. | ||
| A missing report is not an error -- --check runs against ad-hoc registries in the self-tests, | ||
| which have no report beside them.""" | ||
| if not os.path.exists(report_path): |
There was a problem hiding this comment.
Blocker: This makes deleting the canonical coverage_report.md pass --check, undercutting the new SPEC promise that the committed report is enforced. Please distinguish ad-hoc registry use from the canonical default: a missing canonical report must be an error. Add that deletion as a negative test rather than explicitly blessing it.
vjaganat90
left a comment
There was a problem hiding this comment.
The core result is valuable and likely correct: direct MeasureObjectIntensity output supports four edge-intensity statistics plus MASS_DISPLACEMENT, and demoting EDGE_STDDEV_INTENSITY for the population-vs-sample estimator difference is the honest disposition.
I cannot approve this version because the evidence chain is still self-referential and several SPEC or mathematical claims are incorrect; the inline blockers give the exact fixes.
Apart from those inline issues, please rewrite the PR description against the current rebased diff:
- 11 changed files, not 9.
- Coverage moves 606 → 605 and morphology 77 → 76; the current diff contains no ten-feature GLCM drop.
- The
3ROBUST_MEANmalformed-row note is stale; that row is already fixed. - Say five CellProfiler rows rather than five edge-intensity rows: one of the five is
MASS_DISPLACEMENT. - Correct the tolerance story: the old failure was a Nyxus snapshot presented as a CellProfiler golden. A 0.1% band could not absorb the measured 2.899% estimator gap.
The historical #422 incident is still useful motivation for the report checker, but it should not be described as a change in this diff. Once the generator reads the repository fixture and header, the recipe/matrix and registry source are SPEC-compliant, deletion of the canonical report fails, and the description matches the branch, the central five-vetted/one-regression conclusion will be well supported.
…s it claims to check Review round on PolusAI#425. Six blockers, each verified against the tree before being acted on; all six held. gen_morphology_cellprofiler.py held its own copy of the shape2d fixture and its own copy of the goldens, and opened neither test_data.h nor the headers. So a fixture edit could move the gtest input while the oracle stayed green, and the registry's claim that the generator "re-verifies every pin in the header it feeds" was simply false. It now parses both fixtures out of test_data.h and both reference tables out of the headers, and enforces key equality in BOTH directions: a pin that disagrees fails, a pin for a feature this recipe does not vet is EXTRA, a feature CellProfiler vets with no pin is UNPINNED, and EDGE_STDDEV_INTENSITY appearing in the cellprofiler header is MISPLACED. The Nyxus stddev pin is read from the regression header too, so the sqrt(n/(n-1)) identity is checked against the number the C++ side actually asserts. Five mutations of the tree -- edit a pin, delete a pin, add an unbacked pin, edit the stddev pin, edit the fixture -- each exit 1. This is the binding gen_neighbor_cellprofiler.py already used; this generator should have followed it. The comment justifying the tightened band was wrong and is corrected. It said the old 0.1% band was loose enough to pass the estimator gap. The gap is 2.9% relative, 29 times that band, so 0.1% would have caught CellProfiler's value. What let the old assertion pass is that its golden was the Nyxus number: a test labelled cellprofiler judging Nyxus against itself, which is the condition not_covered.md section C exists to surface and a sharper problem than a loose tolerance. Corrected in the header, both registry notes and section C, which now states the real reason explicitly because the wrong one is what a reader reaches for first. Tightening to 1e-6 stands on the measured 5.2e-8 residual alone. source=generator is not a SPEC 3 value -- the set is in-tree / tracker / audit -- and those six rows were the only ones in 900 using it. They name a runnable gtest case, so they read in-tree and the generator is cited in notes. It survived because validate_rows checked status and oracle against closed sets and never source, so an invented token read as meaningful; ALLOWED_SOURCES closes that, with a self-test over valid, invented and blank. A missing coverage_report.md passed --check, which made deleting the file the one edit the new staleness rule could not see. report_staleness now takes a canonical flag: --report defaults to None and resolves to DEFAULT_REPORT, so a missing CANONICAL report is an error while a missing ad-hoc one still claims nothing -- the self-tests validate registries in tmp_path with no report beside them. Negative test added for the deletion. The recipe now names the Nyxus production cell it vets, ContourFeature::calculate() at SINGLEROI=false, rather than describing only the CellProfiler side, and says that the whole-slide and osized_calculate paths are not covered. matrix/morphology.md records all three cells, which SPEC 5.1 requires and the family did not have. Writing it surfaced two untriaged points, recorded there as open questions rather than findings because this vetting did not run those paths: buildWholeSlideContour() pushes the four AABB corners at aux_max, so every edge statistic at SINGLEROI=true is degenerate by construction; and osized_calculate() computes PERIMETER as a contour pixel count where calculate() sums Euclidean step lengths, with no empty-contour guard where calculate() has one. The two paths are required to agree, so that is a cell worth measuring. Still no src/nyx change.
|
One small description cleanup remains: the title still says “five edge-intensity rows proved,” but the proved set is |
vjaganat90
left a comment
There was a problem hiding this comment.
LGTM - Just fix the MASS_DISPLACEMENT function attribution and wording.
…n out-of-core defect Second review round on PolusAI#425. Both points held: the recipe named only ContourFeature's cell while the same oracle test also asserts MASS_DISPLACEMENT, and matrix/morphology.md left two reachable production cells labelled NOT MEASURED, which is not one of the three dispositions SPEC 5.1 defines. MASS_DISPLACEMENT does not inherit a contour-builder cell. It is computed by BasicMorphologyFeatures::calculate() from the geometric and intensity-weighted centroids, reads no contour, and has its own osized_calculate(). It gets its own axis in the matrix, landing on the same dispositions by measurement rather than by inheritance. Both NOT MEASURED cells are now measured, per SPEC 5.2's rule that verdicts are measured rather than hand-labelled, on one 64x64 single-disk fixture so the three cells are comparable numbers on one shape: feature segmented out-of-core whole-slide PERIMETER 131.88225099 112.0 256.0 MASS_DISPLACEMENT 2.75261401 2.75261401 3.34531182 EDGE_MEAN_INTENSITY 257.0 257.0 397.0 EDGE_STDDEV_INTENSITY 98.12659593 98.12659593 0.0 EDGE_MAX / EDGE_MIN 397.0 / 117.0 397.0 / 117.0 397.0 / 397.0 EDGE_INTEGRATED_INTENSITY 28784.0 28784.0 1588.0 The whole-slide cell is VALID-BUT-PRODUCTION-ONLY and degenerate exactly as the source predicts: buildWholeSlideContour() pushes the four AABB corners each carrying aux_max, so min == max == mean == the image maximum, stddev is 0 and integrated is 4*maximum. It is a different quantity from the per-object edge statistics CellProfiler vets, which is why the vetted rows state their scope as the segmented in-RAM path. The out-of-core cell agrees exactly for the five EDGE_* statistics and MASS_DISPLACEMENT -- and does NOT agree for PERIMETER. osized_calculate() sets fval_PERIMETER = K.size(), the contour pixel count, where calculate() sums Euclidean step lengths: 112.0 against 131.88225099390849, ~15%, and the out-of-core value being an integer is what identifies the definition rather than accumulation as the cause. CLAUDE.md requires the two paths to produce identical values, so this is a defect and not a convention. It is characterized here, not fixed: the fix is a src/nyx change and PERIMETER is vetted under a different recipe. Why nothing caught it: test_2d_ooc_invariant.py has asserted *ALL_MORPHOLOGY* equality across the two paths since it was written, and passes. Its fixture is a full-image RECTANGLE, and around a rectangle every contour step is an axis-aligned unit step, so the pixel count and the Euclidean sum are the same number. The invariant is sound; that shape cannot discriminate. A disk separates them. The general form -- a path-equality fixture symmetric in the axis under test proves nothing about that axis -- is recorded in not_covered.md section G, together with a note to audit the other _ooc_ fixtures on the same grounds. Three assertions and seven registry rows back the cells: a discriminating-fixture invariant for the six features that agree, test_2d_ooc_regression.py pinning the PERIMETER divergence so a correct fix must break it, and test_2d_morphology_regression.py pinning the whole-slide cell and asserting it differs from the segmented one so the CellProfiler evidence cannot be read across. The coverage report is unchanged: these are regression rows for features whose rollup is already vetted. Still no src/nyx change.
|
Thank you for doing the measurement work — my correction crossed this commit while it was in flight. I had over-scoped the matrix requirement, so please move That is also the safest technical split. The new commit expands this PR from 12 to 15 files and introduces a separate out-of-core The disk fixture and the |
15 files, all under
tests/. Nosrc/nyxchange —git rev-parse HEAD:srcequalsgit rev-parse upstream/main:src.Rebased onto current
mainand reworked against it (the branch was 177 commits behind), then tworeview rounds. Both rounds found real problems; the second turned up a defect in
src/nyxthat noexisting test could see.
The defect this PR started from
Six morphology rows read
status=vetted, oracle=cellprofilerand carried no evidence for it:no version, no config, no generator. Nothing in the tree distinguished a CellProfiler number from a
Nyxus one, so the verdict rested on the tracker alone.
not_covered.md§C listed it as the oneentry where not even the provenance was established — as against the MATLAB entries, where the
values are the oracle's and only the recipe is missing. That is why it was worth running first.
What the oracle found
tests/vetting/oracles/gen_morphology_cellprofiler.pydrives the realcellprofiler.modules.MeasureObjectIntensity(4.2.8 / cellprofiler-core 4.2.8.1 / centrosome 1.2.3)on the
shape2d_morphology_{mask,intensity}fixture.Five reproduce and are now vetted for real —
MASS_DISPLACEMENTand the mean, max, min andintegrated edge intensities:
The agreement is exact rather than approximate: CellProfiler's edge is
find_boundaries(mode="inner"), so an object pixel is an edge pixel unless all four N/S/E/Wneighbours share its label — 18 of the 26 ROI pixels here, summing to 753 against the ROI's 1048,
the same set Nyxus walks. The 5.2e-8 residual is entirely CellProfiler's float32 image storage
(
raw/255 → float32 → ×255), not a disagreement.The band tightens from 0.1% to 1e-6 relative, set from that measurement rather than inherited
from the shared snapshot helper. Mutation-checked: an 8.8e-6 perturbation of the mean fails.
EDGE_STDDEV_INTENSITYdoes not reproduce and is demoted toregression. Over the identical 18pixels Nyxus divides the variance by n-1 (
Moments4::std()) and CellProfiler by n, so the twodiffer by exactly
sqrt(n/(n-1))= 1.0289915 at n=18: 16.769194 against 16.296728. Adefinitional gap, not a tolerance one. Its snapshot moves to
test_2d_morphology_regression.hsothe
_cellprofilerfile holds only assertions CellProfiler actually backs. The generator checks therelationship as an identity — CP against the population std, Nyxus against the sample std, the
ratio against
sqrt(n/(n-1))— so it cannot drift unnoticed in either direction.The generator is bound to what it claims to check
It originally held its own copy of the fixture and its own copy of the goldens, and opened neither
test_data.hnor the headers — so the registry's claim that it "re-verifies every pin in the headerit feeds" was false, and a fixture edit could move the gtest input while the oracle stayed green.
It now parses both fixtures out of
test_data.hand both reference tables out of the headers, andenforces key equality in both directions. Five mutations of the tree each make it exit 1:
FAIL EDGE_MEAN_INTENSITY … rel=1.57e-06UNPINNED EDGE_MIN_INTENSITY: CP vets it but the header pins nothingEXTRA EDGE_STDDEV_INTENSITY … this recipe does not vet itIDENTITY BROKEN -- investigatetest_data.hFAIL EDGE_INTEGRATED_INTENSITY … rel=0.0106This is the binding
gen_neighbor_cellprofiler.pyalready used, whose docstring makes the pointthat a local copy "would only ever compare this script against itself".
The config matrix — and a
src/nyxdefect it exposedtests/vetting/matrix/morphology.mdis new; SPEC §5.1 requires one per family and morphology hadnone. It covers the
ContourFeaturegroup plusMASS_DISPLACEMENT, which does not inherit acontour-builder cell — it is
BasicMorphologyFeatures::calculate(), reads no contour, and has itsown
osized_calculate(), so it gets its own axis.Every cell is measured, per SPEC §5.2, on one 64×64 single-disk fixture:
PERIMETERMASS_DISPLACEMENTEDGE_MEAN_INTENSITYEDGE_STDDEV_INTENSITYEDGE_MAX_INTENSITYEDGE_MIN_INTENSITYEDGE_INTEGRATED_INTENSITYWhole-slide is
VALID-BUT-PRODUCTION-ONLY:buildWholeSlideContour()does not trace a boundaryat all — it pushes the four AABB corners each carrying
aux_max, so min = max = mean = the imagemaximum, stddev = 0, integrated = 4 × maximum. A different quantity from the per-object edge
statistics CellProfiler vets, which is why the vetted rows state their scope as the segmented
in-RAM path.
Out-of-core agrees exactly for the five
EDGE_*andMASS_DISPLACEMENT— and not forPERIMETER.osized_calculate()setsfval_PERIMETER = (StatsInt) K.size(), the contour pixelcount, where
calculate()sums Euclidean step lengths: 112.0 against 131.882, ~15%, and theout-of-core value being an integer is what identifies the definition rather than accumulation as the
cause.
CLAUDE.mdrequires the two paths to produce identical values, so this is a defect, nota convention.
Why nothing caught it.
test_2d_ooc_invariant.pyhas asserted*ALL_MORPHOLOGY*equalityacross the two paths since it was written, and it passes. Its fixture is a full-image rectangle,
and around a rectangle every contour step is an axis-aligned unit step — so the pixel count and the
Euclidean sum are the same number, and the one feature that differs cannot appear. The invariant is
sound; the shape cannot discriminate. The general form is recorded in
not_covered.md§G: apath-equality fixture symmetric in the axis under test proves nothing about that axis, with a note
to audit the other
_ooc_fixtures on the same grounds.The defect is characterized, not fixed — the fix is a
src/nyxchange andPERIMETERis vettedunder a different recipe.
tests/python/test_2d_ooc_regression.pypins the divergence and a correctfix must break it, at which point
PERIMETERfolds back into the invariant's feature list.Also in this PR
--checkfails whencoverage_report.mdno longer matches the registry, or is missing. Thereport is generated and says so in its own header, but nothing verified it still matched, so "re-run
--write" was remembered rather than enforced — and it has been forgotten: #422 demoted ten GLCMmatlabrows without regenerating, and the report claimedglcm118/118 against a registry holding108 until someone happened to regenerate.
--checknow re-renders and compares, and treats amissing canonical report as the same failure, since deleting the file would otherwise be the one
edit that passes. Only the
--reportdefault must exist; a path named on the command line isad-hoc, which the throwaway-registry self-tests rely on. Mutation-checked in both directions.
sourceis validated against its SPEC §3 set.validate_rows()checkedstatusandoracleagainst closed sets but never
source, so an invented token read as meaningful — six rows saidgenerator, the only ones in 900. Same shape of hole as the report one.A separate config recipe. The CellProfiler comparison needs oracle-side settings
morphology.shape2d_nativedoes not carry (raw/255, background padding), so it takes its ownmorphology.cellprofiler_edge_intensityid — the splitradial.cellprofiler_8binalready uses —and names the Nyxus production cell on both axes.
What the rebase absorbed
coverage_report.mddrift correction.mainregenerated it since, so that is droppedrather than replayed. What was not closed is the hole that let it happen, which this PR keeps.
The report change here is one row and one headline figure: the single demotion above
(morphology 77 → 76, headline 606 → 605).
test_nameandbenchmark;check_coverage.pygainedbenchmark/test_name/config_recipevalidators. The six rowswere rewritten against that schema and now carry the
test_nameandbenchmarkSPEC §3 asks for.not_covered.md§C closes the entry the waymainnow closes them — row removed from the"not yet satisfied" table, finding written up in prose — rather than struck through under a
heading that says otherwise.
Verification
ALL CHECKS PASSED, exit 0 — 5 verified, 0 failed, 0 unproducible, 0 unpinned,identity holds; 5/5 tree mutations caught (table above)
TEST_2D_GABOR_GPU_RUNS_MECHANICSon a CPU-only build)-fsanitize=address,undefined -fno-sanitize-recover=undefined, Linux):gtest 883 passed / 1 skipped of 884, 0 diagnostics, wrapper exit 0 — the gate log's md5
block matches the tree on every changed file. The four-test delta from Windows is the DICOM
HU-loader tests, absent from a
NOEXTRAS=ONbuild. Instrumentedpytest tests/python/:95 passed / 1 skipped / 7 Arrow, again 0 diagnostics, with
test_nonimq_wsi_scalabilitydeselected — it aborts on a pre-existing UBSan finding,
src/nyx/features/glszm.cpp:426: signed integer overflow: 46341 * 46341 cannot be represented in type 'int', reached through the2D whole-slide path. Not attributable to this PR: the
srctree hash is identical toupstream/main, so the instrumented module is built from the same bytes.pytest tests/python/: 96 passed / 1 skipped (7 pre-existing Arrow failures of atiff-only build); vetting self-tests 14/14
check_coverage.py --checkandcheck_test_names.py --checkclean;--writereproduces thecommitted report byte for byte
--shortstatand--ignore-all-space --shortstatagree, so no whitespace churnCommits
test(vetting): run the CellProfiler oracle -- five edge-intensity rows proved, one disprovedtest(vetting): fail --check when coverage_report.md no longer matches the registrytest(vetting): bind the CellProfiler generator to the fixture and pins it claims to checktest(vetting): measure the three morphology contour cells, and find an out-of-core defectCommits 3 and 4 are the two review rounds; every blocker was verified against the tree before being
acted on, and all of them held.
Left open, deliberately
PERIMETERdefect. Asrc/nyxfix, characterized here and recorded innot_covered.md§G.Moments4::std()is shared across features, so Nyxusreports the sample estimator wherever it is used while the tools it is vetted against report the
population one. A
src/nyxdecision, recorded in §C and pinned as an identity by the generator sothe answer cannot drift while it waits.
matrix/morphology.mdcovers theContourFeaturegroupand
MASS_DISPLACEMENT— the features this PR makes a claim about. Hull, caliper, moments-fitellipse and fractal have no rows yet, and the file says so rather than reading as complete.