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.
* Ignored Path test now executes the filter and asserts FeatureIds
actually changed. Previously it only preflighted, so comparing the
DataStructure against a fresh fixture could not fail -- the test
passed identically with an empty ignore list.
* Collapse CheckDilateOutput/CheckErodeOutput into a single table
keyed on (operation, directions, iterations), removing ~100 lines of
duplicated if/else and the bare REQUIRE(false) fallthrough.
* Move the all-directions-off guard above CreateTestData and report the
skip with SUCCEED so it is visible in the Catch2 output.
* CheckPathIgnored uses REQUIRE_NOTHROW + getDataRefAs instead of
dereferencing raw getDataAs results.
* Separate the AttributeMatrix/DataArray tuple shape (ZYX {2,4,4}) from
the ImageGeom dimensions (XYZ {4,4,2}); build the fixture DataStores
with the tuple shape so they inherit it from their parent.
* Add UnitTest::CheckArraysInheritTupleDims to every TEST_CASE that
builds a DataStructure.
* Declare the operation as ChoicesParameter::ValueType rather than
relying on implicit conversion from uint64/int32.
* Restore constexpr on faceNeighborInternalIdx (regression vs develop)
and add a trailing taskRunner.wait() after the FeatureIds transfer so
the completion invariant is local rather than implied.
* Rename k_NoGeometryDimensions to k_NoGeometryDimensionsError to match
the sibling k_NoDirectionsError.
* Correct the user-facing docs: erode ties are deterministic
(first neighbor in [-Z,-Y,-X,+X,+Y,+Z] scan order wins), not random.
Document the direction restrictions and the -14601/-14602 preflight
errors, and drop a stray backtick.
* V&V report: cite a commit that exists and the correct branch spelling,
correct the assertion count to the measured 2283, resolve the
exemplar-archive contradiction, and state the oracle plainly as
Class 2 (regenerated from the 6.5.171 binary) instead of claiming the
1792 expected values were hand-traced.
All 7 ErodeDilateBadData tests pass, 2283 assertions.
Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
Audited the report against docs/vv_templates/report_gates.md and the 34 other reports in src/Plugins/*/vv/. Fixed every structural divergence. * Add the missing "## Exemplar archive" section. This report was the only one of 35 without it. Records 6_6_erode_dilate_test.tar.gz, its SHA512 (verified against test/CMakeLists.txt and the on-disk archive), and the provenance sidecar path. * Add the provenance sidecar vv/provenance/6_6_erode_dilate_test.md, reconstructed from the 18-step legacy pipeline embedded in the .dream3d file itself. Records that both exemplars were written by the legacy SIMPL ErodeDilateBadData filter (UUID 3adfe077-..., Erode/Dilate, NumIterations=2, all directions on) on a 189x201x20 Small IN100 slice, so the archive is not a circular oracle. Documents two caveats: the generating build is FilterVersion 6.6.338, later than the 6.5.171 baseline, so the archive alone is not a 6.5.171 comparison; and both exemplars use all directions on, so the archive cannot discriminate direction gating. * Verified commit field now uses the standard "*<filled at SBIR deliverable assembly>*" placeholder, as 32 of the other 34 reports do. This is the correct resolution of the review finding that the previously cited SHA did not exist -- that field is not meant to carry a SHA during review. * Rename the "Test fixtures" at-a-glance row to "Exemplar archive" and "Oracle" to "Oracle (confirmed)", matching the gate list and the prevailing usage (32/33 and 29/35 respectively). * Add the Status column to the test inventory (kept | new-for-V&V), as 28 of 35 reports do, and record what changed per test this cycle. * Trim the Summary to the 2-3 sentences the gate calls for. * Add the Port-time deltas list and a Material PRs since baseline line to Algorithm Relationship, citing the four PRs that actually touched the algorithm (BlueQuartzSoftware#1523, BlueQuartzSoftware#1590, BlueQuartzSoftware#1340, BlueQuartzSoftware#1687). * Restructure Oracle into the standard Class / Applied / Encoded / Second-engineer review shape, and mark second-engineer review as pending rather than implying it was done. * Add the line count to the Code path coverage Source line, enumerate both preflight error paths separately, and add the cancel path as a row marked not-directly-tested rather than describing it in prose below the table. Count is now 10 of 11. * Renumber ErodeDilateBadDataFilter-B1 to -D1. The -B<N> convention appeared in no other deviations doc; CAxisSegmentFeaturesFilter-D1 is the precedent for a SIMPLNX-side bug found during a V&V cycle. An ID note preserves the old alias; no external references existed. * Give the deviation entry the standard field table (ID, Filter UUID, Status) plus the Affected users and Recommendation fields the gate requires, and promote the confirmed non-deviations from the report into the deviations doc where they belong. Also ran the test suite in the out-of-core build (simplnx-ooc-Rel) to satisfy the "both in-core and OOC" test gate. All 7 tests pass in both configurations with identical assertion counts (2283). Signed-off-by: Michael Jackson <mike.jackson@bluequartz.net>
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