VV: Erode Dilate Bad Data - #1687
Conversation
Full V&V Review — ErodeDilateBadDataReview methodology: every claim in Blocking: the direction fix is incorrect and regresses default behavior
A/B evidence (all runs on this branch's build):
So the PR breaks the default all-on case that
Blocking: the test oracle is circularThe V&V report classifies the oracle as Class 1 (Analytical), hand-traced. That claim does not hold: the hardcoded
Tests: gaps and correctness
Preflight / filter code
V&V document corrections (beyond the oracle section)
Algorithm quality (pre-existing, but in scope for a V&V pass)
Verified-correct claims (for the record)The following report claims were independently confirmed: 5/5 test cases pass with exactly 1883 assertions on this branch; legacy UUID |
5a009f4 to
f465510
Compare
imikejackson
left a comment
There was a problem hiding this comment.
See comments for feedback
f465510 to
d3217c7
Compare
* Refactored the algorithm for easier readability. Most ne4w functions are fully documented for inline readability in an IDE. * Updated temporary data structs from std::vector to AbstractDataStores to account forlarge OOC data.
* Clang-format
* Fixed a bug where x/y/z directions could not be turned off in the algorithm. * Added tests for no direction and no geometry dimensions. * Added errors to ErodeDilateBadDataFilter for no direction enabled and missing ImageGeom dimensions. Add V&V docs Re-added SIMPL backwards compatibility testing
Values taken from DREAM.3D for A/B testing.
* Add ShouldCancel check in the algorithm. * Fix valid neighbor check to also check allowed directionality. * Fix ErodeDilateBadData exemplar data
6826192 to
42934ff
Compare
imikejackson
left a comment
There was a problem hiding this comment.
PR 1687 Re-Review — VV: Erode Dilate Bad Data
Re-review of head
42934ffcdagainst the 2026-07-24 review. Branch built and all 7 tests run locally; every prior checkbox re-verified against the tree.
Both blocking items are resolved — verified independently
Direction gating is now correct. adjustValidNeighbors masks the per-voxel isValidFaceNeighbor array with the right axis pairing instead of bitwise-ANDing face indices.
I re-derived the oracle from scratch rather than trusting the fix: read the legacy source (Source/Plugins/Processing/ProcessingFilters/ErodeDilateBadData.cpp, 6.5.172 checkout), wrote a standalone simulator of it, ran all 28 combinations (2 ops × 7 direction combos × 2 iteration counts) on this PR's 4×4×2 fixture, and diffed against the committed k_Exemplar* constants:
| Check | Result |
|---|---|
| Committed expected arrays vs. independent simulation of legacy 6.5.172 semantics | 28/28 exact (FeatureIds and Misc) |
| Committed expected arrays vs. simulation of this PR's semantics | 28/28 exact |
Prior review's 28/28 legacy-vs-&=-bug mismatch |
no longer reproduces |
The oracle is no longer circular. The arrays now discriminate direction combinations and agree with legacy element-wise. Whatever their stated provenance, they are independently correct.
Tests: 7/7 pass, 2039 assertions (55 + 939 + 939 + 67 + 9 + 3 + 27). Note: the run initially failed on a stale sibling DREAM3D_Plugins/SimplnxReview checkout claiming UUID 5e18a9e2-…, which has since moved to OrientationAnalysis — environmental, unrelated to this PR.
Bugs
-
Ignored Pathtest never executes the filter — it asserts nothing
File:ErodeDilateBadDataTest.cpp(SimplnxCore::ErodeDilateBadDataFilter Ignored Path)
The test callsfilter.preflight(...)and thenCheckPathIgnored, which compares the DataStructure against a freshly builtCreateTestData(). Since the filter never ran, nothing could have changed, so the 64REQUIREs pass vacuously.
Proven empirically: replacingMultiArraySelectionParameter::ValueType{ignoredPath}with{}(an empty ignore list) and rebuilding — the test still passes. It cannot distinguish "ignored" from "not ignored".
Fix: addauto executeResult = filter.execute(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result)beforeCheckPathIgnored. Consider also asserting thatFeatureIdsdid change, so the test proves the filter actually did work.
Knock-on: the V&V report's test-inventory row — "Confirms an array listed inIgnoredDataArrayPaths(Misc) is left untouched. Passes." — is not true as written.
Memory / Lifetime
No issues found. m_Neighbors is now const std::vector<int64>& (lifetime outlives taskRunner.wait()), and the dead m_FilterAlg member is gone. Both prior findings resolved.
CPU / Algorithm Efficiency
-
constexprwas dropped fromfaceNeighborInternalIdx(regression vs.develop)
File:Algorithms/ErodeDilateBadData.cpp(operator())
develophadconstexpr std::array<FaceNeighborType, k_NumFaceNeighbors> faceNeighborInternalIdx = initializeFaceNeighborInternalIdx();. This PR removedconstexpr— presumably needed by the earlier&=-mutation approach, which is gone. Nothing mutates it now; restoreconstexpr.
GenerateDataArrayList and MessageHelper are now hoisted above the iteration loop — prior finding resolved.
Naming Consistency
-
k_NoGeometryDimensionsshould bek_NoGeometryDimensionsError
File:ErodeDilateBadDataFilter.cpp(anonymous namespace)
Both areconstexpr int32now (prior finding resolved), but the sibling isk_NoDirectionsError. Match it. -
uint64 operation/int32 operationshould beChoicesParameter::ValueType
File:ErodeDilateBadDataTest.cpp((Erode) Expanded,(Dilate) Expanded,Ignored Path,No Direction)
These are immediately fed tostd::make_any<ChoicesParameter::ValueType>(operation). It compiles by implicit conversion, but declaring the parameter's own type is clearer and matchesk_Dilate/k_Erodeabove.
Const-Correctness
No issues found. const auto& imageGeom in preflight and const DataStructure& on both check helpers — both prior findings resolved.
Readability
-
CheckDilateOutputandCheckErodeOutputare two copies of the same 50-line if/else chain
File:ErodeDilateBadDataTest.cpp
Collapsing 28 near-identical functions into 2 is a big improvement over the prior revision, but these two differ only in which constants they name. A single table keyed on(operation, directions, iterations)removes the duplication and theREQUIRE(false)fallthrough entirely. -
REQUIRE(false)on an unhandled direction combination
File:ErodeDilateBadDataTest.cpp(CheckDilateOutput,CheckErodeOutput)
ProducesREQUIRE( false )with no explanation. UseFAIL("unhandled direction combination"). -
All-directions-off is skipped by a bare
returnafter the fixture is built
Files:ErodeDilateBadDataTest.cpp((Erode) Expanded,(Dilate) Expanded)
CreateTestData()runs and is thrown away for the invalid combination. Move the guard above it, and add aSUCCEED("at least one direction is required")so the skip is visible in the Catch2 output rather than silent.
Formatting: clang-format is not installed on the review machine, so the read-only format gate could not be run — flagging that rather than claiming a pass. By inspection all three files are tab-free, have no trailing whitespace, are ≤199 columns, and end with a newline (prior EOF finding resolved). The "acccount" typo is gone.
Robustness / Defensive
-
CheckPathIgnoreddereferences rawgetDataAs<Int32Array>results without a null check
File:ErodeDilateBadDataTest.cpp(CheckPathIgnored)
A typo inignoredPathyields a segfault instead of a test failure. Project convention isREQUIRE_NOTHROW(ds.getDataRefAs<Int32Array>(path))followed bygetDataRefAs. -
Missing
taskRunner.wait()after the FeatureIds transfer (Pre-existing bug surfaced by this PR.)
File:Algorithms/ErodeDilateBadData.cpp(operator())
The finaltaskRunner.execute(...)is correct only becausesetParallelizationEnabled(false)was called one line earlier. On the next iteration the runner is re-created, so nothing leaks today — but a trailingwait()makes the invariant local instead of implied.
Tests
-
k_TupleShape{4, 4, 2}is used as both the AttributeMatrix tuple shape and the geometry dimensions
File:ErodeDilateBadDataTest.cpp(CreateTestData)
geom->setDimensions(SizeVec3{4, 4, 2})is X=4, Y=4, Z=2, but DataArray/AttributeMatrix tuple shape is slowest-to-fastest ZYX (cf.RotateSampleRefFrameFilter.cpp:249,ReadNIfTIFileFilter.cpp:273). The correct tuple shape here is{2, 4, 4}. Harmless today only because both spell 32 tuples — use two separate constants. -
6 of 7 TEST_CASEs omit
UnitTest::CheckArraysInheritTupleDims(dataStructure)
File:ErodeDilateBadDataTest.cpp
Only the archive-based(Erode)test calls it. Adding it to theExpandedcases would also have caught the tuple-shape item above.
Prior test findings now resolved: all file-local helpers are inside the anonymous namespace (the ComputeFeatureRectTest.cpp::CreateTestData ODR hazard is gone); No Direction and No Dimensions assert -14601/-14602 explicitly and No Dimensions now enables all directions so it actually reaches the dimension check; and restoring the 6_6_erode_dilate_test.tar.gz exemplar test brings back the multi-type/multi-component copyTuple stride coverage (EulerAngles float32×3, Mask uint8, Phases int32) that the inline fixture alone does not provide.
Documentation
-
Verified commit 4437eacdadoes not exist in this repository (flagged last round, unchanged)
File:vv/ErodeDilateBadDataFilter.md(header table)
git cat-file -t 4437eacda→ "Not a valid object name". The commit whose message is "Fixing ErodeDilateBadData" is56b30c923. The branch is also still spelledvv/ErodeDialateBadData; the real name isvv/ErodeDilateBadData. Cite the PR head (42934ffcd) instead. -
Assertion count is stated as 2033; the actual count is 2039
File:vv/ErodeDilateBadDataFilter.md(Summary)
Measured per-test: 55 / 939 / 939 / 67 / 9 / 3 / 27. -
Internal contradiction about exemplar archives
File:vv/ErodeDilateBadDataFilter.md(At-a-glance → Test fixtures)
"InlineCreateTestData()— no exemplar archive for the automated tests" contradicts the test-inventory row describing(Erode)as "Exemplar-archive-based". Qualify it as "no archive for the Expanded sweep". -
Drop the "hand-traced" provenance claim for the 28 expected arrays
Files:vv/ErodeDilateBadDataFilter.md(Oracle),vv/deviations/ErodeDilateBadDataFilter.md(B1 Verification)
1,792 hand-traced values is not a credible claim, and it is not needed: the arrays match genuine 6.5.171 output, which is a stronger oracle. State plainly that they were regenerated from the legacy binary and verified element-wise — Class 2, corroborated. The "Class 1 in form, Class 2 in substance" hedging invites exactly the circularity question it is trying to answer. -
docs/ErodeDilateBadDataFilter.mdstill says erode ties are "chosen randomly"
File:src/Plugins/SimplnxCore/docs/ErodeDilateBadDataFilter.md(line 27)
The deviations doc correctly identifies this as inaccurate legacy language, but the user-facing doc was never corrected and is not in this PR's file list. Replace with the actual behavior (first-processed neighbor in[-Z,-Y,-X,+X,+Y,+Z]scan order wins; fully deterministic). While there, document the two new preflight errors.
Confirmed Correct (no action needed)
- Direction masking of the Erode
featureCountreset loop. This PR reuses the direction-maskedisValidFaceNeighborin the reset loop; legacy resets over boundary-valid neighbors and ignores the direction flags there. Simulating both variants across all 28 combinations gives identical output, and it is provably equivalent in general — the reset set is a superset of the increment set in both variants, sofeatureCountreturns to all-zeros after every bad voxel either way. Not a deviation. - Deferring the FeatureIds transfer to a second pass. Legacy interleaves FeatureIds with the other arrays in one pass and mutates
m_FeatureIdsas it goes. Equivalent here: erode only maps 0→>0 and dilate only >0→0,neighbors[]always points at a voxel whose relevant polarity is preserved by the operation, and each index is written at most once per pass — so neither transfer predicate can ever observe a changed value. neighborsis intentionally not reset between iterations — matches legacy, and the deviations doc records the investigation that established this.MessageHelpershared across parallel tasks. Each task gets its ownThrottledMessenger(independent timing state) over a sharedstd::shared_ptr<Messenger>;trySendMessageis the documented cross-thread path. Safe.- Cancel check placement (per Z-slice, inside the iteration loop) — real early-exit, negligible cost, ahead of legacy which has none.
- Preflight ordering —
-14601is checked before the geometry lookup,-14602after; both codes asserted by name in tests. FromSIMPLJsonandparametersVersion— parameters unchanged, no version bump required; both 6.4 and 6.5 fixtures still convert.
The two blocking issues from the last round are genuinely fixed, and the fix has been verified against legacy rather than against this PR's own tests. The one remaining substantive defect is the vacuous Ignored Path test; the rest are nits and V&V-document accuracy.
Naming Conventions
Naming of variables should descriptive where needed. Loop Control Variables can use
iif warranted. Most of these conventions are enforced through the clang-tidy and clang-format configuration files. See the filesimplnx/docs/Code_Style_Guide.mdfor a more in depth explanation.Filter Checklist
The help file
simplnx/docs/Porting_Filters.mdhas documentation to help you port or write new filters. At the top is a nice checklist of items that should be noted when porting a filter.Unit Testing
The idea of unit testing is to test the filter for proper execution and error handling. How many variations on a unit test each filter needs is entirely dependent on what the filter is doing. Generally, the variations can fall into a few categories:
Code Cleanup