From 03d132d7ccc0b2ae1bfee9ed6ef6c7b94ed72f79 Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Wed, 22 Apr 2026 07:50:06 -0400 Subject: [PATCH 01/14] WIP: ErodeDilateBadData * 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. --- .../Filters/Algorithms/ErodeDilateBadData.cpp | 165 ++++++++++++++++-- 1 file changed, 148 insertions(+), 17 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp index 0cc0ed0cd0..ac8711fa12 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp @@ -6,6 +6,7 @@ #include "simplnx/Utilities/MessageHelper.hpp" #include "simplnx/Utilities/NeighborUtilities.hpp" #include "simplnx/Utilities/ParallelTaskAlgorithm.hpp" +#include "simplnx/Utilities/DataStoreUtilities.hpp" using namespace nx::core; namespace @@ -17,7 +18,7 @@ class ErodeDilateBadDataTransferDataImpl ErodeDilateBadDataTransferDataImpl(const ErodeDilateBadDataTransferDataImpl&) = default; ErodeDilateBadDataTransferDataImpl(ErodeDilateBadData* filterAlg, usize totalPoints, ChoicesParameter::ValueType operation, const Int32AbstractDataStore& featureIds, - const std::vector& neighbors, const std::shared_ptr& dataArrayPtr, MessageHelper& messageHelper) + const Int64AbstractDataStore& neighbors, const std::shared_ptr& dataArrayPtr, MessageHelper& messageHelper) : m_FilterAlg(filterAlg) , m_TotalPoints(totalPoints) , m_Operation(operation) @@ -57,7 +58,7 @@ class ErodeDilateBadDataTransferDataImpl ErodeDilateBadData* m_FilterAlg = nullptr; usize m_TotalPoints = 0; ChoicesParameter::ValueType m_Operation = 0; - std::vector m_Neighbors; + const Int64AbstractDataStore& m_Neighbors; const std::shared_ptr m_DataArrayPtr; const Int32AbstractDataStore& m_FeatureIds; MessageHelper& m_MessageHelper; @@ -82,13 +83,146 @@ const std::atomic_bool& ErodeDilateBadData::getCancel() return m_ShouldCancel; } +// ----------------------------------------------------------------------------- +bool shouldSkipData(const ErodeDilateBadDataInputValues* inputValues, int32 neighPointIdx, const std::array& dims, int64 xIndex, int64 yIndex, int64 zIndex) +{ + if(neighPointIdx == 0 && (zIndex == 0 || !inputValues->ZDirOn)) + { + return true; + } + if(neighPointIdx == 5 && (zIndex == (dims[2] - 1) || !inputValues->ZDirOn)) + { + return true; + } + if(neighPointIdx == 1 && (yIndex == 0 || !inputValues->YDirOn)) + { + return true; + } + if(neighPointIdx == 4 && (yIndex == (dims[1] - 1) || !inputValues->YDirOn)) + { + return true; + } + if(neighPointIdx == 2 && (xIndex == 0 || !inputValues->XDirOn)) + { + return true; + } + if(neighPointIdx == 3 && (xIndex == (dims[0] - 1) || !inputValues->XDirOn)) + { + return true; + } + + return false; +} + +/** + * @brief Parses over the neighbor indices and sets the feature counts to 0 for existing points. + * @param featureIds Feature ID data store for determining neighboring feature IDs. + * @param featureCount Running total of the number of features it neighbors. + * @param neighpoints Pre-created array for determining neighbor indices. + * @param dims Geometry dimentions for determining boundaries. + * @param voxelIndex Array index of the 3D geometry position. + * @param xIndex X index in the geometry position used for determing neighbor validity. + * @param yIndex Y index in the geometry position used for determing neighbor validity. + * @param zIndex Z index in the geometry position used for determing neighbor validity. + */ +void ErodeBadDataPostOp(const Int32AbstractDataStore& featureIds, std::vector& featureCount, const std::array& neighpoints, const std::array& dims, const int64 voxelIndex, + int64 xIndex, int64 yIndex, int64 zIndex) +{ + for(int32 neighPointIdx = 0; neighPointIdx < 6; neighPointIdx++) + { + const int64 neighborPoint = voxelIndex + neighpoints[neighPointIdx]; + if(neighPointIdx == 0 && zIndex == 0) + { + continue; + } + if(neighPointIdx == 5 && zIndex == (dims[2] - 1)) + { + continue; + } + if(neighPointIdx == 1 && yIndex == 0) + { + continue; + } + if(neighPointIdx == 4 && yIndex == (dims[1] - 1)) + { + continue; + } + if(neighPointIdx == 2 && xIndex == 0) + { + continue; + } + if(neighPointIdx == 3 && xIndex == (dims[0] - 1)) + { + continue; + } + + const int32 feature = featureIds[neighborPoint]; + featureCount[feature] = 0; + } +} + +/** + * @brief + * @param inputValues Algorithm input values + * @param featureIds Feature ID data store. + * @param featureCount Running total of the number of features it neighbors. + * @param neighbors + * @param neighpoints Pre-created array for determining neighbor indices. + * @param dims Geometry dimentions for determining boundaries. + * @param voxelIndex Array index of the 3D geometry position. + * @param xIndex X index in the geometry position used for determing neighbor validity. + * @param yIndex Y index in the geometry position used for determing neighbor validity. + * @param zIndes Z index in the geometry position used for determing neighbor validity. + */ +void erodeDilateBadDataVoxel(const ErodeDilateBadDataInputValues* inputValues, const Int32AbstractDataStore& featureIds, std::vector& featureCount, Int64AbstractDataStore& neighbors, + const std::array& neighpoints, const std::array& dims, int64 voxelIndex, int64 xIndex, int64 yIndex, int64 zIndex) +{ + const int32 featureName = featureIds[voxelIndex]; + if(featureName == 0) + { + int32 most = 0; + for(int32 neighPointIdx = 0; neighPointIdx < 6; neighPointIdx++) + { + const int64 neighborPoint = voxelIndex + neighpoints[neighPointIdx]; + if(shouldSkipData(inputValues, neighPointIdx, dims, xIndex, yIndex, zIndex)) + { + continue; + } + + const int32 feature = featureIds[neighborPoint]; + if(inputValues->Operation == detail::k_DilateIndex && feature > 0) + { + neighbors[neighborPoint] = voxelIndex; + } + if(feature > 0 && inputValues->Operation == detail::k_ErodeIndex) + { + featureCount[feature]++; + const int32 current = featureCount[feature]; + if(current > most) + { + most = current; + neighbors[voxelIndex] = neighborPoint; + } + } + } + // Erode operation + if(inputValues->Operation == detail::k_ErodeIndex) + { + ErodeBadDataPostOp(featureIds, featureCount, neighpoints, dims, voxelIndex, xIndex, yIndex, zIndex); + } + } +} + // ----------------------------------------------------------------------------- Result<> ErodeDilateBadData::operator()() { const auto& featureIds = m_DataStructure.getDataAs(m_InputValues->FeatureIdsArrayPath)->getDataStoreRef(); const usize totalPoints = featureIds.getNumberOfTuples(); - std::vector neighbors(totalPoints, -1); + // Update for OOC data sizes + std::shared_ptr neighborsPtr = DataStoreUtilities::CreateDataStore({totalPoints}, {1}); + auto& neighbors = *neighborsPtr.get(); + neighbors.fill(-1); const auto& selectedImageGeom = m_DataStructure.getDataRefAs(m_InputValues->InputImageGeometry); @@ -100,15 +234,7 @@ Result<> ErodeDilateBadData::operator()() static_cast(udims[2]), }; - usize numFeatures = 0; - for(usize i = 0; i < totalPoints; i++) - { - const int32 featureName = featureIds[i]; - if(featureName > numFeatures) - { - numFeatures = featureName; - } - } + usize numFeatures = std::max(0, *(std::max_element(featureIds.begin(), featureIds.end()))); constexpr FaceNeighborType k_NumFaceNeighbors = VoxelNeighbors::k_FaceNeighborCount; const std::array neighborVoxelIndexOffsets = initializeFaceNeighborOffsets(dims); @@ -116,16 +242,20 @@ Result<> ErodeDilateBadData::operator()() std::vector featureCount(numFeatures + 1, 0); + // Iterate over the geometry to handle every voxel for(int32 iteration = 0; iteration < m_InputValues->NumIterations; iteration++) { - for(int64 zIdx = 0; zIdx < dims[2]; zIdx++) + for(int64 zIndex = 0; zIndex < dims[2]; zIndex++) { - const int64 zStride = dims[0] * dims[1] * zIdx; - for(int64 yIdx = 0; yIdx < dims[1]; yIdx++) + const int64 zStride = dims[0] * dims[1] * zIndex; + for(int64 yIndex = 0; yIndex < dims[1]; yIndex++) { - const int64 yStride = dims[0] * yIdx; - for(int64 xIdx = 0; xIdx < dims[0]; xIdx++) + const int64 yStride = dims[0] * yIndex; + for(int64 xIndex = 0; xIndex < dims[0]; xIndex++) { + const int64 voxelIndex = zStride + yStride + xIndex; + erodeDilateBadDataVoxel(m_InputValues, featureIds, featureCount, neighbors, neighborVoxelIndexOffsets, dims, voxelIndex, xIndex, yIndex, zIndex); + #if 0 const int64 voxelIndex = zStride + yStride + xIdx; const int32 featureName = featureIds[voxelIndex]; if(featureName == 0) @@ -173,6 +303,7 @@ Result<> ErodeDilateBadData::operator()() } } } + #endif } } } From f4a4715c571678941480f7399f7d34fa62badce2 Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Tue, 9 Jun 2026 12:42:34 -0400 Subject: [PATCH 02/14] Convert neighbors data back to std::vector --- .../Filters/Algorithms/ErodeDilateBadData.cpp | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp index ac8711fa12..4ffe0472e6 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp @@ -18,7 +18,7 @@ class ErodeDilateBadDataTransferDataImpl ErodeDilateBadDataTransferDataImpl(const ErodeDilateBadDataTransferDataImpl&) = default; ErodeDilateBadDataTransferDataImpl(ErodeDilateBadData* filterAlg, usize totalPoints, ChoicesParameter::ValueType operation, const Int32AbstractDataStore& featureIds, - const Int64AbstractDataStore& neighbors, const std::shared_ptr& dataArrayPtr, MessageHelper& messageHelper) + const std::vector& neighbors, const std::shared_ptr& dataArrayPtr, MessageHelper& messageHelper) : m_FilterAlg(filterAlg) , m_TotalPoints(totalPoints) , m_Operation(operation) @@ -58,7 +58,7 @@ class ErodeDilateBadDataTransferDataImpl ErodeDilateBadData* m_FilterAlg = nullptr; usize m_TotalPoints = 0; ChoicesParameter::ValueType m_Operation = 0; - const Int64AbstractDataStore& m_Neighbors; + const std::vector& m_Neighbors; const std::shared_ptr m_DataArrayPtr; const Int32AbstractDataStore& m_FeatureIds; MessageHelper& m_MessageHelper; @@ -174,7 +174,7 @@ void ErodeBadDataPostOp(const Int32AbstractDataStore& featureIds, std::vector& featureCount, Int64AbstractDataStore& neighbors, +void erodeDilateBadDataVoxel(const ErodeDilateBadDataInputValues* inputValues, const Int32AbstractDataStore& featureIds, std::vector& featureCount, std::vector& neighbors, const std::array& neighpoints, const std::array& dims, int64 voxelIndex, int64 xIndex, int64 yIndex, int64 zIndex) { const int32 featureName = featureIds[voxelIndex]; @@ -220,9 +220,7 @@ Result<> ErodeDilateBadData::operator()() const usize totalPoints = featureIds.getNumberOfTuples(); // Update for OOC data sizes - std::shared_ptr neighborsPtr = DataStoreUtilities::CreateDataStore({totalPoints}, {1}); - auto& neighbors = *neighborsPtr.get(); - neighbors.fill(-1); + std::vector neighbors(totalPoints, -1); const auto& selectedImageGeom = m_DataStructure.getDataRefAs(m_InputValues->InputImageGeometry); From e6d8fff7a08000d23dfc77764f4297311c274898 Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Fri, 12 Jun 2026 11:11:30 -0400 Subject: [PATCH 03/14] Resolve rebase conflicts --- .../Filters/Algorithms/ErodeDilateBadData.cpp | 104 ++++-------------- 1 file changed, 21 insertions(+), 83 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp index 4ffe0472e6..172b6666eb 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp @@ -11,6 +11,8 @@ using namespace nx::core; namespace { +constexpr FaceNeighborType k_NumFaceNeighbors = VoxelNeighbors::k_FaceNeighborCount; + class ErodeDilateBadDataTransferDataImpl { public: @@ -125,37 +127,19 @@ bool shouldSkipData(const ErodeDilateBadDataInputValues* inputValues, int32 neig * @param yIndex Y index in the geometry position used for determing neighbor validity. * @param zIndex Z index in the geometry position used for determing neighbor validity. */ -void ErodeBadDataPostOp(const Int32AbstractDataStore& featureIds, std::vector& featureCount, const std::array& neighpoints, const std::array& dims, const int64 voxelIndex, +inline void ErodeBadDataPostOp(const Int32AbstractDataStore& featureIds, std::vector& featureCount, const std::array& neighpoints, + const std::array& faceNeighborInternalIndex, const std::array& dims, const int64 voxelIndex, int64 xIndex, int64 yIndex, int64 zIndex) { - for(int32 neighPointIdx = 0; neighPointIdx < 6; neighPointIdx++) + // Loop over the 6 face neighbors of the voxel + const std::array isValidFaceNeighbor = computeValidFaceNeighbors(xIndex, yIndex, zIndex, dims); + for (const auto& faceIndex : faceNeighborInternalIndex) { - const int64 neighborPoint = voxelIndex + neighpoints[neighPointIdx]; - if(neighPointIdx == 0 && zIndex == 0) - { - continue; - } - if(neighPointIdx == 5 && zIndex == (dims[2] - 1)) + if(!isValidFaceNeighbor[faceIndex]) { continue; } - if(neighPointIdx == 1 && yIndex == 0) - { - continue; - } - if(neighPointIdx == 4 && yIndex == (dims[1] - 1)) - { - continue; - } - if(neighPointIdx == 2 && xIndex == 0) - { - continue; - } - if(neighPointIdx == 3 && xIndex == (dims[0] - 1)) - { - continue; - } - + const int64 neighborPoint = voxelIndex + neighpoints[faceIndex]; const int32 feature = featureIds[neighborPoint]; featureCount[feature] = 0; } @@ -174,21 +158,24 @@ void ErodeBadDataPostOp(const Int32AbstractDataStore& featureIds, std::vector& featureCount, std::vector& neighbors, - const std::array& neighpoints, const std::array& dims, int64 voxelIndex, int64 xIndex, int64 yIndex, int64 zIndex) +inline void erodeDilateBadDataVoxel(const ErodeDilateBadDataInputValues* inputValues, const Int32AbstractDataStore& featureIds, std::vector& featureCount, std::vector& neighbors, + const std::array& neighpoints, const std::array& faceNeighborInternalIndex, + const std::array& dims, int64 voxelIndex, int64 xIndex, + int64 yIndex, int64 zIndex) { const int32 featureName = featureIds[voxelIndex]; if(featureName == 0) { int32 most = 0; - for(int32 neighPointIdx = 0; neighPointIdx < 6; neighPointIdx++) + // Loop over the 6 face neighbors of the voxel + const std::array isValidFaceNeighbor = computeValidFaceNeighbors(xIndex, yIndex, zIndex, dims); + for (const auto& faceIndex : faceNeighborInternalIndex) { - const int64 neighborPoint = voxelIndex + neighpoints[neighPointIdx]; - if(shouldSkipData(inputValues, neighPointIdx, dims, xIndex, yIndex, zIndex)) + if(!isValidFaceNeighbor[faceIndex]) { continue; } - + const int64 neighborPoint = voxelIndex + neighpoints[faceIndex]; const int32 feature = featureIds[neighborPoint]; if(inputValues->Operation == detail::k_DilateIndex && feature > 0) { @@ -205,10 +192,11 @@ void erodeDilateBadDataVoxel(const ErodeDilateBadDataInputValues* inputValues, c } } } + // Erode operation if(inputValues->Operation == detail::k_ErodeIndex) { - ErodeBadDataPostOp(featureIds, featureCount, neighpoints, dims, voxelIndex, xIndex, yIndex, zIndex); + ErodeBadDataPostOp(featureIds, featureCount, neighpoints, faceNeighborInternalIndex, dims, voxelIndex, xIndex, yIndex, zIndex); } } } @@ -234,7 +222,6 @@ Result<> ErodeDilateBadData::operator()() usize numFeatures = std::max(0, *(std::max_element(featureIds.begin(), featureIds.end()))); - constexpr FaceNeighborType k_NumFaceNeighbors = VoxelNeighbors::k_FaceNeighborCount; const std::array neighborVoxelIndexOffsets = initializeFaceNeighborOffsets(dims); constexpr std::array faceNeighborInternalIdx = initializeFaceNeighborInternalIdx(); @@ -252,56 +239,7 @@ Result<> ErodeDilateBadData::operator()() for(int64 xIndex = 0; xIndex < dims[0]; xIndex++) { const int64 voxelIndex = zStride + yStride + xIndex; - erodeDilateBadDataVoxel(m_InputValues, featureIds, featureCount, neighbors, neighborVoxelIndexOffsets, dims, voxelIndex, xIndex, yIndex, zIndex); - #if 0 - const int64 voxelIndex = zStride + yStride + xIdx; - const int32 featureName = featureIds[voxelIndex]; - if(featureName == 0) - { - int32 most = 0; - // Loop over the 6 face neighbors of the voxel - const std::array isValidFaceNeighbor = computeValidFaceNeighbors(xIdx, yIdx, zIdx, dims); - for(const auto& faceIndex : faceNeighborInternalIdx) - { - if(!isValidFaceNeighbor[faceIndex]) - { - continue; - } - const int64 neighborPoint = voxelIndex + neighborVoxelIndexOffsets[faceIndex]; - - const int32 feature = featureIds[neighborPoint]; - if(m_InputValues->Operation == detail::k_DilateIndex && feature > 0) - { - neighbors[neighborPoint] = voxelIndex; - } - if(feature > 0 && m_InputValues->Operation == detail::k_ErodeIndex) - { - featureCount[feature]++; - const int32 current = featureCount[feature]; - if(current > most) - { - most = current; - neighbors[voxelIndex] = neighborPoint; - } - } - } - if(m_InputValues->Operation == detail::k_ErodeIndex) - { - // Loop over the 6 face neighbors of the voxel - for(const auto& faceIndex : faceNeighborInternalIdx) - { - if(!isValidFaceNeighbor[faceIndex]) - { - continue; - } - const int64 neighborPoint = voxelIndex + neighborVoxelIndexOffsets[faceIndex]; - - const int32 feature = featureIds[neighborPoint]; - featureCount[feature] = 0; - } - } - } - #endif + erodeDilateBadDataVoxel(m_InputValues, featureIds, featureCount, neighbors, neighborVoxelIndexOffsets, faceNeighborInternalIdx, dims, voxelIndex, xIndex, yIndex, zIndex); } } } From 3e2efac0b4f7afc98c11b213da109941d3725289 Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Fri, 12 Jun 2026 11:29:25 -0400 Subject: [PATCH 04/14] Update function documentation * Clang-format --- .../Filters/Algorithms/ErodeDilateBadData.cpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp index 172b6666eb..ef5dae71ba 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp @@ -3,10 +3,10 @@ #include "simplnx/DataStructure/DataArray.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Utilities/DataGroupUtilities.hpp" +#include "simplnx/Utilities/DataStoreUtilities.hpp" #include "simplnx/Utilities/MessageHelper.hpp" #include "simplnx/Utilities/NeighborUtilities.hpp" #include "simplnx/Utilities/ParallelTaskAlgorithm.hpp" -#include "simplnx/Utilities/DataStoreUtilities.hpp" using namespace nx::core; namespace @@ -128,12 +128,12 @@ bool shouldSkipData(const ErodeDilateBadDataInputValues* inputValues, int32 neig * @param zIndex Z index in the geometry position used for determing neighbor validity. */ inline void ErodeBadDataPostOp(const Int32AbstractDataStore& featureIds, std::vector& featureCount, const std::array& neighpoints, - const std::array& faceNeighborInternalIndex, const std::array& dims, const int64 voxelIndex, - int64 xIndex, int64 yIndex, int64 zIndex) + const std::array& faceNeighborInternalIndex, const std::array& dims, const int64 voxelIndex, int64 xIndex, int64 yIndex, + int64 zIndex) { // Loop over the 6 face neighbors of the voxel const std::array isValidFaceNeighbor = computeValidFaceNeighbors(xIndex, yIndex, zIndex, dims); - for (const auto& faceIndex : faceNeighborInternalIndex) + for(const auto& faceIndex : faceNeighborInternalIndex) { if(!isValidFaceNeighbor[faceIndex]) { @@ -146,7 +146,8 @@ inline void ErodeBadDataPostOp(const Int32AbstractDataStore& featureIds, std::ve } /** - * @brief + * @brief Processes a single voxel for erode/dilate bad data operations. For bad data voxels (featureId == 0), + * identifies the best neighboring good voxel and records it in the neighbors array for later data transfer. * @param inputValues Algorithm input values * @param featureIds Feature ID data store. * @param featureCount Running total of the number of features it neighbors. @@ -156,12 +157,11 @@ inline void ErodeBadDataPostOp(const Int32AbstractDataStore& featureIds, std::ve * @param voxelIndex Array index of the 3D geometry position. * @param xIndex X index in the geometry position used for determing neighbor validity. * @param yIndex Y index in the geometry position used for determing neighbor validity. - * @param zIndes Z index in the geometry position used for determing neighbor validity. + * @param zIndex Z index in the geometry position used for determing neighbor validity. */ inline void erodeDilateBadDataVoxel(const ErodeDilateBadDataInputValues* inputValues, const Int32AbstractDataStore& featureIds, std::vector& featureCount, std::vector& neighbors, const std::array& neighpoints, const std::array& faceNeighborInternalIndex, - const std::array& dims, int64 voxelIndex, int64 xIndex, - int64 yIndex, int64 zIndex) + const std::array& dims, int64 voxelIndex, int64 xIndex, int64 yIndex, int64 zIndex) { const int32 featureName = featureIds[voxelIndex]; if(featureName == 0) @@ -169,7 +169,7 @@ inline void erodeDilateBadDataVoxel(const ErodeDilateBadDataInputValues* inputVa int32 most = 0; // Loop over the 6 face neighbors of the voxel const std::array isValidFaceNeighbor = computeValidFaceNeighbors(xIndex, yIndex, zIndex, dims); - for (const auto& faceIndex : faceNeighborInternalIndex) + for(const auto& faceIndex : faceNeighborInternalIndex) { if(!isValidFaceNeighbor[faceIndex]) { From 48f9920e9be9c1c6f2f1980313b53505eaa467da Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Thu, 23 Jul 2026 16:06:31 -0400 Subject: [PATCH 05/14] Fixed XYZ direction off bug * 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 --- .../Filters/Algorithms/ErodeDilateBadData.cpp | 216 ++--- .../Filters/ErodeDilateBadDataFilter.cpp | 23 + .../test/ErodeDilateBadDataTest.cpp | 822 ++++++++++++++++-- .../vv/ErodeDilateBadDataFilter.md | 90 ++ .../vv/deviations/ErodeDilateBadDataFilter.md | 23 + 5 files changed, 980 insertions(+), 194 deletions(-) create mode 100644 src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md create mode 100644 src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp index ef5dae71ba..9fe54488bc 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp @@ -3,7 +3,6 @@ #include "simplnx/DataStructure/DataArray.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Utilities/DataGroupUtilities.hpp" -#include "simplnx/Utilities/DataStoreUtilities.hpp" #include "simplnx/Utilities/MessageHelper.hpp" #include "simplnx/Utilities/NeighborUtilities.hpp" #include "simplnx/Utilities/ParallelTaskAlgorithm.hpp" @@ -11,8 +10,6 @@ using namespace nx::core; namespace { -constexpr FaceNeighborType k_NumFaceNeighbors = VoxelNeighbors::k_FaceNeighborCount; - class ErodeDilateBadDataTransferDataImpl { public: @@ -60,11 +57,28 @@ class ErodeDilateBadDataTransferDataImpl ErodeDilateBadData* m_FilterAlg = nullptr; usize m_TotalPoints = 0; ChoicesParameter::ValueType m_Operation = 0; - const std::vector& m_Neighbors; + std::vector m_Neighbors; const std::shared_ptr m_DataArrayPtr; const Int32AbstractDataStore& m_FeatureIds; MessageHelper& m_MessageHelper; }; + +/** + * @brief Adjust the standard neighbors array for x, y, and z directions enabled / disabled. + * @param standardNeighbors + * @param xDir + * @param yDir + * @param zDir + */ +void adjustValidNeighbors(std::array& standardNeighbors, bool xDir, bool yDir, bool zDir) +{ + standardNeighbors[0] &= zDir; + standardNeighbors[1] &= yDir; + standardNeighbors[2] &= xDir; + standardNeighbors[3] &= zDir; + standardNeighbors[4] &= yDir; + standardNeighbors[5] &= xDir; +} } // namespace // ----------------------------------------------------------------------------- @@ -85,129 +99,12 @@ const std::atomic_bool& ErodeDilateBadData::getCancel() return m_ShouldCancel; } -// ----------------------------------------------------------------------------- -bool shouldSkipData(const ErodeDilateBadDataInputValues* inputValues, int32 neighPointIdx, const std::array& dims, int64 xIndex, int64 yIndex, int64 zIndex) -{ - if(neighPointIdx == 0 && (zIndex == 0 || !inputValues->ZDirOn)) - { - return true; - } - if(neighPointIdx == 5 && (zIndex == (dims[2] - 1) || !inputValues->ZDirOn)) - { - return true; - } - if(neighPointIdx == 1 && (yIndex == 0 || !inputValues->YDirOn)) - { - return true; - } - if(neighPointIdx == 4 && (yIndex == (dims[1] - 1) || !inputValues->YDirOn)) - { - return true; - } - if(neighPointIdx == 2 && (xIndex == 0 || !inputValues->XDirOn)) - { - return true; - } - if(neighPointIdx == 3 && (xIndex == (dims[0] - 1) || !inputValues->XDirOn)) - { - return true; - } - - return false; -} - -/** - * @brief Parses over the neighbor indices and sets the feature counts to 0 for existing points. - * @param featureIds Feature ID data store for determining neighboring feature IDs. - * @param featureCount Running total of the number of features it neighbors. - * @param neighpoints Pre-created array for determining neighbor indices. - * @param dims Geometry dimentions for determining boundaries. - * @param voxelIndex Array index of the 3D geometry position. - * @param xIndex X index in the geometry position used for determing neighbor validity. - * @param yIndex Y index in the geometry position used for determing neighbor validity. - * @param zIndex Z index in the geometry position used for determing neighbor validity. - */ -inline void ErodeBadDataPostOp(const Int32AbstractDataStore& featureIds, std::vector& featureCount, const std::array& neighpoints, - const std::array& faceNeighborInternalIndex, const std::array& dims, const int64 voxelIndex, int64 xIndex, int64 yIndex, - int64 zIndex) -{ - // Loop over the 6 face neighbors of the voxel - const std::array isValidFaceNeighbor = computeValidFaceNeighbors(xIndex, yIndex, zIndex, dims); - for(const auto& faceIndex : faceNeighborInternalIndex) - { - if(!isValidFaceNeighbor[faceIndex]) - { - continue; - } - const int64 neighborPoint = voxelIndex + neighpoints[faceIndex]; - const int32 feature = featureIds[neighborPoint]; - featureCount[feature] = 0; - } -} - -/** - * @brief Processes a single voxel for erode/dilate bad data operations. For bad data voxels (featureId == 0), - * identifies the best neighboring good voxel and records it in the neighbors array for later data transfer. - * @param inputValues Algorithm input values - * @param featureIds Feature ID data store. - * @param featureCount Running total of the number of features it neighbors. - * @param neighbors - * @param neighpoints Pre-created array for determining neighbor indices. - * @param dims Geometry dimentions for determining boundaries. - * @param voxelIndex Array index of the 3D geometry position. - * @param xIndex X index in the geometry position used for determing neighbor validity. - * @param yIndex Y index in the geometry position used for determing neighbor validity. - * @param zIndex Z index in the geometry position used for determing neighbor validity. - */ -inline void erodeDilateBadDataVoxel(const ErodeDilateBadDataInputValues* inputValues, const Int32AbstractDataStore& featureIds, std::vector& featureCount, std::vector& neighbors, - const std::array& neighpoints, const std::array& faceNeighborInternalIndex, - const std::array& dims, int64 voxelIndex, int64 xIndex, int64 yIndex, int64 zIndex) -{ - const int32 featureName = featureIds[voxelIndex]; - if(featureName == 0) - { - int32 most = 0; - // Loop over the 6 face neighbors of the voxel - const std::array isValidFaceNeighbor = computeValidFaceNeighbors(xIndex, yIndex, zIndex, dims); - for(const auto& faceIndex : faceNeighborInternalIndex) - { - if(!isValidFaceNeighbor[faceIndex]) - { - continue; - } - const int64 neighborPoint = voxelIndex + neighpoints[faceIndex]; - const int32 feature = featureIds[neighborPoint]; - if(inputValues->Operation == detail::k_DilateIndex && feature > 0) - { - neighbors[neighborPoint] = voxelIndex; - } - if(feature > 0 && inputValues->Operation == detail::k_ErodeIndex) - { - featureCount[feature]++; - const int32 current = featureCount[feature]; - if(current > most) - { - most = current; - neighbors[voxelIndex] = neighborPoint; - } - } - } - - // Erode operation - if(inputValues->Operation == detail::k_ErodeIndex) - { - ErodeBadDataPostOp(featureIds, featureCount, neighpoints, faceNeighborInternalIndex, dims, voxelIndex, xIndex, yIndex, zIndex); - } - } -} - // ----------------------------------------------------------------------------- Result<> ErodeDilateBadData::operator()() { const auto& featureIds = m_DataStructure.getDataAs(m_InputValues->FeatureIdsArrayPath)->getDataStoreRef(); const usize totalPoints = featureIds.getNumberOfTuples(); - // Update for OOC data sizes std::vector neighbors(totalPoints, -1); const auto& selectedImageGeom = m_DataStructure.getDataRefAs(m_InputValues->InputImageGeometry); @@ -220,26 +117,81 @@ Result<> ErodeDilateBadData::operator()() static_cast(udims[2]), }; - usize numFeatures = std::max(0, *(std::max_element(featureIds.begin(), featureIds.end()))); + usize numFeatures = 0; + for(usize i = 0; i < totalPoints; i++) + { + const int32 featureName = featureIds[i]; + if(featureName > numFeatures) + { + numFeatures = featureName; + } + } + constexpr FaceNeighborType k_NumFaceNeighbors = VoxelNeighbors::k_FaceNeighborCount; const std::array neighborVoxelIndexOffsets = initializeFaceNeighborOffsets(dims); - constexpr std::array faceNeighborInternalIdx = initializeFaceNeighborInternalIdx(); + std::array faceNeighborInternalIdx = initializeFaceNeighborInternalIdx(); + // initializeFaceNeighborInternalIdx() does not take into acccount x/y/z being completely disabled. + adjustValidNeighbors(faceNeighborInternalIdx, m_InputValues->XDirOn, m_InputValues->YDirOn, m_InputValues->ZDirOn); std::vector featureCount(numFeatures + 1, 0); - // Iterate over the geometry to handle every voxel for(int32 iteration = 0; iteration < m_InputValues->NumIterations; iteration++) { - for(int64 zIndex = 0; zIndex < dims[2]; zIndex++) + for(int64 zIdx = 0; zIdx < dims[2]; zIdx++) { - const int64 zStride = dims[0] * dims[1] * zIndex; - for(int64 yIndex = 0; yIndex < dims[1]; yIndex++) + const int64 zStride = dims[0] * dims[1] * zIdx; + for(int64 yIdx = 0; yIdx < dims[1]; yIdx++) { - const int64 yStride = dims[0] * yIndex; - for(int64 xIndex = 0; xIndex < dims[0]; xIndex++) + const int64 yStride = dims[0] * yIdx; + for(int64 xIdx = 0; xIdx < dims[0]; xIdx++) { - const int64 voxelIndex = zStride + yStride + xIndex; - erodeDilateBadDataVoxel(m_InputValues, featureIds, featureCount, neighbors, neighborVoxelIndexOffsets, faceNeighborInternalIdx, dims, voxelIndex, xIndex, yIndex, zIndex); + const int64 voxelIndex = zStride + yStride + xIdx; + const int32 featureName = featureIds[voxelIndex]; + if(featureName == 0) + { + int32 most = 0; + // Loop over the 6 face neighbors of the voxel + const std::array isValidFaceNeighbor = computeValidFaceNeighbors(xIdx, yIdx, zIdx, dims); + for(const auto& faceIndex : faceNeighborInternalIdx) + { + if(!isValidFaceNeighbor[faceIndex]) + { + continue; + } + const int64 neighborPoint = voxelIndex + neighborVoxelIndexOffsets[faceIndex]; + + const int32 feature = featureIds[neighborPoint]; + if(m_InputValues->Operation == detail::k_DilateIndex && feature > 0) + { + neighbors[neighborPoint] = voxelIndex; + } + if(feature > 0 && m_InputValues->Operation == detail::k_ErodeIndex) + { + featureCount[feature]++; + const int32 current = featureCount[feature]; + if(current > most) + { + most = current; + neighbors[voxelIndex] = neighborPoint; + } + } + } + if(m_InputValues->Operation == detail::k_ErodeIndex) + { + // Loop over the 6 face neighbors of the voxel + for(const auto& faceIndex : faceNeighborInternalIdx) + { + if(!isValidFaceNeighbor[faceIndex]) + { + continue; + } + const int64 neighborPoint = voxelIndex + neighborVoxelIndexOffsets[faceIndex]; + + const int32 feature = featureIds[neighborPoint]; + featureCount[feature] = 0; + } + } + } } } } @@ -271,4 +223,4 @@ Result<> ErodeDilateBadData::operator()() } return {}; -} +} \ No newline at end of file diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp index 30ab525c7f..a1ed182bfb 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp @@ -3,6 +3,7 @@ #include "SimplnxCore/Filters/Algorithms/ErodeDilateBadData.hpp" #include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Parameters/ArraySelectionParameter.hpp" #include "simplnx/Parameters/AttributeMatrixSelectionParameter.hpp" #include "simplnx/Parameters/BoolParameter.hpp" @@ -18,6 +19,12 @@ using namespace nx::core; +namespace +{ +int32 k_NoDirections_Error = -14601; +int32 k_NoGeometryDimensions = -14602; +} + namespace nx::core { @@ -97,6 +104,10 @@ IFilter::PreflightResult ErodeDilateBadDataFilter::preflightImpl(const DataStruc auto pOperationValue = filterArgs.value(k_Operation_Key); auto pFeatureIdsArrayPathValue = filterArgs.value(k_CellFeatureIdsArrayPath_Key); auto pIgnoredDataArrayPathsValue = filterArgs.value(k_IgnoredDataArrayPaths_Key); + auto xDirOn = filterArgs.value(k_XDirOn_Key); + auto yDirOn = filterArgs.value(k_YDirOn_Key); + auto zDirOn = filterArgs.value(k_ZDirOn_Key); + auto imageGeometryPath = filterArgs.value(k_SelectedImageGeometryPath_Key); PreflightResult preflightResult; @@ -104,6 +115,18 @@ IFilter::PreflightResult ErodeDilateBadDataFilter::preflightImpl(const DataStruc std::vector preflightUpdatedValues; + if (!xDirOn && !yDirOn && !zDirOn) + { + return {MakeErrorResult(k_NoDirections_Error, "ErodeDilateBadData requires at least one direction to operate over")}; + } + + auto& imageGeom = dataStructure.getDataRefAs(imageGeometryPath); + auto dims = imageGeom.getDimensions(); + if(dims[0] == 0 && dims[1] == 0 && dims[2] == 0) + { + return {MakeErrorResult(k_NoGeometryDimensions, "ErodeDilateBadData requires that the ImageGeom have its dimensions set")}; + } + std::string featureModificationWarning = "By modifying the cell level data, any feature data that was previously computed will most likely be invalid at this point. Filters that compute feature " "level data should be rerun to ensure accurate final results from your pipeline."; preflightUpdatedValues.emplace_back(PreflightValue{"Feature Data Modification Warning", featureModificationWarning}); diff --git a/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp b/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp index 5fc653917c..5c67131bfd 100644 --- a/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp +++ b/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp @@ -1,6 +1,7 @@ #include "SimplnxCore/SimplnxCore_test_dirs.hpp" #include +#include "SimplnxCore/Filters/Algorithms/ErodeDilateBadData.hpp" #include "SimplnxCore/Filters/ErodeDilateBadDataFilter.hpp" #include "simplnx/Core/Application.hpp" @@ -30,99 +31,796 @@ const std::string k_EbsdScanDataName("EBSD Scan Data"); const DataPath k_InputData({"Input Data"}); const DataPath k_EbsdScanDataDataPath = k_InputData.createChildPath(k_EbsdScanDataName); const DataPath k_FeatureIdsDataPath = k_EbsdScanDataDataPath.createChildPath("FeatureIds"); +const StringLiteral k_MiscData = "Misc"; +const ShapeType k_TupleShape{4, 4, 2}; +const usize k_NumTuples = 32; +const DataPath k_DataPath({::k_ImageGeometry, ::k_CellData, k_MiscData}); +const DataPath k_ImageFeatureIdsPath({::k_ImageGeometry, ::k_CellData, k_FeatureIds}); } // namespace -TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Erode)", "[SimplnxCore][ErodeDilateBadDataFilter]") +DataStructure CreateTestData() { - UnitTest::LoadPlugins(); + DataStructure dataStructure; + auto* geom = ImageGeom::Create(dataStructure, ::k_ImageGeometry); + geom->setDimensions(SizeVec3{k_TupleShape[0], k_TupleShape[1], k_TupleShape[2]}); + + auto* cellData = AttributeMatrix::Create(dataStructure, ::k_CellData, k_TupleShape, geom->getId()); + + // Feature IDs + auto featureIdsPtr = std::make_shared(k_NumTuples, 0); + auto* featureIdsArray = Int32Array::Create(dataStructure, ::k_FeatureIds, featureIdsPtr, cellData->getId()); + + // Index 0, 14, 31 + auto& featureIds = featureIdsArray->getDataStoreRef(); + featureIds[0] = 0; + featureIds[1] = 1; + featureIds[2] = 1; + featureIds[3] = 2; + + featureIds[4] = 2; + featureIds[5] = 1; + featureIds[6] = 2; + featureIds[7] = 2; + + featureIds[8] = 1; + featureIds[9] = 1; + featureIds[10] = 0; + featureIds[11] = 2; + + featureIds[12] = 2; + featureIds[13] = 0; + featureIds[14] = 0; + featureIds[15] = 3; + // Z + featureIds[16] = 4; + featureIds[17] = 4; + featureIds[18] = 4; + featureIds[19] = 4; + + featureIds[20] = 3; + featureIds[21] = 3; + featureIds[22] = 3; + featureIds[23] = 3; + + featureIds[24] = 5; + featureIds[25] = 5; + featureIds[26] = 5; + featureIds[27] = 5; + + featureIds[28] = 5; + featureIds[29] = 6; + featureIds[30] = 6; + featureIds[31] = 0; + + // Misc DataArray + auto dataStorePtr = std::make_shared(k_NumTuples, 0); + auto* miscArray = Int32Array::Create(dataStructure, k_MiscData, dataStorePtr, cellData->getId()); + + auto& dataStore = miscArray->getDataStoreRef(); + for(usize i = 0; i < dataStore.size(); i++) + { + dataStore[i] = i; + } - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_6_erode_dilate_test.tar.gz", "6_6_erode_dilate_test"); + return dataStructure; +} - UnitTest::LoadPlugins(); +// Erode 1 +void CheckDataErode1XYZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; - // Read Exemplar DREAM3D File Filter - auto exemplarFilePath = fs::path(fmt::format("{}/6_6_erode_dilate_test/6_6_erode_dilate_bad_data.dream3d", unit_test::k_TestFilesDir)); - DataStructure dataStructure = LoadDataStructure(exemplarFilePath); + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataErode1YZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; + for(usize i = 0; i < dataStore.size(); i++) { - const ErodeDilateBadDataFilter filter; - Arguments args; + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataErode1Z(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; - // Create default Parameters for the filter. - args.insertOrAssign(ErodeDilateBadDataFilter::k_Operation_Key, std::make_any(k_Erode)); - args.insertOrAssign(ErodeDilateBadDataFilter::k_NumIterations_Key, std::make_any(2)); - args.insertOrAssign(ErodeDilateBadDataFilter::k_XDirOn_Key, std::make_any(true)); - args.insertOrAssign(ErodeDilateBadDataFilter::k_YDirOn_Key, std::make_any(true)); - args.insertOrAssign(ErodeDilateBadDataFilter::k_ZDirOn_Key, std::make_any(true)); - args.insertOrAssign(ErodeDilateBadDataFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_FeatureIdsDataPath)); - args.insertOrAssign(ErodeDilateBadDataFilter::k_IgnoredDataArrayPaths_Key, std::make_any(MultiArraySelectionParameter::ValueType{})); - args.insertOrAssign(ErodeDilateBadDataFilter::k_SelectedImageGeometryPath_Key, std::make_any(k_InputData)); + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataErode1XZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataErode1X(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; - // Execute the filter and check the result - auto executeResult = filter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); } +} +void CheckDataErode1XY(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; -// Write the DataStructure out to the file system -#ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fs::path(fmt::format("{}/7_0_erode_dilate_bad_data.dream3d", unit_test::k_BinaryTestOutputDir))); -#endif + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataErode1Y(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; - const std::string k_ExemplarDataContainerName("Exemplar Bad Data Erode"); - const DataPath k_ErodeCellAttributeMatrixDataPath = DataPath({k_ExemplarDataContainerName, "EBSD Scan Data"}); + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} - UnitTest::CompareExemplarToGeneratedData(dataStructure, dataStructure, k_EbsdScanDataDataPath, k_ExemplarDataContainerName); +void CheckDataErode1(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore, const std::array& dir) +{ + if(dir[0]) + { + if(dir[1]) + { + if(dir[2]) + { + CheckDataErode1XYZ(featureIds, dataStore); + } + else + { + CheckDataErode1XY(featureIds, dataStore); + } + } + // Not Y + else + { + if(dir[2]) + { + CheckDataErode1XZ(featureIds, dataStore); + } + else + { + CheckDataErode1X(featureIds, dataStore); + } + } + } + // Not X + else + { + if(dir[1]) + { + if(dir[2]) + { + CheckDataErode1YZ(featureIds, dataStore); + } + else + { + CheckDataErode1Y(featureIds, dataStore); + } + } + // Not Y + else + { + if(dir[2]) + { + CheckDataErode1Z(featureIds, dataStore); + } + else + { + REQUIRE(false); + } + } + } +} - UnitTest::CheckArraysInheritTupleDims(dataStructure); +// Erode 2 +void CheckDataErode2XYZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 6, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } } +void CheckDataErode2YZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 6, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; -TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Dilate)", "[SimplnxCore][ErodeDilateBadDataFilter]") + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataErode2Z(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) { - UnitTest::LoadPlugins(); + std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 6, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_6_erode_dilate_test.tar.gz", "6_6_erode_dilate_test"); + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataErode2XY(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 6, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; - const std::string k_ExemplarDataContainerName("Exemplar Bad Data Dilate"); - const DataPath k_DilateCellAttributeMatrixDataPath = DataPath({k_ExemplarDataContainerName, "EBSD Scan Data"}); + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataErode2X(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 6, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataErode2XZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 6, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataErode2Y(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 6, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} + +void CheckDataErode2(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore, const std::array& dir) +{ + if(dir[0]) + { + if(dir[1]) + { + if(dir[2]) + { + CheckDataErode2XYZ(featureIds, dataStore); + } + else + { + CheckDataErode2XY(featureIds, dataStore); + } + } + // Not Y + else + { + if(dir[2]) + { + CheckDataErode2XZ(featureIds, dataStore); + } + else + { + CheckDataErode2X(featureIds, dataStore); + } + } + } + // Not X + else + { + if(dir[1]) + { + if(dir[2]) + { + CheckDataErode2YZ(featureIds, dataStore); + } + else + { + CheckDataErode2Y(featureIds, dataStore); + } + } + // Not Y + else + { + if(dir[2]) + { + CheckDataErode2Z(featureIds, dataStore); + } + else + { + REQUIRE(false); + } + } + } +} + +void CheckDataErode(Int32Array& featureIdsArray, Int32Array& dataArray, int32 numIterations, const std::array& directions) +{ + const auto& featureIds = featureIdsArray.getDataStoreRef(); + const auto& dataStore = dataArray.getDataStoreRef(); + // Close up 0 features + switch(numIterations) + { + case 1: + CheckDataErode1(featureIds, dataStore, directions); + break; + case 2: + CheckDataErode2(featureIds, dataStore, directions); + break; + default: + REQUIRE(false); + } +} + +// Dilate +void CheckDataDilate1XYZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 10, 7, 8, 13, 10, 11, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 31, 28, 29, 30, 31}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 6, 0}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataDilate1XY(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 10, 7, 8, 13, 10, 11, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 31, 28, 29, 30, 31}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 6, 0}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataDilate1XZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 10, 7, 8, 13, 10, 11, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 31, 28, 29, 30, 31}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 6, 0}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataDilate1X(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 10, 7, 8, 13, 10, 11, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 31, 28, 29, 30, 31}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 6, 0}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataDilate1YZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 10, 7, 8, 13, 10, 11, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 31, 28, 29, 30, 31}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 6, 0}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataDilate1Y(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 10, 7, 8, 13, 10, 11, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 31, 28, 29, 30, 31}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 6, 0}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataDilate1Z(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 2, 3, 4, 5, 10, 7, 8, 13, 10, 11, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 31, 28, 29, 30, 31}; + std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 6, 0}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} + +void CheckDataDilate1(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore, const std::array& dir) +{ + if(dir[0]) + { + if(dir[1]) + { + if(dir[2]) + { + CheckDataDilate1XYZ(featureIds, dataStore); + } + else + { + CheckDataDilate1XY(featureIds, dataStore); + } + } + // Not Y + else + { + if(dir[2]) + { + CheckDataDilate1XZ(featureIds, dataStore); + } + else + { + CheckDataDilate1X(featureIds, dataStore); + } + } + } + // Not X + else + { + if(dir[1]) + { + if(dir[2]) + { + CheckDataDilate1YZ(featureIds, dataStore); + } + else + { + CheckDataDilate1Y(featureIds, dataStore); + } + } + // Not Y + else + { + if(dir[2]) + { + CheckDataDilate1Z(featureIds, dataStore); + } + else + { + REQUIRE(false); + } + } + } +} + +void CheckDataDilate2XYZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 10, 3, 4, 13, 10, 7, 8, 13, 10, 31, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 31, 24, 25, 26, 31, 28, 29, 30, 31}; + std::vector exemplarFeatures{0, 1, 0, 2, 2, 0, 0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 5, 0, 5, 6, 6, 0}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataDilate2XY(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 10, 3, 4, 13, 10, 7, 8, 13, 10, 31, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 31, 24, 25, 26, 31, 28, 29, 30, 31}; + std::vector exemplarFeatures{0, 1, 0, 2, 2, 0, 0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 5, 0, 5, 6, 6, 0}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataDilate2XZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 10, 3, 4, 13, 10, 7, 8, 13, 10, 31, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 31, 24, 25, 26, 31, 28, 29, 30, 31}; + std::vector exemplarFeatures{0, 1, 0, 2, 2, 0, 0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 5, 0, 5, 6, 6, 0}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataDilate2X(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 10, 3, 4, 13, 10, 7, 8, 13, 10, 31, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 31, 24, 25, 26, 31, 28, 29, 30, 31}; + std::vector exemplarFeatures{0, 1, 0, 2, 2, 0, 0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 5, 0, 5, 6, 6, 0}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataDilate2YZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 10, 3, 4, 13, 10, 7, 8, 13, 10, 31, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 31, 24, 25, 26, 31, 28, 29, 30, 31}; + std::vector exemplarFeatures{0, 1, 0, 2, 2, 0, 0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 5, 0, 5, 6, 6, 0}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataDilate2Y(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 10, 3, 4, 13, 10, 7, 8, 13, 10, 31, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 31, 24, 25, 26, 31, 28, 29, 30, 31}; + std::vector exemplarFeatures{0, 1, 0, 2, 2, 0, 0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 5, 0, 5, 6, 6, 0}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} +void CheckDataDilate2Z(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +{ + std::vector exemplarData{0, 1, 10, 3, 4, 13, 10, 7, 8, 13, 10, 31, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 31, 24, 25, 26, 31, 28, 29, 30, 31}; + std::vector exemplarFeatures{0, 1, 0, 2, 2, 0, 0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 5, 0, 5, 6, 6, 0}; + + for(usize i = 0; i < dataStore.size(); i++) + { + REQUIRE(dataStore[i] == exemplarData[i]); + REQUIRE(featureIds[i] == exemplarFeatures[i]); + } +} + +void CheckDataDilate2(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore, const std::array& dir) +{ + if(dir[0]) + { + if(dir[1]) + { + if(dir[2]) + { + CheckDataDilate2XYZ(featureIds, dataStore); + } + else + { + CheckDataDilate2XY(featureIds, dataStore); + } + } + // Not Y + else + { + if(dir[2]) + { + CheckDataDilate2XZ(featureIds, dataStore); + } + else + { + CheckDataDilate2X(featureIds, dataStore); + } + } + } + // Not X + else + { + if(dir[1]) + { + if(dir[2]) + { + CheckDataDilate2YZ(featureIds, dataStore); + } + else + { + CheckDataDilate2Y(featureIds, dataStore); + } + } + // Not Y + else + { + if(dir[2]) + { + CheckDataDilate2Z(featureIds, dataStore); + } + else + { + REQUIRE(false); + } + } + } +} + +void CheckDataDilate(const Int32Array& featureIdsArray, const Int32Array& dataArray, usize numIterations, const std::array& dir) +{ + const auto& featureIds = featureIdsArray.getDataStoreRef(); + const auto& dataStore = dataArray.getDataStoreRef(); + + // Expand 0 features + switch(numIterations) + { + case 1: + CheckDataDilate1(featureIds, dataStore, dir); + break; + case 2: + CheckDataDilate2(featureIds, dataStore, dir); + break; + default: + REQUIRE(false); + } +} + +void RunFilter(DataStructure& dataStructure, ChoicesParameter::ValueType operation, int32 numIterations, const std::array& directions, const DataPath& geometryPath, + const DataPath& featureIdsPath) +{ + const ErodeDilateBadDataFilter filter; + Arguments args; + + // Create default Parameters for the filter. + args.insertOrAssign(ErodeDilateBadDataFilter::k_Operation_Key, std::make_any(operation)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_NumIterations_Key, std::make_any(numIterations)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_XDirOn_Key, std::make_any(directions[0])); + args.insertOrAssign(ErodeDilateBadDataFilter::k_YDirOn_Key, std::make_any(directions[1])); + args.insertOrAssign(ErodeDilateBadDataFilter::k_ZDirOn_Key, std::make_any(directions[2])); + args.insertOrAssign(ErodeDilateBadDataFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(featureIdsPath)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_IgnoredDataArrayPaths_Key, std::make_any(MultiArraySelectionParameter::ValueType{})); + args.insertOrAssign(ErodeDilateBadDataFilter::k_SelectedImageGeometryPath_Key, std::make_any(geometryPath)); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + // Execute the filter and check the result + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) +} + +TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded", "[SimplnxCore][ErodeDilateBadDataFilter]") +{ UnitTest::LoadPlugins(); - // Read Exemplar DREAM3D File Filter - auto exemplarFilePath = fs::path(fmt::format("{}/6_6_erode_dilate_test/6_6_erode_dilate_bad_data.dream3d", unit_test::k_TestFilesDir)); - DataStructure dataStructure = LoadDataStructure(exemplarFilePath); + bool dirX = GENERATE(true, false); + bool dirY = GENERATE(true, false); + bool dirZ = GENERATE(true, false); + + DataStructure dataStructure = CreateTestData(); + std::array directions = {dirX, dirY, dirZ}; + uint64 operation = nx::core::detail::k_ErodeIndex; + int32 numIterations = GENERATE(1, 2); + // At least one direction is required. + if(!dirX && !dirY && !dirZ) { - const ErodeDilateBadDataFilter filter; + return; + } - Arguments args; + RunFilter(dataStructure, operation, numIterations, directions, DataPath({k_ImageGeometry}), k_ImageFeatureIdsPath); + auto& dataArray = dataStructure.getDataRefAs(k_DataPath); + auto& featureIdsArray = dataStructure.getDataRefAs(k_ImageFeatureIdsPath); - // Create default Parameters for the filter. - args.insertOrAssign(ErodeDilateBadDataFilter::k_Operation_Key, std::make_any(k_Dilate)); - args.insertOrAssign(ErodeDilateBadDataFilter::k_NumIterations_Key, std::make_any(2)); - args.insertOrAssign(ErodeDilateBadDataFilter::k_XDirOn_Key, std::make_any(true)); - args.insertOrAssign(ErodeDilateBadDataFilter::k_YDirOn_Key, std::make_any(true)); - args.insertOrAssign(ErodeDilateBadDataFilter::k_ZDirOn_Key, std::make_any(true)); - args.insertOrAssign(ErodeDilateBadDataFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_FeatureIdsDataPath)); - args.insertOrAssign(ErodeDilateBadDataFilter::k_IgnoredDataArrayPaths_Key, std::make_any(MultiArraySelectionParameter::ValueType{})); - args.insertOrAssign(ErodeDilateBadDataFilter::k_SelectedImageGeometryPath_Key, std::make_any(k_InputData)); + CheckDataErode(featureIdsArray, dataArray, numIterations, directions); +} - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) +TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Dilate) Expanded", "[SimplnxCore][ErodeDilateBadDataFilter]") +{ + UnitTest::LoadPlugins(); + + bool dirX = GENERATE(true, false); + bool dirY = GENERATE(true, false); + bool dirZ = GENERATE(true, false); + + DataStructure dataStructure = CreateTestData(); + std::array directions = {dirX, dirY, dirZ}; + uint64 operation = nx::core::detail::k_DilateIndex; + int32 numIterations = GENERATE(1, 2); - // Execute the filter and check the result - auto executeResult = filter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + // At least one direction is required. + if(!dirX && !dirY && !dirZ) + { + return; } - UnitTest::CompareExemplarToGeneratedData(dataStructure, dataStructure, k_EbsdScanDataDataPath, k_ExemplarDataContainerName); + RunFilter(dataStructure, operation, numIterations, directions, DataPath({k_ImageGeometry}), k_ImageFeatureIdsPath); + auto& dataArray = dataStructure.getDataRefAs(k_DataPath); + auto& featureIdsArray = dataStructure.getDataRefAs(k_ImageFeatureIdsPath); - UnitTest::CheckArraysInheritTupleDims(dataStructure); + CheckDataDilate(featureIdsArray, dataArray, numIterations, directions); +} + +TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Dilate) No Dimensions", "[SimplnxCore][ErodeDilateBadDataFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure = CreateTestData(); + std::array directions = {false, false, false}; + int32 operation = GENERATE(0, 1); + int32 numIterations = GENERATE(1, 2); + + DataPath imageGeomPath({k_ImageGeometry}); + + const ErodeDilateBadDataFilter filter; + Arguments args; + + // Create default Parameters for the filter. + args.insertOrAssign(ErodeDilateBadDataFilter::k_Operation_Key, std::make_any(operation)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_NumIterations_Key, std::make_any(numIterations)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_XDirOn_Key, std::make_any(directions[0])); + args.insertOrAssign(ErodeDilateBadDataFilter::k_YDirOn_Key, std::make_any(directions[1])); + args.insertOrAssign(ErodeDilateBadDataFilter::k_ZDirOn_Key, std::make_any(directions[2])); + args.insertOrAssign(ErodeDilateBadDataFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_ImageFeatureIdsPath)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_IgnoredDataArrayPaths_Key, std::make_any(MultiArraySelectionParameter::ValueType{})); + args.insertOrAssign(ErodeDilateBadDataFilter::k_SelectedImageGeometryPath_Key, std::make_any(imageGeomPath)); + + auto& imageGeom = dataStructure.getDataRefAs(imageGeomPath); + imageGeom.setDimensions(SizeVec3{0, 0, 0}); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions) +} + +TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Dilate) No Direction", "[SimplnxCore][ErodeDilateBadDataFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure = CreateTestData(); + std::array directions = {false, false, false}; + int32 operation = GENERATE(0, 1); + int32 numIterations = GENERATE(1, 2); + + const ErodeDilateBadDataFilter filter; + Arguments args; + + // Create default Parameters for the filter. + args.insertOrAssign(ErodeDilateBadDataFilter::k_Operation_Key, std::make_any(operation)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_NumIterations_Key, std::make_any(numIterations)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_XDirOn_Key, std::make_any(directions[0])); + args.insertOrAssign(ErodeDilateBadDataFilter::k_YDirOn_Key, std::make_any(directions[1])); + args.insertOrAssign(ErodeDilateBadDataFilter::k_ZDirOn_Key, std::make_any(directions[2])); + args.insertOrAssign(ErodeDilateBadDataFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_ImageFeatureIdsPath)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_IgnoredDataArrayPaths_Key, std::make_any(MultiArraySelectionParameter::ValueType{})); + args.insertOrAssign(ErodeDilateBadDataFilter::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({k_ImageGeometry}))); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions) } TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter: SIMPL Backwards Compatibility", "[SimplnxCore][ErodeDilateBadDataFilter][BackwardsCompatibility]") @@ -131,7 +829,7 @@ TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter: SIMPL Backwards Compatibility" UnitTest::LoadPlugins(); auto filterList = app->getFilterList(); - const fs::path conversionDir = fs::path(nx::core::unit_test::k_SourceDir.view()) / "test" / "simpl_conversion"; + const fs::path conversionDir = fs::path(unit_test::k_SourceDir.view()) / "test" / "simpl_conversion"; const std::vector> fixtures = { {"SIMPL 6.5 (UUID)", conversionDir / "6_5" / "ErodeDilateBadDataFilter.json"}, @@ -158,7 +856,7 @@ TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter: SIMPL Backwards Compatibility" CHECK(pipelineFilter->getComments().empty()); const Arguments args = pipelineFilter->getArguments(); - CHECK(args.value(ErodeDilateBadDataFilter::k_Operation_Key) == 0); + CHECK(args.value(ErodeDilateBadDataFilter::k_Operation_Key) == k_Dilate); CHECK(args.value(ErodeDilateBadDataFilter::k_NumIterations_Key) == 5); CHECK(args.value(ErodeDilateBadDataFilter::k_XDirOn_Key) == true); CHECK(args.value(ErodeDilateBadDataFilter::k_YDirOn_Key) == true); @@ -168,4 +866,4 @@ TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter: SIMPL Backwards Compatibility" // Complex type (MultiDataArraySelectionFilterParameterConverter) - verified by successful pipeline loading } } -} +} \ No newline at end of file diff --git a/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md b/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md new file mode 100644 index 0000000000..04a451ebb2 --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md @@ -0,0 +1,90 @@ +# V&V Report: ErodeDilateBadDataFilter + +| | | +|-----------------------------|--------------------------------------------------------------------------| +| Plugin | SimplnxCore | +| SIMPLNX UUID | `7f2f7378-580e-4337-8c04-a29e7883db0b` | +| SIMPLNX Human Name | Erode/Dilate Bad Data | +| DREAM3D 6.5.171 equivalent | `ErodeDilateBadData`, SIMPL UUID `3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254` (legacy source not present in this repository — see Algorithm Relationship) | +| Verified commit | `3cd0f6cbd` (branch `vv/ErodeDialateBadData`) plus SIMPL-backwards-compatibility `TEST_CASE` added to `test/ErodeDilateBadDataTest.cpp` this pass — `SimplnxCoreUnitTest.exe` (Debug) built and run locally 2026-07-23 | +| Status | READY FOR REVIEW | +| Sign-off | *pending* | + +## At a glance + +| Aspect | Current state | +|------------------------|----------------| +| Algorithm Relationship | **Port** (inferred) — filter markdown description and legacy SIMPL UUID mapping confirm this replaces legacy `ErodeDilateBadData`; no line-level diff against legacy source was possible (source not in this repo). | +| Oracle (confirmed) | **Class 1 (Analytical)** — expected outputs are hand-traced against a fixed 32-voxel (4×4×2) synthetic `FeatureIds` dataset with 5 bad-data voxels, for both operations, both iteration counts, and all 7 valid face-direction combinations. | +| Code paths enumerated | 8 of 9 paths exercised. 1 confirmed gap: the zero-dimensions preflight error is never reached by any test (see below). | +| Tests today | **5 TEST_CASEs, all pass** (built + run locally, 1883 assertions): `(Erode) Expanded`, `(Dilate) Expanded` (GENERATE sweep, 14 valid runs each), `(Dilate) No Dimensions`, `(Dilate) No Direction`, and `: SIMPL Backwards Compatibility` (new this pass — 2 `DYNAMIC_SECTION`s, 6.4 and 6.5, 27 assertions, both pass). | +| Test fixtures | Inline `CreateTestData()` — no exemplar archive. 32-voxel `ImageGeom` (4×4×2), hand-set `FeatureIds` (5 bad voxels at indices 0, 10, 13, 14, 31; features 1–6 elsewhere) plus a `Misc` int32 array initialized to its own index (`data[i] = i`) so every transferred value traces back to its source voxel unambiguously. | +| Legacy comparison | **Not performed.** Oracle is analytical only; no DREAM3D 6.5.171 pipeline/binary comparison was run for this V&V pass, and legacy source is not available in this repository to diff against. | +| Bug flags | None confirmed. One implementation detail (`adjustValidNeighbors`) and one observed fixture characteristic are flagged below for second-engineer attention — see Code path coverage. | +| V&V phase | Tests pass as written, including the newly added SIMPL backwards-compatibility test. Outstanding before promotion: (1) add a fixture that actually distinguishes direction combinations (see finding below); (2) add a dedicated zero-dimensions preflight test that doesn't also trip the no-direction check; (3) second-engineer review of the `adjustValidNeighbors` direction-masking implementation; (4) commit the new test case (currently uncommitted on this branch). | + +## Summary + +`ErodeDilateBadDataFilter` either erodes or dilates voxels with `FeatureId == 0` ("bad data") in an `ImageGeometry`. In *dilate* mode, every good voxel face-adjacent to a bad voxel has its data overwritten by the bad voxel's data (the bad region grows by one voxel per iteration). In *erode* mode, each bad voxel is assigned the data of whichever good face-neighbor's feature id occurs most often among its valid neighbors (first-processed wins on a tie). The operation repeats for a configurable number of iterations and can be restricted to any non-empty combination of X, Y, and Z face directions. + +Verification is via a **Class 1 (Analytical) oracle**: two `GENERATE`-driven test cases (`(Erode) Expanded`, `(Dilate) Expanded`) sweep all 7 valid direction combinations (all-off is skipped) × 2 iteration counts against hand-traced expected `FeatureIds`/`Misc` arrays for a small, fully-inspectable 32-voxel dataset. Both tests pass, as do two preflight-error tests. A `SIMPL Backwards Compatibility` test (both the SIMPL 6.4 and 6.5 legacy pipeline JSON fixtures) was added this pass and also passes. Built and executed locally against the current branch head: **5/5 test cases pass, 1883 assertions**. + +A concrete, verified gap: for this specific fixture, the 7 per-direction-combination expected-value functions (`CheckDataErode1XYZ`, `CheckDataErode1XY`, `CheckDataErode1XZ`, `CheckDataErode1X`, `CheckDataErode1YZ`, `CheckDataErode1Y`, `CheckDataErode1Z`, and their `Erode2`/`Dilate1`/`Dilate2` counterparts) all encode byte-identical expected arrays. The fixture therefore validates the core neighbor-voting/marking logic thoroughly, but does not actually discriminate "direction flag correctly restricts which neighbors participate" from "direction flag has no effect" for this dataset — see Code path coverage for detail and a recommended fixture change. + +## Algorithm Relationship + +*Classification:* **Port** (inferred) ~~| Minor changes | Rewrite | New filter~~ + +*Evidence available:* +- The SIMPLNX filter markdown (`docs/ErodeDilateBadDataFilter.md`) describes the same semantics as legacy DREAM3D — erode assigns the majority neighbor feature id ("if there is a tie... one... chosen randomly" — legacy phrasing retained), dilate grows the bad region by overwriting good neighbors. This text reads as carried over from the legacy filter's own documentation. +- `SimplnxCoreLegacyUUIDMapping.hpp` maps legacy SIMPL UUID `3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254` directly to `FilterTraits`, and `test/simpl_conversion/{6_4,6_5}/ErodeDilateBadDataFilter.json` carry the legacy `Direction`/`NumIterations`/`XDirOn`/`YDirOn`/`ZDirOn`/`FeatureIdsArrayPath`/`IgnoredDataArrayPaths` parameter set unchanged — this is the same filter, not a reimplementation with a different parameter model. +- **What could not be verified:** the legacy DREAM3D 6.5.171 C++ source (`Source/Plugins/Processing/ProcessingFilters/ErodeDilateBadData.{h,cpp}`) is not present in this repository, so no line-level comparison of the vote/tie-break/boundary-handling logic against the legacy implementation was possible in this pass. The "Port" classification should be treated as inferred from documentation and UUID/parameter continuity, not confirmed by source diff. + +*SIMPLNX implementation:* `Algorithms/ErodeDilateBadData.cpp` (~226 lines) uses `NeighborUtilities::VoxelNeighbors` for face-neighbor offsets and boundary validity, and `ParallelTaskAlgorithm` to transfer non-`FeatureIds` arrays in parallel (with `FeatureIds` itself transferred afterward, serially, since the transfer condition for every other array depends on the *current* `FeatureIds` values). + +## Oracle + +*Class:* **1 (Analytical)** — confirmed 2026-07-23. + +*Applied:* `CreateTestData()` builds an in-memory 4×4×2 (32-voxel) `ImageGeom` with a hand-authored `FeatureIds` array (features 1–6, with bad voxels at flat indices 0, 10, 13, 14, and 31) and a `Misc` `int32` array initialized so `Misc[i] == i`, making every copied tuple traceable to its source voxel by value alone. Expected output arrays (`exemplarData`/`exemplarFeatures`, despite the "exemplar" naming these are hand-derived, not legacy-sourced) are provided per operation (Erode/Dilate), per iteration count (1, 2), and per direction combination (XYZ, XY, XZ, YZ, X, Y, Z) via 28 dedicated `CheckData*` functions in `test/ErodeDilateBadDataTest.cpp`. + +*Encoded:* `SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded` and `(Dilate) Expanded` — each `GENERATE`s `dirX,dirY,dirZ ∈ {true,false}` and `numIterations ∈ {1,2}`, skips the all-directions-off combination (invalid per preflight), and dispatches to the matching `CheckData{Erode,Dilate}{1,2}{XYZ,XY,XZ,YZ,X,Y,Z}` function. **14 valid parameterized runs each for Erode and Dilate — all pass** (verified by local build+run, not just static review). + +*Second-engineer review:* *Pending.* Recommend focused review of: (1) the erode tie-break order (first-processed-neighbor-wins, per `faceNeighborInternalIdx` iteration order `[-Z,-Y,-X,+X,+Y,+Z]`) against the intended/legacy semantics; (2) whether the fixture should be extended so that direction combinations produce genuinely different expected output (see below). + +## Code path coverage + +8 of 9 paths exercised. Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp`. + +| # | Phase | Path | Test case | +|---|-------|------|-----------| +| 1 | Setup | `numFeatures` scan, face-offset/validity initialization, `adjustValidNeighbors` direction masking | All tests | +| 2 | (b) Per-voxel | `featureName != 0` (good voxel) → skip | All tests (majority of the 32 voxels are good) | +| 3 | (b) Per-voxel | `featureName == 0` + Dilate + neighbor `feature > 0` → `neighbors[neighborPoint] = voxelIndex` | `(Dilate) Expanded` | +| 4 | (b) Per-voxel | `featureName == 0` + Erode + neighbor `feature > 0` → vote accumulation, `neighbors[voxelIndex] = neighborPoint` on new-max (ties keep the first-processed neighbor) | `(Erode) Expanded` | +| 5 | (b) Per-voxel | Erode post-vote cleanup — `featureCount[feature] = 0` for each valid neighbor of the bad voxel | `(Erode) Expanded` (implicitly, via correct 2-iteration results) | +| 6 | (c) Transfer | `neighbor >= 0` + Erode condition (`featureName==0 && featureIds[neighbor]>0`) → `copyTuple` | `(Erode) Expanded` | +| 7 | (c) Transfer | `neighbor >= 0` + Dilate condition (`featureName>0 && featureIds[neighbor]==0`) → `copyTuple` | `(Dilate) Expanded` | +| 8 | (c) Transfer | `neighbor == -1` → skip (voxel untouched this iteration) | Both `Expanded` tests, implicitly (voxels far from bad data are unchanged in every expected array) | +| 9 | Preflight | `dims[0]==0 && dims[1]==0 && dims[2]==0` → error `-14602` (`k_NoGeometryDimensions`) | **Not covered.** The only test that zeroes the geometry dimensions (`(Dilate) No Dimensions`) *also* sets all three direction flags off, so the earlier `-14601` (`k_NoDirections_Error`) check fires first and the zero-dimensions branch is never reached. Confirmed by running the test locally: its assertion message is `-14601`, not `-14602`, despite the test's name. | + +Additional confirmed items, not path gaps but worth recording: + +- **No cancel path exists.** `m_ShouldCancel` is passed into `ErodeDilateBadData` and exposed via `getCancel()`, but `operator()` never reads it. The erode/dilate loop runs to completion regardless of a cancellation request — this is a behavior characteristic of the current implementation, not merely an untested path. +- **Direction masking is implemented unusually.** `adjustValidNeighbors` bitwise-ANDs the *face-index constants themselves* (`faceNeighborInternalIdx`, values 0–5) against the direction booleans, rather than gating a separate boolean-validity array. Combined with the observation that all 7 direction-combination fixtures for a given operation/iteration-count produce byte-identical expected output (see Summary and Oracle), this is flagged for second-engineer scrutiny — not as a confirmed defect (the current fixture cannot distinguish correct per-direction gating from a no-op direction gate), but as an area where an independent reviewer should hand-trace at least one single-axis-only case (e.g. Erode, `X` only, on a voxel whose good neighbors differ between the X-only and XYZ neighbor sets) to positively confirm the direction restriction behaves as documented. + +## Test inventory + +| Test case | Notes | +|-----------|-------| +| `SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded` | Class 1 oracle. `GENERATE` over 7 valid direction combinations × 2 iteration counts (14 runs). Compares `FeatureIds` and `Misc` against hand-traced expected arrays. Passes. | +| `SimplnxCore::ErodeDilateBadDataFilter(Dilate) Expanded` | Same sweep, Dilate operation. Passes. | +| `SimplnxCore::ErodeDilateBadDataFilter(Dilate) No Dimensions` | Preflight-error test: `ImageGeom` dimensions forced to `{0,0,0}`, directions also all off. Asserts `preflightResult.outputActions.invalid()`. **Misleading name** — actually exercises the no-direction path (`-14601`), not the zero-dimensions path (`-14602`), because directions are also off and that check runs first. | +| `SimplnxCore::ErodeDilateBadDataFilter(Dilate) No Direction` | Preflight-error test: all directions off, geometry otherwise valid. Asserts `-14601`. Correctly named and covers the intended path. | +| `SimplnxCore::ErodeDilateBadDataFilter: SIMPL Backwards Compatibility` | **New this pass.** `DYNAMIC_SECTION` over `simpl_conversion/6_5/ErodeDilateBadDataFilter.json` (matched by `Filter_Uuid`) and `simpl_conversion/6_4/ErodeDilateBadDataFilter.json` (matched by `Filter_Name`, no UUID field present in that fixture). Loads each legacy pipeline JSON via `Pipeline::FromSIMPLFile`, confirms it resolves to a single `PipelineFilter` with `FilterTraits::uuid`, and checks the converted arguments: `Operation == k_Dilate` (legacy `Direction: 0` round-trips to SIMPLNX's own `Dilate = 0`), `NumIterations == 5`, `XDirOn/YDirOn/ZDirOn == true`, geometry path `DataPath({"DataContainer"})`, feature-ids path `DataPath({"DataContainer","CellData","TestArray"})`. `IgnoredDataArrayPaths` (a `MultiDataArraySelectionFilterParameterConverter`) is verified only by successful pipeline load, not by value, matching the pattern used in `FillBadDataTest.cpp`. **27 assertions, both fixtures pass.** | + +Both `simpl_conversion/6_4/ErodeDilateBadDataFilter.json` and `simpl_conversion/6_5/ErodeDilateBadDataFilter.json` were already present on disk (as they are for sibling filters such as `FillBadDataFilter`) but were unused until this pass — the gap noted in the previous revision of this report is now closed. + +## Deviations from DREAM3D 6.5.171 + +Not evaluated in this pass — see [`deviations/ErodeDilateBadDataFilter.md`](deviations/ErodeDilateBadDataFilter.md). No legacy binary/pipeline comparison has been run for this filter; the oracle is Class 1 (Analytical) only, and legacy source is not present in this repository to support a source-level diff. diff --git a/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md new file mode 100644 index 0000000000..1601e1df59 --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md @@ -0,0 +1,23 @@ +# Deviations from DREAM3D 6.5.171: ErodeDilateBadDataFilter + +Entries use stable IDs (`ErodeDilateBadDataFilter-D` for legacy deviations, `ErodeDilateBadDataFilter-B` for SIMPLNX-side bugs). + +--- + +## Headline: No legacy comparison has been performed + +The [V&V report](../ErodeDilateBadDataFilter.md) for this filter uses a **Class 1 (Analytical) oracle only** — expected outputs are hand-traced against a small synthetic dataset, independent of any DREAM3D 6.5.171 run. No pipeline was executed in legacy DREAM3D 6.5.171 to produce a reference `.dream3d` file, and the legacy `ErodeDilateBadData` C++ source (`Source/Plugins/Processing/ProcessingFilters/ErodeDilateBadData.{h,cpp}`) is not present in this repository, so no source-level diff was possible either. + +Consequently, this file records **no confirmed deviations** — not because none exist, but because the comparison that would surface them has not been done. This is a gap, not a clean bill of health. + +## What would need to happen to fill this in + +1. Obtain or build a DREAM3D 6.5.171 binary (available locally at `C:\Users\holym\BlueQuartz\Builds\DREAM3D\DREAM3D-6.5.171-Win64` on this machine) and run an `ErodeDilateBadData` pipeline against a shared input dataset, in both Erode and Dilate modes, covering at least one case where direction restriction actually changes the result (see the V&V report's note that the current Class 1 fixture is direction-invariant for all 7 combinations it exercises). +2. Compare the legacy output against SIMPLNX output on the same input, using the same comparison discipline as other filters in this plugin (`UnitTest::CompareExemplarToGeneratedData` or equivalent element-wise check). +3. If legacy source becomes available for reference, diff the neighbor-selection, vote/tie-break, and direction-masking logic (`adjustValidNeighbors` in `Algorithms/ErodeDilateBadData.cpp`) against it directly — this is the one piece of the current implementation flagged for second-engineer scrutiny in the V&V report, precisely because the tie-break/direction-masking behavior could not be corroborated against a reference. + +## Non-deviations (documented for awareness) + +### Legacy tie-break language says "chosen randomly"; SIMPLNX is deterministic + +The SIMPLNX filter markdown (`docs/ErodeDilateBadDataFilter.md`), which reads as carried over from legacy documentation, states that erode ties are broken "randomly." The current SIMPLNX implementation is deterministic: the first-processed neighbor (by `faceNeighborInternalIdx` order, `[-Z,-Y,-X,+X,+Y,+Z]`) wins ties, since a later neighbor's vote must strictly exceed the current maximum to replace it. Whether legacy DREAM3D 6.5.171 was actually nondeterministic (e.g., using an RNG) or merely used "random" loosely to mean "implementation-defined scan-order" has not been verified against legacy source. Recorded here as a documentation-language discrepancy worth resolving once legacy source or a legacy binary comparison is available, not asserted as a behavioral deviation. From cf69f872e27f095489ba9e18c3b5498cdb93974e Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Thu, 23 Jul 2026 17:04:38 -0400 Subject: [PATCH 06/14] Clang format --- .../src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp index a1ed182bfb..b0950a4875 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp @@ -23,7 +23,7 @@ namespace { int32 k_NoDirections_Error = -14601; int32 k_NoGeometryDimensions = -14602; -} +} // namespace namespace nx::core { @@ -115,7 +115,7 @@ IFilter::PreflightResult ErodeDilateBadDataFilter::preflightImpl(const DataStruc std::vector preflightUpdatedValues; - if (!xDirOn && !yDirOn && !zDirOn) + if(!xDirOn && !yDirOn && !zDirOn) { return {MakeErrorResult(k_NoDirections_Error, "ErodeDilateBadData requires at least one direction to operate over")}; } From 5bbef82fe89ea3fb6dcf0c4ab6332aab971fb14c Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Tue, 11 Aug 2026 12:31:50 -0400 Subject: [PATCH 07/14] Fixed ErodeDilateBadData exemplar test values Values taken from DREAM.3D for A/B testing. --- .../test/ErodeDilateBadDataTest.cpp | 805 ++++++------------ 1 file changed, 264 insertions(+), 541 deletions(-) diff --git a/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp b/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp index 5c67131bfd..f26c9c5a82 100644 --- a/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp +++ b/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp @@ -37,7 +37,103 @@ const ShapeType k_TupleShape{4, 4, 2}; const usize k_NumTuples = 32; const DataPath k_DataPath({::k_ImageGeometry, ::k_CellData, k_MiscData}); const DataPath k_ImageFeatureIdsPath({::k_ImageGeometry, ::k_CellData, k_FeatureIds}); -} // namespace + +using DirectionType = std::array; +constexpr DirectionType k_XDir{true, false, false}; +constexpr DirectionType k_XYDir{true, true, false}; +constexpr DirectionType k_XYZDir{true, true, true}; +constexpr DirectionType k_XZDir{true, false, true}; +constexpr DirectionType k_YDir{false, true, false}; +constexpr DirectionType k_YZDir{false, true, true}; +constexpr DirectionType k_ZDir{false, false, true}; + +using ExemplarDataType = std::array; +// Exemplar Dilate data for A/B testing +constexpr ExemplarDataType k_ExemplarFeatureIdsDilateX1{0, 0, 1, 2, 2, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 0, 0}; +constexpr ExemplarDataType k_ExemplarDataDilateX1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsDilateX2{0, 0, 0, 2, 2, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 0, 0, 0}; +constexpr ExemplarDataType k_ExemplarDataDilateX2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsDilateXY1{0, 0, 1, 2, 0, 1, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 0, 0}; +constexpr ExemplarDataType k_ExemplarDataDilateXY1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsDilateXY2{0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 0, 0, 5, 0, 0, 0}; +constexpr ExemplarDataType k_ExemplarDataDilateXY2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsDilateXYZ1{0, 0, 1, 2, 0, 1, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 3, 3, 3, 3, 5, 5, 0, 0, 5, 0, 0, 0}; +constexpr ExemplarDataType k_ExemplarDataDilateXYZ1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsDilateXYZ2{0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 3, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0}; +constexpr ExemplarDataType k_ExemplarDataDilateXYZ2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsDilateXZ1{0, 0, 1, 2, 2, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 3, 3, 3, 3, 5, 5, 0, 5, 5, 0, 0, 0}; +constexpr ExemplarDataType k_ExemplarDataDilateXZ1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsDilateXZ2{0, 0, 0, 2, 2, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 3, 3, 3, 3, 5, 0, 0, 0, 0, 0, 0, 0}; +constexpr ExemplarDataType k_ExemplarDataDilateXZ2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsDilateY1{0, 1, 1, 2, 0, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 6, 0}; +constexpr ExemplarDataType k_ExemplarDataDilateY1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsDilateY2{0, 1, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 2, 0, 0, 3, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 5, 0, 5, 6, 6, 0}; +constexpr ExemplarDataType k_ExemplarDataDilateY2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsDilateYZ1{0, 1, 1, 2, 0, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 0, 0, 4, 4, 4, 3, 3, 3, 3, 5, 5, 0, 0, 5, 0, 0, 0}; +constexpr ExemplarDataType k_ExemplarDataDilateYZ1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsDilateYZ2{0, 1, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 4, 4, 4, 0, 3, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0}; +constexpr ExemplarDataType k_ExemplarDataDilateYZ2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsDilateZ1{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 0, 2, 2, 0, 0, 0, 0, 4, 4, 4, 3, 3, 3, 3, 5, 5, 0, 5, 5, 0, 0, 0}; +constexpr ExemplarDataType k_ExemplarDataDilateZ1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsDilateZ2{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 0, 2, 2, 0, 0, 0, 0, 4, 4, 4, 3, 3, 3, 3, 5, 5, 0, 5, 5, 0, 0, 0}; +constexpr ExemplarDataType k_ExemplarDataDilateZ2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +// Exemplar Erode data for A/B testing +constexpr ExemplarDataType k_ExemplarFeatureIdsErodeX1{1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 6}; +constexpr ExemplarDataType k_ExemplarDataErodeX1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsErodeX2{1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 6}; +constexpr ExemplarDataType k_ExemplarDataErodeX2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsErodeXY1{1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 5}; +constexpr ExemplarDataType k_ExemplarDataErodeXY1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsErodeXY2{1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 5}; +constexpr ExemplarDataType k_ExemplarDataErodeXY2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsErodeXYZ1{1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; +constexpr ExemplarDataType k_ExemplarDataErodeXYZ1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsErodeXYZ2{1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; +constexpr ExemplarDataType k_ExemplarDataErodeXYZ2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsErodeXZ1{1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; +constexpr ExemplarDataType k_ExemplarDataErodeXZ1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsErodeXZ2{1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; +constexpr ExemplarDataType k_ExemplarDataErodeXZ2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsErodeY1{2, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 5}; +constexpr ExemplarDataType k_ExemplarDataErodeY1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsErodeY2{2, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 5}; +constexpr ExemplarDataType k_ExemplarDataErodeY2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsErodeYZ1{2, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 6, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; +constexpr ExemplarDataType k_ExemplarDataErodeYZ1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsErodeYZ2{2, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 6, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; +constexpr ExemplarDataType k_ExemplarDataErodeYZ2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsErodeZ1{4, 1, 1, 2, 2, 1, 2, 2, 1, 1, 5, 2, 2, 6, 6, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; +constexpr ExemplarDataType k_ExemplarDataErodeZ1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + +constexpr ExemplarDataType k_ExemplarFeatureIdsErodeZ2{4, 1, 1, 2, 2, 1, 2, 2, 1, 1, 5, 2, 2, 6, 6, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; +constexpr ExemplarDataType k_ExemplarDataErodeZ2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; + DataStructure CreateTestData() { @@ -106,585 +202,137 @@ DataStructure CreateTestData() return dataStructure; } -// Erode 1 -void CheckDataErode1XYZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +void CheckPathIgnored(const DataStructure& dataStructure) { - std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; + DataStructure exemplarStructure = CreateTestData(); + DataPath ignoredPath({k_ImageGeometry, k_CellData, k_MiscData}); - for(usize i = 0; i < dataStore.size(); i++) - { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); - } -} -void CheckDataErode1YZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; + const auto* dataArray = dataStructure.getDataAs(ignoredPath); + const auto* exemplarArray = exemplarStructure.getDataAs(ignoredPath); - for(usize i = 0; i < dataStore.size(); i++) - { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); - } -} -void CheckDataErode1Z(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; + const auto& dataStore = dataArray->getDataStoreRef(); + const auto& exemplarStore = exemplarArray->getDataStoreRef(); - for(usize i = 0; i < dataStore.size(); i++) + const usize size = dataStore.size(); + for(usize i = 0; i < size; i++) { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); + REQUIRE(dataStore[i] == exemplarStore[i]); } } -void CheckDataErode1XZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; - for(usize i = 0; i < dataStore.size(); i++) - { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); - } -} -void CheckDataErode1X(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +void CheckOutput(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore, const ExemplarDataType& exemplarFeatureIds, const ExemplarDataType& exemplarData) { - std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; + REQUIRE(dataStore.size() == exemplarData.size()); for(usize i = 0; i < dataStore.size(); i++) - { + { REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); + REQUIRE(featureIds[i] == exemplarFeatureIds[i]); } } -void CheckDataErode1XY(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; - for(usize i = 0; i < dataStore.size(); i++) - { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); - } -} -void CheckDataErode1Y(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +void CheckDilateOutput(const DataStructure& dataStructure, const DirectionType& directions, int32 iterations) { - std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; + const Int32AbstractDataStore& featureIds = dataStructure.getDataRefAs(k_ImageFeatureIdsPath).getDataStoreRef(); + const Int32AbstractDataStore& dataStore = dataStructure.getDataRefAs(k_DataPath).getDataStoreRef(); - for(usize i = 0; i < dataStore.size(); i++) - { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); - } -} + ExemplarDataType exemplarFeatureIds; + ExemplarDataType exemplarData; + bool only1Iteration = iterations == 1; -void CheckDataErode1(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore, const std::array& dir) -{ - if(dir[0]) + if (directions == k_XDir) { - if(dir[1]) - { - if(dir[2]) - { - CheckDataErode1XYZ(featureIds, dataStore); - } - else - { - CheckDataErode1XY(featureIds, dataStore); - } - } - // Not Y - else - { - if(dir[2]) - { - CheckDataErode1XZ(featureIds, dataStore); - } - else - { - CheckDataErode1X(featureIds, dataStore); - } - } + exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsDilateX1 : k_ExemplarFeatureIdsDilateX2; + exemplarData = only1Iteration ? k_ExemplarDataDilateX1 : k_ExemplarDataDilateX2; } - // Not X - else + else if (directions == k_XYDir) { - if(dir[1]) - { - if(dir[2]) - { - CheckDataErode1YZ(featureIds, dataStore); - } - else - { - CheckDataErode1Y(featureIds, dataStore); - } - } - // Not Y - else - { - if(dir[2]) - { - CheckDataErode1Z(featureIds, dataStore); - } - else - { - REQUIRE(false); - } - } + exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsDilateXY1 : k_ExemplarFeatureIdsDilateXY2; + exemplarData = only1Iteration ? k_ExemplarDataDilateXY1 : k_ExemplarDataDilateXY2; } -} - -// Erode 2 -void CheckDataErode2XYZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 6, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; - - for(usize i = 0; i < dataStore.size(); i++) + else if(directions == k_XYZDir) { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); + exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsDilateXYZ1 : k_ExemplarFeatureIdsDilateXYZ2; + exemplarData = only1Iteration ? k_ExemplarDataDilateXYZ1 : k_ExemplarDataDilateXYZ2; } -} -void CheckDataErode2YZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 6, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; - - for(usize i = 0; i < dataStore.size(); i++) + else if(directions == k_XZDir) { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); + exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsDilateXZ1 : k_ExemplarFeatureIdsDilateXZ2; + exemplarData = only1Iteration ? k_ExemplarDataDilateXZ1 : k_ExemplarDataDilateXZ2; } -} -void CheckDataErode2Z(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 6, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; - - for(usize i = 0; i < dataStore.size(); i++) + else if(directions == k_YDir) { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); + exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsDilateY1 : k_ExemplarFeatureIdsDilateY2; + exemplarData = only1Iteration ? k_ExemplarDataDilateY1 : k_ExemplarDataDilateY2; } -} -void CheckDataErode2XY(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 6, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; - - for(usize i = 0; i < dataStore.size(); i++) - { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); - } -} -void CheckDataErode2X(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 6, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; - - for(usize i = 0; i < dataStore.size(); i++) - { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); - } -} -void CheckDataErode2XZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 6, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; - - for(usize i = 0; i < dataStore.size(); i++) - { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); - } -} -void CheckDataErode2Y(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 6, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; - - for(usize i = 0; i < dataStore.size(); i++) + else if(directions == k_YZDir) { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); + exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsDilateYZ1 : k_ExemplarFeatureIdsDilateYZ2; + exemplarData = only1Iteration ? k_ExemplarDataDilateYZ1 : k_ExemplarDataDilateYZ2; } -} - -void CheckDataErode2(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore, const std::array& dir) -{ - if(dir[0]) + else if(directions == k_ZDir) { - if(dir[1]) - { - if(dir[2]) - { - CheckDataErode2XYZ(featureIds, dataStore); - } - else - { - CheckDataErode2XY(featureIds, dataStore); - } - } - // Not Y - else - { - if(dir[2]) - { - CheckDataErode2XZ(featureIds, dataStore); - } - else - { - CheckDataErode2X(featureIds, dataStore); - } - } + exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsDilateZ1 : k_ExemplarFeatureIdsDilateZ2; + exemplarData = only1Iteration ? k_ExemplarDataDilateZ1 : k_ExemplarDataDilateZ2; } - // Not X else { - if(dir[1]) - { - if(dir[2]) - { - CheckDataErode2YZ(featureIds, dataStore); - } - else - { - CheckDataErode2Y(featureIds, dataStore); - } - } - // Not Y - else - { - if(dir[2]) - { - CheckDataErode2Z(featureIds, dataStore); - } - else - { - REQUIRE(false); - } - } - } -} - -void CheckDataErode(Int32Array& featureIdsArray, Int32Array& dataArray, int32 numIterations, const std::array& directions) -{ - const auto& featureIds = featureIdsArray.getDataStoreRef(); - const auto& dataStore = dataArray.getDataStoreRef(); - - // Close up 0 features - switch(numIterations) - { - case 1: - CheckDataErode1(featureIds, dataStore, directions); - break; - case 2: - CheckDataErode2(featureIds, dataStore, directions); - break; - default: REQUIRE(false); } -} - -// Dilate -void CheckDataDilate1XYZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 2, 3, 4, 5, 10, 7, 8, 13, 10, 11, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 31, 28, 29, 30, 31}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 6, 0}; - - for(usize i = 0; i < dataStore.size(); i++) - { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); - } -} -void CheckDataDilate1XY(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 2, 3, 4, 5, 10, 7, 8, 13, 10, 11, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 31, 28, 29, 30, 31}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 6, 0}; - - for(usize i = 0; i < dataStore.size(); i++) - { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); - } -} -void CheckDataDilate1XZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 2, 3, 4, 5, 10, 7, 8, 13, 10, 11, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 31, 28, 29, 30, 31}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 6, 0}; - - for(usize i = 0; i < dataStore.size(); i++) - { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); - } -} -void CheckDataDilate1X(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 2, 3, 4, 5, 10, 7, 8, 13, 10, 11, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 31, 28, 29, 30, 31}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 6, 0}; - for(usize i = 0; i < dataStore.size(); i++) - { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); - } + CheckOutput(featureIds, dataStore, exemplarFeatureIds, exemplarData); } -void CheckDataDilate1YZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 2, 3, 4, 5, 10, 7, 8, 13, 10, 11, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 31, 28, 29, 30, 31}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 6, 0}; - for(usize i = 0; i < dataStore.size(); i++) - { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); - } -} -void CheckDataDilate1Y(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) +void CheckErodeOutput(const DataStructure& dataStructure, const DirectionType& directions, int32 iterations) { - std::vector exemplarData{0, 1, 2, 3, 4, 5, 10, 7, 8, 13, 10, 11, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 31, 28, 29, 30, 31}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 6, 0}; + const Int32AbstractDataStore& featureIds = dataStructure.getDataRefAs(k_ImageFeatureIdsPath).getDataStoreRef(); + const Int32AbstractDataStore& dataStore = dataStructure.getDataRefAs(k_DataPath).getDataStoreRef(); - for(usize i = 0; i < dataStore.size(); i++) - { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); - } -} -void CheckDataDilate1Z(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 2, 3, 4, 5, 10, 7, 8, 13, 10, 11, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 31, 28, 29, 30, 31}; - std::vector exemplarFeatures{0, 1, 1, 2, 2, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 6, 0}; + ExemplarDataType exemplarFeatureIds; + ExemplarDataType exemplarData; + bool only1Iteration = iterations == 1; - for(usize i = 0; i < dataStore.size(); i++) + if(directions == k_XDir) { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); + exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsErodeX1 : k_ExemplarFeatureIdsErodeX2; + exemplarData = only1Iteration ? k_ExemplarDataErodeX1 : k_ExemplarDataErodeX2; } -} - -void CheckDataDilate1(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore, const std::array& dir) -{ - if(dir[0]) + else if(directions == k_XYDir) { - if(dir[1]) - { - if(dir[2]) - { - CheckDataDilate1XYZ(featureIds, dataStore); - } - else - { - CheckDataDilate1XY(featureIds, dataStore); - } - } - // Not Y - else - { - if(dir[2]) - { - CheckDataDilate1XZ(featureIds, dataStore); - } - else - { - CheckDataDilate1X(featureIds, dataStore); - } - } + exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsErodeXY1 : k_ExemplarFeatureIdsErodeXY2; + exemplarData = only1Iteration ? k_ExemplarDataErodeXY1 : k_ExemplarDataErodeXY2; } - // Not X - else + else if(directions == k_XYZDir) { - if(dir[1]) - { - if(dir[2]) - { - CheckDataDilate1YZ(featureIds, dataStore); - } - else - { - CheckDataDilate1Y(featureIds, dataStore); - } - } - // Not Y - else - { - if(dir[2]) - { - CheckDataDilate1Z(featureIds, dataStore); - } - else - { - REQUIRE(false); - } - } + exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsErodeXYZ1 : k_ExemplarFeatureIdsErodeXYZ2; + exemplarData = only1Iteration ? k_ExemplarDataErodeXYZ1 : k_ExemplarDataErodeXYZ2; } -} - -void CheckDataDilate2XYZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 10, 3, 4, 13, 10, 7, 8, 13, 10, 31, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 31, 24, 25, 26, 31, 28, 29, 30, 31}; - std::vector exemplarFeatures{0, 1, 0, 2, 2, 0, 0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 5, 0, 5, 6, 6, 0}; - - for(usize i = 0; i < dataStore.size(); i++) + else if(directions == k_XZDir) { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); + exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsErodeXZ1 : k_ExemplarFeatureIdsErodeXZ2; + exemplarData = only1Iteration ? k_ExemplarDataErodeXZ1 : k_ExemplarDataErodeXZ2; } -} -void CheckDataDilate2XY(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 10, 3, 4, 13, 10, 7, 8, 13, 10, 31, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 31, 24, 25, 26, 31, 28, 29, 30, 31}; - std::vector exemplarFeatures{0, 1, 0, 2, 2, 0, 0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 5, 0, 5, 6, 6, 0}; - - for(usize i = 0; i < dataStore.size(); i++) + else if(directions == k_YDir) { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); + exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsErodeY1 : k_ExemplarFeatureIdsErodeY2; + exemplarData = only1Iteration ? k_ExemplarDataErodeY1 : k_ExemplarDataErodeY2; } -} -void CheckDataDilate2XZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 10, 3, 4, 13, 10, 7, 8, 13, 10, 31, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 31, 24, 25, 26, 31, 28, 29, 30, 31}; - std::vector exemplarFeatures{0, 1, 0, 2, 2, 0, 0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 5, 0, 5, 6, 6, 0}; - - for(usize i = 0; i < dataStore.size(); i++) + else if(directions == k_YZDir) { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); + exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsErodeYZ1 : k_ExemplarFeatureIdsErodeYZ2; + exemplarData = only1Iteration ? k_ExemplarDataErodeYZ1 : k_ExemplarDataErodeYZ2; } -} -void CheckDataDilate2X(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 10, 3, 4, 13, 10, 7, 8, 13, 10, 31, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 31, 24, 25, 26, 31, 28, 29, 30, 31}; - std::vector exemplarFeatures{0, 1, 0, 2, 2, 0, 0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 5, 0, 5, 6, 6, 0}; - - for(usize i = 0; i < dataStore.size(); i++) + else if(directions == k_ZDir) { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); - } -} -void CheckDataDilate2YZ(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 10, 3, 4, 13, 10, 7, 8, 13, 10, 31, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 31, 24, 25, 26, 31, 28, 29, 30, 31}; - std::vector exemplarFeatures{0, 1, 0, 2, 2, 0, 0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 5, 0, 5, 6, 6, 0}; - - for(usize i = 0; i < dataStore.size(); i++) - { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); - } -} -void CheckDataDilate2Y(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 10, 3, 4, 13, 10, 7, 8, 13, 10, 31, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 31, 24, 25, 26, 31, 28, 29, 30, 31}; - std::vector exemplarFeatures{0, 1, 0, 2, 2, 0, 0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 5, 0, 5, 6, 6, 0}; - - for(usize i = 0; i < dataStore.size(); i++) - { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); + exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsErodeZ1 : k_ExemplarFeatureIdsErodeZ2; + exemplarData = only1Iteration ? k_ExemplarDataErodeZ1 : k_ExemplarDataErodeZ2; } -} -void CheckDataDilate2Z(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore) -{ - std::vector exemplarData{0, 1, 10, 3, 4, 13, 10, 7, 8, 13, 10, 31, 12, 13, 14, 31, 16, 17, 18, 19, 20, 21, 22, 31, 24, 25, 26, 31, 28, 29, 30, 31}; - std::vector exemplarFeatures{0, 1, 0, 2, 2, 0, 0, 2, 1, 0, 0, 0, 2, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 5, 0, 5, 6, 6, 0}; - - for(usize i = 0; i < dataStore.size(); i++) - { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatures[i]); - } -} - -void CheckDataDilate2(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore, const std::array& dir) -{ - if(dir[0]) - { - if(dir[1]) - { - if(dir[2]) - { - CheckDataDilate2XYZ(featureIds, dataStore); - } - else - { - CheckDataDilate2XY(featureIds, dataStore); - } - } - // Not Y - else - { - if(dir[2]) - { - CheckDataDilate2XZ(featureIds, dataStore); - } - else - { - CheckDataDilate2X(featureIds, dataStore); - } - } - } - // Not X else { - if(dir[1]) - { - if(dir[2]) - { - CheckDataDilate2YZ(featureIds, dataStore); - } - else - { - CheckDataDilate2Y(featureIds, dataStore); - } - } - // Not Y - else - { - if(dir[2]) - { - CheckDataDilate2Z(featureIds, dataStore); - } - else - { - REQUIRE(false); - } - } - } -} - -void CheckDataDilate(const Int32Array& featureIdsArray, const Int32Array& dataArray, usize numIterations, const std::array& dir) -{ - const auto& featureIds = featureIdsArray.getDataStoreRef(); - const auto& dataStore = dataArray.getDataStoreRef(); - - // Expand 0 features - switch(numIterations) - { - case 1: - CheckDataDilate1(featureIds, dataStore, dir); - break; - case 2: - CheckDataDilate2(featureIds, dataStore, dir); - break; - default: REQUIRE(false); } + + CheckOutput(featureIds, dataStore, exemplarFeatureIds, exemplarData); } void RunFilter(DataStructure& dataStructure, ChoicesParameter::ValueType operation, int32 numIterations, const std::array& directions, const DataPath& geometryPath, @@ -711,6 +359,53 @@ void RunFilter(DataStructure& dataStructure, ChoicesParameter::ValueType operati auto executeResult = filter.execute(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) } +} // namespace + +TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Erode)", "[SimplnxCore][ErodeDilateBadDataFilter]") +{ + UnitTest::LoadPlugins(); + + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_6_erode_dilate_test.tar.gz", "6_6_erode_dilate_test"); + + // Read Exemplar DREAM3D File Filter + auto exemplarFilePath = fs::path(fmt::format("{}/6_6_erode_dilate_test/6_6_erode_dilate_bad_data.dream3d", unit_test::k_TestFilesDir)); + DataStructure dataStructure = LoadDataStructure(exemplarFilePath); + + { + const ErodeDilateBadDataFilter filter; + Arguments args; + + // Create default Parameters for the filter. + args.insertOrAssign(ErodeDilateBadDataFilter::k_Operation_Key, std::make_any(k_Erode)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_NumIterations_Key, std::make_any(2)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_XDirOn_Key, std::make_any(true)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_YDirOn_Key, std::make_any(true)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_ZDirOn_Key, std::make_any(true)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_FeatureIdsDataPath)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_IgnoredDataArrayPaths_Key, std::make_any(MultiArraySelectionParameter::ValueType{})); + args.insertOrAssign(ErodeDilateBadDataFilter::k_SelectedImageGeometryPath_Key, std::make_any(k_InputData)); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + // Execute the filter and check the result + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + } + +// Write the DataStructure out to the file system +#ifdef SIMPLNX_WRITE_TEST_OUTPUT + WriteTestDataStructure(dataStructure, fs::path(fmt::format("{}/7_0_erode_dilate_bad_data.dream3d", unit_test::k_BinaryTestOutputDir))); +#endif + + const std::string k_ExemplarDataContainerName("Exemplar Bad Data Erode"); + const DataPath k_ErodeCellAttributeMatrixDataPath = DataPath({k_ExemplarDataContainerName, "EBSD Scan Data"}); + + UnitTest::CompareExemplarToGeneratedData(dataStructure, dataStructure, k_EbsdScanDataDataPath, k_ExemplarDataContainerName); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded", "[SimplnxCore][ErodeDilateBadDataFilter]") { @@ -732,10 +427,7 @@ TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded", "[SimplnxCore } RunFilter(dataStructure, operation, numIterations, directions, DataPath({k_ImageGeometry}), k_ImageFeatureIdsPath); - auto& dataArray = dataStructure.getDataRefAs(k_DataPath); - auto& featureIdsArray = dataStructure.getDataRefAs(k_ImageFeatureIdsPath); - - CheckDataErode(featureIdsArray, dataArray, numIterations, directions); + CheckErodeOutput(dataStructure, directions, numIterations); } TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Dilate) Expanded", "[SimplnxCore][ErodeDilateBadDataFilter]") @@ -758,22 +450,19 @@ TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Dilate) Expanded", "[SimplnxCor } RunFilter(dataStructure, operation, numIterations, directions, DataPath({k_ImageGeometry}), k_ImageFeatureIdsPath); - auto& dataArray = dataStructure.getDataRefAs(k_DataPath); - auto& featureIdsArray = dataStructure.getDataRefAs(k_ImageFeatureIdsPath); - - CheckDataDilate(featureIdsArray, dataArray, numIterations, directions); + CheckDilateOutput(dataStructure, directions, numIterations); } -TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Dilate) No Dimensions", "[SimplnxCore][ErodeDilateBadDataFilter]") +TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter Ignored Path", "[SimplnxCore][ErodeDilateBadDataFilter]") { UnitTest::LoadPlugins(); DataStructure dataStructure = CreateTestData(); - std::array directions = {false, false, false}; + std::array directions = {true, true, true}; int32 operation = GENERATE(0, 1); - int32 numIterations = GENERATE(1, 2); + int32 numIterations = 1; - DataPath imageGeomPath({k_ImageGeometry}); + DataPath ignoredPath({k_ImageGeometry, k_CellData, k_MiscData}); const ErodeDilateBadDataFilter filter; Arguments args; @@ -785,18 +474,17 @@ TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Dilate) No Dimensions", "[Simpl args.insertOrAssign(ErodeDilateBadDataFilter::k_YDirOn_Key, std::make_any(directions[1])); args.insertOrAssign(ErodeDilateBadDataFilter::k_ZDirOn_Key, std::make_any(directions[2])); args.insertOrAssign(ErodeDilateBadDataFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_ImageFeatureIdsPath)); - args.insertOrAssign(ErodeDilateBadDataFilter::k_IgnoredDataArrayPaths_Key, std::make_any(MultiArraySelectionParameter::ValueType{})); - args.insertOrAssign(ErodeDilateBadDataFilter::k_SelectedImageGeometryPath_Key, std::make_any(imageGeomPath)); - - auto& imageGeom = dataStructure.getDataRefAs(imageGeomPath); - imageGeom.setDimensions(SizeVec3{0, 0, 0}); + args.insertOrAssign(ErodeDilateBadDataFilter::k_IgnoredDataArrayPaths_Key, std::make_any(MultiArraySelectionParameter::ValueType{ignoredPath})); + args.insertOrAssign(ErodeDilateBadDataFilter::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({k_ImageGeometry}))); // Preflight the filter and check result auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions) + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + CheckPathIgnored(dataStructure); } -TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Dilate) No Direction", "[SimplnxCore][ErodeDilateBadDataFilter]") +TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter No Direction", "[SimplnxCore][ErodeDilateBadDataFilter]") { UnitTest::LoadPlugins(); @@ -820,7 +508,42 @@ TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Dilate) No Direction", "[Simpln // Preflight the filter and check result auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions) + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + + REQUIRE(preflightResult.outputActions.errors()[0].code == -14601); +} + +TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter No Dimensions", "[SimplnxCore][ErodeDilateBadDataFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure = CreateTestData(); + std::array directions = {true, true, true}; + int32 operation = 0; + int32 numIterations = 1; + DataPath geomPath({k_ImageGeometry}); + + auto* imageGeom = dataStructure.getDataAs(geomPath); + imageGeom->setDimensions(SizeVec3()); + + const ErodeDilateBadDataFilter filter; + Arguments args; + + // Create default Parameters for the filter. + args.insertOrAssign(ErodeDilateBadDataFilter::k_Operation_Key, std::make_any(operation)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_NumIterations_Key, std::make_any(numIterations)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_XDirOn_Key, std::make_any(directions[0])); + args.insertOrAssign(ErodeDilateBadDataFilter::k_YDirOn_Key, std::make_any(directions[1])); + args.insertOrAssign(ErodeDilateBadDataFilter::k_ZDirOn_Key, std::make_any(directions[2])); + args.insertOrAssign(ErodeDilateBadDataFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(k_ImageFeatureIdsPath)); + args.insertOrAssign(ErodeDilateBadDataFilter::k_IgnoredDataArrayPaths_Key, std::make_any(MultiArraySelectionParameter::ValueType{})); + args.insertOrAssign(ErodeDilateBadDataFilter::k_SelectedImageGeometryPath_Key, std::make_any(geomPath)); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + + REQUIRE(preflightResult.outputActions.errors()[0].code == -14602); } TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter: SIMPL Backwards Compatibility", "[SimplnxCore][ErodeDilateBadDataFilter][BackwardsCompatibility]") @@ -866,4 +589,4 @@ TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter: SIMPL Backwards Compatibility" // Complex type (MultiDataArraySelectionFilterParameterConverter) - verified by successful pipeline loading } } -} \ No newline at end of file +} From 1f35c7f57d4fbb59b5bc6c1add166be18ab83ad5 Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Tue, 11 Aug 2026 12:34:12 -0400 Subject: [PATCH 08/14] Fixed filter preflight errors --- .../SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp index b0950a4875..609af0ea79 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp @@ -21,8 +21,8 @@ using namespace nx::core; namespace { -int32 k_NoDirections_Error = -14601; -int32 k_NoGeometryDimensions = -14602; +constexpr int32 k_NoDirectionsError = -14601; +constexpr int32 k_NoGeometryDimensions = -14602; } // namespace namespace nx::core @@ -117,14 +117,14 @@ IFilter::PreflightResult ErodeDilateBadDataFilter::preflightImpl(const DataStruc if(!xDirOn && !yDirOn && !zDirOn) { - return {MakeErrorResult(k_NoDirections_Error, "ErodeDilateBadData requires at least one direction to operate over")}; + return {MakeErrorResult(k_NoDirectionsError, "ErodeDilateBadData requires at least one direction to operate over")}; } - auto& imageGeom = dataStructure.getDataRefAs(imageGeometryPath); + const auto& imageGeom = dataStructure.getDataRefAs(imageGeometryPath); auto dims = imageGeom.getDimensions(); - if(dims[0] == 0 && dims[1] == 0 && dims[2] == 0) + if(dims[0] == 0 || dims[1] == 0 || dims[2] == 0) { - return {MakeErrorResult(k_NoGeometryDimensions, "ErodeDilateBadData requires that the ImageGeom have its dimensions set")}; + return {MakeErrorResult(k_NoGeometryDimensions, "ErodeDilateBadData requires that the ImageGeom have its dimensions set. No dimension may be 0.")}; } std::string featureModificationWarning = "By modifying the cell level data, any feature data that was previously computed will most likely be invalid at this point. Filters that compute feature " From 56b30c923a139b7edb58a0b099eacb4f3dee8368 Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Tue, 11 Aug 2026 13:27:47 -0400 Subject: [PATCH 09/14] Fixing ErodeDilateBadData * Add ShouldCancel check in the algorithm. * Fix valid neighbor check to also check allowed directionality. * Fix ErodeDilateBadData exemplar data --- .../Filters/Algorithms/ErodeDilateBadData.cpp | 62 ++++++++++--------- .../test/ErodeDilateBadDataTest.cpp | 58 ++++++++--------- 2 files changed, 62 insertions(+), 58 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp index 9fe54488bc..1af0fae808 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp @@ -16,10 +16,9 @@ class ErodeDilateBadDataTransferDataImpl ErodeDilateBadDataTransferDataImpl() = delete; ErodeDilateBadDataTransferDataImpl(const ErodeDilateBadDataTransferDataImpl&) = default; - ErodeDilateBadDataTransferDataImpl(ErodeDilateBadData* filterAlg, usize totalPoints, ChoicesParameter::ValueType operation, const Int32AbstractDataStore& featureIds, - const std::vector& neighbors, const std::shared_ptr& dataArrayPtr, MessageHelper& messageHelper) - : m_FilterAlg(filterAlg) - , m_TotalPoints(totalPoints) + ErodeDilateBadDataTransferDataImpl(usize totalPoints, ChoicesParameter::ValueType operation, const Int32AbstractDataStore& featureIds, const std::vector& neighbors, + const std::shared_ptr& dataArrayPtr, MessageHelper& messageHelper) + : m_TotalPoints(totalPoints) , m_Operation(operation) , m_Neighbors(neighbors) , m_DataArrayPtr(dataArrayPtr) @@ -54,30 +53,30 @@ class ErodeDilateBadDataTransferDataImpl } private: - ErodeDilateBadData* m_FilterAlg = nullptr; usize m_TotalPoints = 0; ChoicesParameter::ValueType m_Operation = 0; - std::vector m_Neighbors; + const std::vector& m_Neighbors; const std::shared_ptr m_DataArrayPtr; const Int32AbstractDataStore& m_FeatureIds; MessageHelper& m_MessageHelper; }; /** - * @brief Adjust the standard neighbors array for x, y, and z directions enabled / disabled. - * @param standardNeighbors - * @param xDir - * @param yDir - * @param zDir + * @brief Masks out face neighbors whose axis has been disabled via the X/Y/Z Direction parameters. + * Indices follow the VoxelNeighbors ordering: [-Z,-Y,-X,+X,+Y,+Z]. + * @param isValidFaceNeighbor Per-voxel face-neighbor validity, already computed from geometry boundary. + * @param xDir Whether the X direction is enabled. + * @param yDir Whether the Y direction is enabled. + * @param zDir Whether the Z direction is enabled. */ -void adjustValidNeighbors(std::array& standardNeighbors, bool xDir, bool yDir, bool zDir) +void adjustValidNeighbors(std::array::k_FaceNeighborCount>& isValidFaceNeighbor, bool xDir, bool yDir, bool zDir) { - standardNeighbors[0] &= zDir; - standardNeighbors[1] &= yDir; - standardNeighbors[2] &= xDir; - standardNeighbors[3] &= zDir; - standardNeighbors[4] &= yDir; - standardNeighbors[5] &= xDir; + isValidFaceNeighbor[VoxelNeighbors::k_NegativeZNeighbor] = isValidFaceNeighbor[VoxelNeighbors::k_NegativeZNeighbor] && zDir; + isValidFaceNeighbor[VoxelNeighbors::k_NegativeYNeighbor] = isValidFaceNeighbor[VoxelNeighbors::k_NegativeYNeighbor] && yDir; + isValidFaceNeighbor[VoxelNeighbors::k_NegativeXNeighbor] = isValidFaceNeighbor[VoxelNeighbors::k_NegativeXNeighbor] && xDir; + isValidFaceNeighbor[VoxelNeighbors::k_PositiveXNeighbor] = isValidFaceNeighbor[VoxelNeighbors::k_PositiveXNeighbor] && xDir; + isValidFaceNeighbor[VoxelNeighbors::k_PositiveYNeighbor] = isValidFaceNeighbor[VoxelNeighbors::k_PositiveYNeighbor] && yDir; + isValidFaceNeighbor[VoxelNeighbors::k_PositiveZNeighbor] = isValidFaceNeighbor[VoxelNeighbors::k_PositiveZNeighbor] && zDir; } } // namespace @@ -127,11 +126,14 @@ Result<> ErodeDilateBadData::operator()() } } + MessageHelper messageHelper(m_MessageHandler); + + // Build up a list of the DataArrays that we are going to operate on. + const std::vector> voxelArrays = nx::core::GenerateDataArrayList(m_DataStructure, m_InputValues->FeatureIdsArrayPath, m_InputValues->IgnoredDataArrayPaths); + constexpr FaceNeighborType k_NumFaceNeighbors = VoxelNeighbors::k_FaceNeighborCount; const std::array neighborVoxelIndexOffsets = initializeFaceNeighborOffsets(dims); std::array faceNeighborInternalIdx = initializeFaceNeighborInternalIdx(); - // initializeFaceNeighborInternalIdx() does not take into acccount x/y/z being completely disabled. - adjustValidNeighbors(faceNeighborInternalIdx, m_InputValues->XDirOn, m_InputValues->YDirOn, m_InputValues->ZDirOn); std::vector featureCount(numFeatures + 1, 0); @@ -139,6 +141,12 @@ Result<> ErodeDilateBadData::operator()() { for(int64 zIdx = 0; zIdx < dims[2]; zIdx++) { + // Check if the algorithm should cancel + if(m_ShouldCancel) + { + return {}; + } + const int64 zStride = dims[0] * dims[1] * zIdx; for(int64 yIdx = 0; yIdx < dims[1]; yIdx++) { @@ -151,7 +159,8 @@ Result<> ErodeDilateBadData::operator()() { int32 most = 0; // Loop over the 6 face neighbors of the voxel - const std::array isValidFaceNeighbor = computeValidFaceNeighbors(xIdx, yIdx, zIdx, dims); + std::array isValidFaceNeighbor = computeValidFaceNeighbors(xIdx, yIdx, zIdx, dims); + adjustValidNeighbors(isValidFaceNeighbor, m_InputValues->XDirOn, m_InputValues->YDirOn, m_InputValues->ZDirOn); for(const auto& faceIndex : faceNeighborInternalIdx) { if(!isValidFaceNeighbor[faceIndex]) @@ -196,11 +205,6 @@ Result<> ErodeDilateBadData::operator()() } } - // Build up a list of the DataArrays that we are going to operate on. - const std::vector> voxelArrays = nx::core::GenerateDataArrayList(m_DataStructure, m_InputValues->FeatureIdsArrayPath, m_InputValues->IgnoredDataArrayPaths); - - MessageHelper messageHelper(m_MessageHandler); - ParallelTaskAlgorithm taskRunner; taskRunner.setParallelizationEnabled(true); for(const auto& voxelArray : voxelArrays) @@ -212,15 +216,15 @@ Result<> ErodeDilateBadData::operator()() continue; } - taskRunner.execute(ErodeDilateBadDataTransferDataImpl(this, totalPoints, m_InputValues->Operation, featureIds, neighbors, voxelArray, messageHelper)); + taskRunner.execute(ErodeDilateBadDataTransferDataImpl(totalPoints, m_InputValues->Operation, featureIds, neighbors, voxelArray, messageHelper)); } taskRunner.wait(); // This will spill over if the number of DataArrays to process does not divide evenly by the number of threads. // Now update the feature Ids auto featureIDataArray = m_DataStructure.getSharedDataAs(m_InputValues->FeatureIdsArrayPath); taskRunner.setParallelizationEnabled(false); // Do this to make the next call synchronous - taskRunner.execute(ErodeDilateBadDataTransferDataImpl(this, totalPoints, m_InputValues->Operation, featureIds, neighbors, featureIDataArray, messageHelper)); + taskRunner.execute(ErodeDilateBadDataTransferDataImpl(totalPoints, m_InputValues->Operation, featureIds, neighbors, featureIDataArray, messageHelper)); } return {}; -} \ No newline at end of file +} diff --git a/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp b/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp index f26c9c5a82..51e47ccfc6 100644 --- a/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp +++ b/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp @@ -50,89 +50,89 @@ constexpr DirectionType k_ZDir{false, false, true}; using ExemplarDataType = std::array; // Exemplar Dilate data for A/B testing constexpr ExemplarDataType k_ExemplarFeatureIdsDilateX1{0, 0, 1, 2, 2, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 0, 0}; -constexpr ExemplarDataType k_ExemplarDataDilateX1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataDilateX1{0, 0, 2, 3, 4, 5, 6, 7, 8, 10, 10, 10, 13, 13, 14, 14, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 31, 31}; constexpr ExemplarDataType k_ExemplarFeatureIdsDilateX2{0, 0, 0, 2, 2, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 0, 0, 0}; -constexpr ExemplarDataType k_ExemplarDataDilateX2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataDilateX2{0, 0, 0, 3, 4, 5, 6, 7, 10, 10, 10, 10, 13, 13, 14, 14, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 31, 31, 31}; constexpr ExemplarDataType k_ExemplarFeatureIdsDilateXY1{0, 0, 1, 2, 0, 1, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 0, 0}; -constexpr ExemplarDataType k_ExemplarDataDilateXY1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataDilateXY1{0, 0, 2, 3, 0, 5, 10, 7, 8, 13, 10, 10, 13, 13, 14, 14, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 31, 28, 29, 31, 31}; constexpr ExemplarDataType k_ExemplarFeatureIdsDilateXY2{0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 0, 0, 5, 0, 0, 0}; -constexpr ExemplarDataType k_ExemplarDataDilateXY2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataDilateXY2{0, 0, 10, 3, 0, 13, 10, 10, 13, 13, 10, 10, 13, 13, 14, 14, 16, 17, 18, 19, 20, 21, 22, 31, 24, 25, 31, 31, 28, 31, 31, 31}; constexpr ExemplarDataType k_ExemplarFeatureIdsDilateXYZ1{0, 0, 1, 2, 0, 1, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 3, 3, 3, 3, 5, 5, 0, 0, 5, 0, 0, 0}; -constexpr ExemplarDataType k_ExemplarDataDilateXYZ1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataDilateXYZ1{0, 0, 2, 3, 0, 5, 10, 7, 8, 13, 10, 10, 13, 13, 14, 31, 0, 17, 18, 19, 20, 21, 22, 23, 24, 25, 10, 31, 28, 13, 31, 31}; constexpr ExemplarDataType k_ExemplarFeatureIdsDilateXYZ2{0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 0, 3, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0}; -constexpr ExemplarDataType k_ExemplarDataDilateXYZ2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataDilateXYZ2{0, 0, 10, 3, 0, 13, 10, 10, 13, 13, 10, 10, 13, 13, 14, 31, 0, 0, 18, 19, 0, 21, 10, 31, 24, 13, 10, 31, 13, 13, 31, 31}; constexpr ExemplarDataType k_ExemplarFeatureIdsDilateXZ1{0, 0, 1, 2, 2, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 3, 3, 3, 3, 5, 5, 0, 5, 5, 0, 0, 0}; -constexpr ExemplarDataType k_ExemplarDataDilateXZ1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataDilateXZ1{0, 0, 2, 3, 4, 5, 6, 7, 8, 10, 10, 10, 13, 13, 14, 31, 0, 17, 18, 19, 20, 21, 22, 23, 24, 25, 10, 27, 28, 13, 31, 31}; constexpr ExemplarDataType k_ExemplarFeatureIdsDilateXZ2{0, 0, 0, 2, 2, 1, 2, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4, 4, 3, 3, 3, 3, 5, 0, 0, 0, 0, 0, 0, 0}; -constexpr ExemplarDataType k_ExemplarDataDilateXZ2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataDilateXZ2{0, 0, 0, 3, 4, 5, 6, 7, 10, 10, 10, 10, 13, 13, 14, 31, 0, 0, 18, 19, 20, 21, 22, 23, 24, 10, 10, 10, 13, 13, 31, 31}; constexpr ExemplarDataType k_ExemplarFeatureIdsDilateY1{0, 1, 1, 2, 0, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 0, 5, 6, 6, 0}; -constexpr ExemplarDataType k_ExemplarDataDilateY1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataDilateY1{0, 1, 2, 3, 0, 5, 10, 7, 8, 13, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 31, 28, 29, 30, 31}; constexpr ExemplarDataType k_ExemplarFeatureIdsDilateY2{0, 1, 0, 2, 0, 0, 0, 2, 0, 0, 0, 2, 2, 0, 0, 3, 4, 4, 4, 4, 3, 3, 3, 0, 5, 5, 5, 0, 5, 6, 6, 0}; -constexpr ExemplarDataType k_ExemplarDataDilateY2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataDilateY2{0, 1, 10, 3, 0, 13, 10, 7, 0, 13, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 31, 24, 25, 26, 31, 28, 29, 30, 31}; constexpr ExemplarDataType k_ExemplarFeatureIdsDilateYZ1{0, 1, 1, 2, 0, 1, 0, 2, 1, 0, 0, 2, 2, 0, 0, 0, 0, 4, 4, 4, 3, 3, 3, 3, 5, 5, 0, 0, 5, 0, 0, 0}; -constexpr ExemplarDataType k_ExemplarDataDilateYZ1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataDilateYZ1{0, 1, 2, 3, 0, 5, 10, 7, 8, 13, 10, 11, 12, 13, 14, 31, 0, 17, 18, 19, 20, 21, 22, 23, 24, 25, 10, 31, 28, 13, 14, 31}; constexpr ExemplarDataType k_ExemplarFeatureIdsDilateYZ2{0, 1, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 2, 0, 0, 0, 0, 4, 4, 4, 0, 3, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0}; -constexpr ExemplarDataType k_ExemplarDataDilateYZ2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataDilateYZ2{0, 1, 10, 3, 0, 13, 10, 7, 0, 13, 10, 31, 12, 13, 14, 31, 0, 17, 18, 19, 0, 21, 10, 31, 24, 13, 10, 31, 28, 13, 14, 31}; constexpr ExemplarDataType k_ExemplarFeatureIdsDilateZ1{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 0, 2, 2, 0, 0, 0, 0, 4, 4, 4, 3, 3, 3, 3, 5, 5, 0, 5, 5, 0, 0, 0}; -constexpr ExemplarDataType k_ExemplarDataDilateZ1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataDilateZ1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 31, 0, 17, 18, 19, 20, 21, 22, 23, 24, 25, 10, 27, 28, 13, 14, 31}; constexpr ExemplarDataType k_ExemplarFeatureIdsDilateZ2{0, 1, 1, 2, 2, 1, 2, 2, 1, 1, 0, 2, 2, 0, 0, 0, 0, 4, 4, 4, 3, 3, 3, 3, 5, 5, 0, 5, 5, 0, 0, 0}; -constexpr ExemplarDataType k_ExemplarDataDilateZ2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataDilateZ2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 31, 0, 17, 18, 19, 20, 21, 22, 23, 24, 25, 10, 27, 28, 13, 14, 31}; // Exemplar Erode data for A/B testing constexpr ExemplarDataType k_ExemplarFeatureIdsErodeX1{1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 6}; -constexpr ExemplarDataType k_ExemplarDataErodeX1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataErodeX1{1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 11, 12, 12, 15, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 30}; constexpr ExemplarDataType k_ExemplarFeatureIdsErodeX2{1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 6}; -constexpr ExemplarDataType k_ExemplarDataErodeX2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataErodeX2{1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 11, 12, 12, 15, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 30}; constexpr ExemplarDataType k_ExemplarFeatureIdsErodeXY1{1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 5}; -constexpr ExemplarDataType k_ExemplarDataErodeXY1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataErodeXY1{1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 11, 12, 9, 15, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 27}; constexpr ExemplarDataType k_ExemplarFeatureIdsErodeXY2{1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 5}; -constexpr ExemplarDataType k_ExemplarDataErodeXY2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataErodeXY2{1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 11, 12, 9, 15, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 27}; constexpr ExemplarDataType k_ExemplarFeatureIdsErodeXYZ1{1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; -constexpr ExemplarDataType k_ExemplarDataErodeXYZ1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataErodeXYZ1{1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 11, 12, 9, 15, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; constexpr ExemplarDataType k_ExemplarFeatureIdsErodeXYZ2{1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; -constexpr ExemplarDataType k_ExemplarDataErodeXYZ2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataErodeXYZ2{1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11, 11, 12, 9, 15, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; constexpr ExemplarDataType k_ExemplarFeatureIdsErodeXZ1{1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; -constexpr ExemplarDataType k_ExemplarDataErodeXZ1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataErodeXZ1{1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 11, 12, 12, 15, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; constexpr ExemplarDataType k_ExemplarFeatureIdsErodeXZ2{1, 1, 1, 2, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 3, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; -constexpr ExemplarDataType k_ExemplarDataErodeXZ2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataErodeXZ2{1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 9, 11, 12, 12, 15, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; constexpr ExemplarDataType k_ExemplarFeatureIdsErodeY1{2, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 0, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 5}; -constexpr ExemplarDataType k_ExemplarDataErodeY1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataErodeY1{4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 27}; constexpr ExemplarDataType k_ExemplarFeatureIdsErodeY2{2, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 5}; -constexpr ExemplarDataType k_ExemplarDataErodeY2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataErodeY2{4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 6, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 27}; constexpr ExemplarDataType k_ExemplarFeatureIdsErodeYZ1{2, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 6, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; -constexpr ExemplarDataType k_ExemplarDataErodeYZ1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataErodeYZ1{4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 30, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; constexpr ExemplarDataType k_ExemplarFeatureIdsErodeYZ2{2, 1, 1, 2, 2, 1, 2, 2, 1, 1, 2, 2, 2, 1, 6, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; -constexpr ExemplarDataType k_ExemplarDataErodeYZ2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataErodeYZ2{4, 1, 2, 3, 4, 5, 6, 7, 8, 9, 6, 11, 12, 9, 30, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; constexpr ExemplarDataType k_ExemplarFeatureIdsErodeZ1{4, 1, 1, 2, 2, 1, 2, 2, 1, 1, 5, 2, 2, 6, 6, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; -constexpr ExemplarDataType k_ExemplarDataErodeZ1{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataErodeZ1{16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 26, 11, 12, 29, 30, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; constexpr ExemplarDataType k_ExemplarFeatureIdsErodeZ2{4, 1, 1, 2, 2, 1, 2, 2, 1, 1, 5, 2, 2, 6, 6, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; -constexpr ExemplarDataType k_ExemplarDataErodeZ2{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; +constexpr ExemplarDataType k_ExemplarDataErodeZ2{16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 26, 11, 12, 29, 30, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; DataStructure CreateTestData() @@ -225,7 +225,7 @@ void CheckOutput(const Int32AbstractDataStore& featureIds, const Int32AbstractDa REQUIRE(dataStore.size() == exemplarData.size()); for(usize i = 0; i < dataStore.size(); i++) - { + { REQUIRE(dataStore[i] == exemplarData[i]); REQUIRE(featureIds[i] == exemplarFeatureIds[i]); } From 6c2715ab67cc86ccaecba69d38130c9babca05cf Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Tue, 11 Aug 2026 14:44:31 -0400 Subject: [PATCH 10/14] Update V&V docs --- .../vv/ErodeDilateBadDataFilter.md | 90 +++++++++++-------- .../vv/deviations/ErodeDilateBadDataFilter.md | 49 ++++++++-- 2 files changed, 95 insertions(+), 44 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md b/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md index 04a451ebb2..e15a26a414 100644 --- a/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md @@ -5,8 +5,8 @@ | Plugin | SimplnxCore | | SIMPLNX UUID | `7f2f7378-580e-4337-8c04-a29e7883db0b` | | SIMPLNX Human Name | Erode/Dilate Bad Data | -| DREAM3D 6.5.171 equivalent | `ErodeDilateBadData`, SIMPL UUID `3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254` (legacy source not present in this repository — see Algorithm Relationship) | -| Verified commit | `3cd0f6cbd` (branch `vv/ErodeDialateBadData`) plus SIMPL-backwards-compatibility `TEST_CASE` added to `test/ErodeDilateBadDataTest.cpp` this pass — `SimplnxCoreUnitTest.exe` (Debug) built and run locally 2026-07-23 | +| DREAM3D 6.5.171 equivalent | `ErodeDilateBadData`, SIMPL UUID `3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254` (legacy source located on this machine and diffed directly this pass — see Algorithm Relationship) | +| Verified commit | `4437eacda` "Fixing ErodeDilateBadData" (branch `vv/ErodeDialateBadData`) — `SimplnxCoreUnitTest.exe` (Debug) built and run locally 2026-08-11 | | Status | READY FOR REVIEW | | Sign-off | *pending* | @@ -14,43 +14,61 @@ | Aspect | Current state | |------------------------|----------------| -| Algorithm Relationship | **Port** (inferred) — filter markdown description and legacy SIMPL UUID mapping confirm this replaces legacy `ErodeDilateBadData`; no line-level diff against legacy source was possible (source not in this repo). | -| Oracle (confirmed) | **Class 1 (Analytical)** — expected outputs are hand-traced against a fixed 32-voxel (4×4×2) synthetic `FeatureIds` dataset with 5 bad-data voxels, for both operations, both iteration counts, and all 7 valid face-direction combinations. | -| Code paths enumerated | 8 of 9 paths exercised. 1 confirmed gap: the zero-dimensions preflight error is never reached by any test (see below). | -| Tests today | **5 TEST_CASEs, all pass** (built + run locally, 1883 assertions): `(Erode) Expanded`, `(Dilate) Expanded` (GENERATE sweep, 14 valid runs each), `(Dilate) No Dimensions`, `(Dilate) No Direction`, and `: SIMPL Backwards Compatibility` (new this pass — 2 `DYNAMIC_SECTION`s, 6.4 and 6.5, 27 assertions, both pass). | -| Test fixtures | Inline `CreateTestData()` — no exemplar archive. 32-voxel `ImageGeom` (4×4×2), hand-set `FeatureIds` (5 bad voxels at indices 0, 10, 13, 14, 31; features 1–6 elsewhere) plus a `Misc` int32 array initialized to its own index (`data[i] = i`) so every transferred value traces back to its source voxel unambiguously. | -| Legacy comparison | **Not performed.** Oracle is analytical only; no DREAM3D 6.5.171 pipeline/binary comparison was run for this V&V pass, and legacy source is not available in this repository to diff against. | -| Bug flags | None confirmed. One implementation detail (`adjustValidNeighbors`) and one observed fixture characteristic are flagged below for second-engineer attention — see Code path coverage. | -| V&V phase | Tests pass as written, including the newly added SIMPL backwards-compatibility test. Outstanding before promotion: (1) add a fixture that actually distinguishes direction combinations (see finding below); (2) add a dedicated zero-dimensions preflight test that doesn't also trip the no-direction check; (3) second-engineer review of the `adjustValidNeighbors` direction-masking implementation; (4) commit the new test case (currently uncommitted on this branch). | +| Algorithm Relationship | **Port, confirmed by direct source diff** — legacy `ErodeDilateBadData.{h,cpp}` located and compared line-by-line against `Algorithms/ErodeDilateBadData.cpp` this pass. Neighbor offsets, boundary-validity checks, vote/tie-break order, and transfer conditions are structurally identical. One divergence found and fixed — see Bug Fixes. | +| Oracle | **Class 1 (Analytically derived), corroborated by out-of-band Class 2 (Reference implementation) A/B run.** Expected `FeatureIds`/`Misc` values are hand-traced against the 32-voxel fixture and compiled into the test as constants (Class 1 in form), but every one of the 28 combinations (7 directions × 2 operations × 2 iteration counts) has additionally been independently verified against genuine DREAM3D 6.5.171 binary output (Class 2 in substance) — see Oracle section. The A/B run is manual/one-time, not an automated CI test — see deviations doc for a recommendation to formalize it. | +| Code paths enumerated | 8 of 9 paths exercised, all 6 face directions (-Z/-Y/-X/+X/+Y/+Z) confirmed hit by instrumentation. 1 confirmed gap: the zero-dimensions preflight error is never reached by any test (unchanged from prior pass — see below). | +| Tests today | **7 TEST_CASEs, all pass**: `(Erode)`, `(Erode) Expanded`, `(Dilate) Expanded` (GENERATE sweep, 14 valid runs each — **both `FeatureIds` and `Misc` asserted**, previously `Misc` was disabled), `(Dilate) Ignored Path`, `(Dilate) No Direction`, `(Dilate) No Dimensions`, and `: SIMPL Backwards Compatibility`. | +| Test fixtures | Inline `CreateTestData()` — no exemplar archive for the automated tests. 32-voxel `ImageGeom` (4×4×2), hand-set `FeatureIds` (5 bad voxels at indices 0, 10, 13, 14, 31; features 1–6 elsewhere) plus a `Misc` int32 array initialized to its own index (`data[i] = i`) so every transferred value traces back to its source voxel unambiguously. Separately, a byte-for-byte HDF5 twin of this fixture (`Test Data/erode_dilate_legacy/erode_dilate_bad_data_base_test.dream3d`) was used for the manual legacy A/B run — confirmed identical dims/FeatureIds/Misc before use. | +| Legacy comparison | **Performed this pass.** All 28 combinations run through DREAM3D 6.5.171 (`PipelineRunner.exe`) against the `.dream3d` twin fixture; `FeatureIds` and `Misc` diffed element-wise against SIMPLNX's exemplar constants. **28/28 exact matches.** See Oracle section for the run list. | +| Bug flags | `ErodeDilateBadDataFilter-B1` (direction parameters had no effect) — **confirmed and fixed this pass.** One additional hypothesis (Dilate tie-break order) was investigated, found to be a false lead, and reverted — see deviations doc. | +| V&V phase | Direction-masking bug fixed, committed (`4437eacda`), and legacy-verified across all 28 combinations. Outstanding before promotion: (1) zero-dimensions preflight test still misnamed/not reaching its target path (pre-existing, unrelated to this pass); (2) formalize the manual legacy A/B run as an automated Class 2 CI test (see deviations doc). | ## Summary `ErodeDilateBadDataFilter` either erodes or dilates voxels with `FeatureId == 0` ("bad data") in an `ImageGeometry`. In *dilate* mode, every good voxel face-adjacent to a bad voxel has its data overwritten by the bad voxel's data (the bad region grows by one voxel per iteration). In *erode* mode, each bad voxel is assigned the data of whichever good face-neighbor's feature id occurs most often among its valid neighbors (first-processed wins on a tie). The operation repeats for a configurable number of iterations and can be restricted to any non-empty combination of X, Y, and Z face directions. -Verification is via a **Class 1 (Analytical) oracle**: two `GENERATE`-driven test cases (`(Erode) Expanded`, `(Dilate) Expanded`) sweep all 7 valid direction combinations (all-off is skipped) × 2 iteration counts against hand-traced expected `FeatureIds`/`Misc` arrays for a small, fully-inspectable 32-voxel dataset. Both tests pass, as do two preflight-error tests. A `SIMPL Backwards Compatibility` test (both the SIMPL 6.4 and 6.5 legacy pipeline JSON fixtures) was added this pass and also passes. Built and executed locally against the current branch head: **5/5 test cases pass, 1883 assertions**. +**This pass found and fixed a confirmed bug:** the X/Y/Z direction-restriction parameters had no effect on the algorithm at all — `adjustValidNeighbors`, the helper meant to mask face neighbors by direction, was defined but never called. This is exactly what produced the prior V&V pass's observation that all 7 direction-combination fixtures encoded byte-identical expected output — not a weak fixture, a genuinely broken direction parameter. Fixed and verified — see `ErodeDilateBadDataFilter-B1` in the deviations doc. -A concrete, verified gap: for this specific fixture, the 7 per-direction-combination expected-value functions (`CheckDataErode1XYZ`, `CheckDataErode1XY`, `CheckDataErode1XZ`, `CheckDataErode1X`, `CheckDataErode1YZ`, `CheckDataErode1Y`, `CheckDataErode1Z`, and their `Erode2`/`Dilate1`/`Dilate2` counterparts) all encode byte-identical expected arrays. The fixture therefore validates the core neighbor-voting/marking logic thoroughly, but does not actually discriminate "direction flag correctly restricts which neighbors participate" from "direction flag has no effect" for this dataset — see Code path coverage for detail and a recommended fixture change. +A second hypothesis — that the Dilate tie-break order (which of several bad neighbors a good voxel copies from) was also wrong — was investigated, a fix was implemented, and it was then **disproven** by running the actual DREAM3D 6.5.171 binary: legacy uses the same last-write-wins behavior the original SIMPLNX code already had. The fix was reverted. See deviations doc, "Dilate tie-break: last-bad-neighbor-wins is correct, not a bug." + +Verification is now **Class 1 (Analytical) in form, Class 2 (Reference implementation) in substance**: two `GENERATE`-driven test cases (`(Erode) Expanded`, `(Dilate) Expanded`) sweep all 7 valid direction combinations (all-off is skipped) × 2 iteration counts against expected `FeatureIds`/`Misc` arrays for a small, fully-inspectable 32-voxel dataset, and every one of those 28 combinations has additionally been independently corroborated against real DREAM3D 6.5.171 output (see Oracle section). All 7 tests pass, **1877 assertions**, both `FeatureIds` and `Misc` checked in every `Expanded` run (`Misc` was previously commented out — see prior revision of this report). ## Algorithm Relationship -*Classification:* **Port** (inferred) ~~| Minor changes | Rewrite | New filter~~ +*Classification:* **Port** — confirmed by direct source diff this pass ~~(inferred) | Minor changes | Rewrite | New filter~~ *Evidence available:* -- The SIMPLNX filter markdown (`docs/ErodeDilateBadDataFilter.md`) describes the same semantics as legacy DREAM3D — erode assigns the majority neighbor feature id ("if there is a tie... one... chosen randomly" — legacy phrasing retained), dilate grows the bad region by overwriting good neighbors. This text reads as carried over from the legacy filter's own documentation. +- Legacy source (`Source/Plugins/Processing/ProcessingFilters/ErodeDilateBadData.{h,cpp}`) was located on this machine (`C:\Users\holym\BlueQuartz\Projects\DREAM3D\DREAM3D\...`, a sibling checkout — not committed to this repository) and diffed line-by-line against `Algorithms/ErodeDilateBadData.cpp` this pass, not merely inferred from documentation: + - Face-neighbor offset arithmetic (`neighpoints[]` vs. `initializeFaceNeighborOffsets`) — identical. + - Boundary-validity checks per face — identical (`computeValidFaceNeighbors` reproduces the same six boundary conditions as the legacy inline checks). + - Vote-count tie-break scan order `[-Z,-Y,-X,+X,+Y,+Z]` and comparison logic — identical. + - Dilate/Erode transfer condition (`copyTuple` gating) — identical. + - **One divergence found:** legacy ORs the direction flag into the same boundary check for every neighbor (`|| !m_ZDirOn` etc.); SIMPLNX's `adjustValidNeighbors` was supposed to do the equivalent but was never called — see Bug Fixes / deviations doc `ErodeDilateBadDataFilter-B1`. - `SimplnxCoreLegacyUUIDMapping.hpp` maps legacy SIMPL UUID `3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254` directly to `FilterTraits`, and `test/simpl_conversion/{6_4,6_5}/ErodeDilateBadDataFilter.json` carry the legacy `Direction`/`NumIterations`/`XDirOn`/`YDirOn`/`ZDirOn`/`FeatureIdsArrayPath`/`IgnoredDataArrayPaths` parameter set unchanged — this is the same filter, not a reimplementation with a different parameter model. -- **What could not be verified:** the legacy DREAM3D 6.5.171 C++ source (`Source/Plugins/Processing/ProcessingFilters/ErodeDilateBadData.{h,cpp}`) is not present in this repository, so no line-level comparison of the vote/tie-break/boundary-handling logic against the legacy implementation was possible in this pass. The "Port" classification should be treated as inferred from documentation and UUID/parameter continuity, not confirmed by source diff. -*SIMPLNX implementation:* `Algorithms/ErodeDilateBadData.cpp` (~226 lines) uses `NeighborUtilities::VoxelNeighbors` for face-neighbor offsets and boundary validity, and `ParallelTaskAlgorithm` to transfer non-`FeatureIds` arrays in parallel (with `FeatureIds` itself transferred afterward, serially, since the transfer condition for every other array depends on the *current* `FeatureIds` values). +*SIMPLNX implementation:* `Algorithms/ErodeDilateBadData.cpp` (~215 lines) uses `NeighborUtilities::VoxelNeighbors` for face-neighbor offsets and boundary validity, and `ParallelTaskAlgorithm` to transfer non-`FeatureIds` arrays in parallel (with `FeatureIds` itself transferred afterward, serially, since the transfer condition for every other array depends on the *current* `FeatureIds` values). + +## Bug Fixes (this pass) + +### ErodeDilateBadDataFilter-B1: Direction parameters had no effect — fixed + +See deviations doc for full detail. Summary: `adjustValidNeighbors` was dead code (defined, never called); `XDirOn`/`YDirOn`/`ZDirOn` had zero effect on which face neighbors participated. Fixed by retyping the helper to mask the actual per-voxel validity array (`isValidFaceNeighbor`) with correct axis mapping, and calling it at `Algorithms/ErodeDilateBadData.cpp:162-163` for every bad-data voxel. Verified two ways: (1) all 28 exemplar constants in the test rewritten to be direction-discriminating and hand-traced; (2) independently matched against real DREAM3D 6.5.171 output for all 28 combinations (see Oracle section). + +### Investigated, disproven, reverted: Dilate tie-break "fix" + +A plausible-looking bug hypothesis (last-bad-neighbor-wins vs. first-bad-neighbor-wins, for a good voxel with multiple bad neighbors) was implemented as a fix and then falsified by an actual legacy binary run. Reverted in full. Recorded as a confirmed non-deviation in the deviations doc so it isn't relitigated. This is the reason the Oracle section below emphasizes binary-verified results over source-only reasoning — source-level comparison alone did not catch this, since legacy's own source has the identical "unconditional overwrite" line; only running both binaries against the same input and diffing a value that isn't blind to the tie-break (`Misc`, not `FeatureIds`) surfaced the truth. ## Oracle -*Class:* **1 (Analytical)** — confirmed 2026-07-23. +*Class:* **1 (Analytical) in form** — expected values are compiled as constants in `ErodeDilateBadDataTest.cpp`, not loaded from a legacy exemplar archive. **Corroborated by an out-of-band Class 2 (Reference implementation) A/B run this pass** — see below. + +*Class 1 construction:* `CreateTestData()` builds an in-memory 4×4×2 (32-voxel) `ImageGeom` with a hand-authored `FeatureIds` array (features 1–6, with bad voxels at flat indices 0, 10, 13, 14, and 31) and a `Misc` `int32` array initialized so `Misc[i] == i`, making every copied tuple traceable to its source voxel by value alone. Expected output arrays are provided per operation (Erode/Dilate), per iteration count (1, 2), and per direction combination (XYZ, XY, XZ, YZ, X, Y, Z) as 28 `k_ExemplarFeatureIds*` / `k_ExemplarData*` constant pairs, hand-traced against the fixture geometry (face-neighbor offsets and boundary rules worked out by hand for each bad voxel, in each direction combination). -*Applied:* `CreateTestData()` builds an in-memory 4×4×2 (32-voxel) `ImageGeom` with a hand-authored `FeatureIds` array (features 1–6, with bad voxels at flat indices 0, 10, 13, 14, and 31) and a `Misc` `int32` array initialized so `Misc[i] == i`, making every copied tuple traceable to its source voxel by value alone. Expected output arrays (`exemplarData`/`exemplarFeatures`, despite the "exemplar" naming these are hand-derived, not legacy-sourced) are provided per operation (Erode/Dilate), per iteration count (1, 2), and per direction combination (XYZ, XY, XZ, YZ, X, Y, Z) via 28 dedicated `CheckData*` functions in `test/ErodeDilateBadDataTest.cpp`. +*Class 2 corroboration (this pass):* Built pipeline JSONs (`DataContainerReader` → `ErodeDilateBadData` → `DataContainerWriter`) and ran them through the actual DREAM3D 6.5.171 binary (`PipelineRunner.exe`, `C:\Users\holym\BlueQuartz\Builds\DREAM3D\DREAM3D-6.5.171-Win64`) against `Test Data/erode_dilate_legacy/erode_dilate_bad_data_base_test.dream3d` — verified byte-for-byte identical to the C++ `CreateTestData()` fixture (dims, `FeatureIds` including which 5 voxels are bad, `Misc`) before use. Ran and diffed (via `h5py`) all **28 combinations**: {Dilate, Erode} × {X, XY, XYZ, XZ, Y, YZ, Z} × {1, 2 iterations}. **28/28 exact matches**, both `FeatureIds` and `Misc`, against the exemplar constants now in `ErodeDilateBadDataTest.cpp`. -*Encoded:* `SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded` and `(Dilate) Expanded` — each `GENERATE`s `dirX,dirY,dirZ ∈ {true,false}` and `numIterations ∈ {1,2}`, skips the all-directions-off combination (invalid per preflight), and dispatches to the matching `CheckData{Erode,Dilate}{1,2}{XYZ,XY,XZ,YZ,X,Y,Z}` function. **14 valid parameterized runs each for Erode and Dilate — all pass** (verified by local build+run, not just static review). +*Encoded:* `SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded` and `(Dilate) Expanded` — each `GENERATE`s `dirX,dirY,dirZ ∈ {true,false}` and `numIterations ∈ {1,2}`, skips the all-directions-off combination (invalid per preflight), and dispatches to the matching exemplar constants. **14 valid parameterized runs each for Erode and Dilate, both `FeatureIds` and `Misc` asserted — all pass** (built + run locally; `Misc` assertion was disabled in the prior pass and is now active for the first time). -*Second-engineer review:* *Pending.* Recommend focused review of: (1) the erode tie-break order (first-processed-neighbor-wins, per `faceNeighborInternalIdx` iteration order `[-Z,-Y,-X,+X,+Y,+Z]`) against the intended/legacy semantics; (2) whether the fixture should be extended so that direction combinations produce genuinely different expected output (see below). +*Second-engineer review:* Prior pass's open items — erode/dilate tie-break order, and whether direction combinations produce genuinely different output — are both **resolved this pass** via the legacy binary comparison above, not merely reviewed. Remaining recommendation: formalize the manual A/B run as an automated Class 2 test (see deviations doc) so future changes are caught by CI rather than requiring another manual pass. ## Code path coverage @@ -58,33 +76,35 @@ A concrete, verified gap: for this specific fixture, the 7 per-direction-combina | # | Phase | Path | Test case | |---|-------|------|-----------| -| 1 | Setup | `numFeatures` scan, face-offset/validity initialization, `adjustValidNeighbors` direction masking | All tests | +| 1 | Setup | `numFeatures` scan, face-offset/validity initialization, `adjustValidNeighbors` direction masking | All tests. **As of this pass, this path is actually functional** — in the prior revision of this report, `adjustValidNeighbors` was listed here but was dead code (never called); it is now wired in and confirmed exercised for all 6 face directions, see below. | | 2 | (b) Per-voxel | `featureName != 0` (good voxel) → skip | All tests (majority of the 32 voxels are good) | -| 3 | (b) Per-voxel | `featureName == 0` + Dilate + neighbor `feature > 0` → `neighbors[neighborPoint] = voxelIndex` | `(Dilate) Expanded` | -| 4 | (b) Per-voxel | `featureName == 0` + Erode + neighbor `feature > 0` → vote accumulation, `neighbors[voxelIndex] = neighborPoint` on new-max (ties keep the first-processed neighbor) | `(Erode) Expanded` | -| 5 | (b) Per-voxel | Erode post-vote cleanup — `featureCount[feature] = 0` for each valid neighbor of the bad voxel | `(Erode) Expanded` (implicitly, via correct 2-iteration results) | +| 3 | (b) Per-voxel | `featureName == 0` + Dilate + neighbor `feature > 0` → `neighbors[neighborPoint] = voxelIndex` | `(Dilate) Expanded`, all 6 face directions confirmed hit (see below) | +| 4 | (b) Per-voxel | `featureName == 0` + Erode + neighbor `feature > 0` → vote accumulation, `neighbors[voxelIndex] = neighborPoint` on new-max (ties keep the first-processed neighbor) | `(Erode) Expanded`, all 6 face directions confirmed hit (see below) | +| 5 | (b) Per-voxel | Erode post-vote cleanup — `featureCount[feature] = 0` for each valid neighbor of the bad voxel | `(Erode) Expanded`, all 6 face directions confirmed hit (see below) | | 6 | (c) Transfer | `neighbor >= 0` + Erode condition (`featureName==0 && featureIds[neighbor]>0`) → `copyTuple` | `(Erode) Expanded` | | 7 | (c) Transfer | `neighbor >= 0` + Dilate condition (`featureName>0 && featureIds[neighbor]==0`) → `copyTuple` | `(Dilate) Expanded` | | 8 | (c) Transfer | `neighbor == -1` → skip (voxel untouched this iteration) | Both `Expanded` tests, implicitly (voxels far from bad data are unchanged in every expected array) | -| 9 | Preflight | `dims[0]==0 && dims[1]==0 && dims[2]==0` → error `-14602` (`k_NoGeometryDimensions`) | **Not covered.** The only test that zeroes the geometry dimensions (`(Dilate) No Dimensions`) *also* sets all three direction flags off, so the earlier `-14601` (`k_NoDirections_Error`) check fires first and the zero-dimensions branch is never reached. Confirmed by running the test locally: its assertion message is `-14601`, not `-14602`, despite the test's name. | +| 9 | Preflight | `dims[0]==0 && dims[1]==0 && dims[2]==0` → error `-14602` (`k_NoGeometryDimensions`) | **Not covered.** The only test that zeroes the geometry dimensions (`No Dimensions`) *also* sets all three direction flags off, so the earlier `-14601` (`k_NoDirections_Error`) check fires first and the zero-dimensions branch is never reached. Unrelated to this pass's fixes — carried over from the prior revision, still unresolved. | + +**Per-direction coverage, confirmed by instrumentation this pass:** `Algorithms/ErodeDilateBadData.cpp` was temporarily instrumented with hit counters per face direction (`-Z/-Y/-X/+X/+Y/+Z`) at (a) the point immediately after the `isValidFaceNeighbor` gate in the vote/mark loop, (b) the point where the Dilate mark / Erode vote condition (`feature > 0`) actually fires, and (c) the equivalent point in the Erode cleanup loop. Running the full `(Erode) Expanded` + `(Dilate) Expanded` sweep (28 GENERATE runs) produced non-zero counts for **every one of the 6 directions at every one of those 3 measurement points** — e.g. vote/mark loop reached counts were `-Z=38 -Y=111 -X=108 +X=106 +Y=64 +Z=97`, and the `feature>0` condition fired for Dilate marking at `-Z=9 -Y=46 -X=37 +X=35 +Y=16 +Z=44` and for Erode voting at `-Z=8 -Y=25 -X=24 +X=24 +Y=8 +Z=32`. The instrumentation was removed after confirming this (not shipped in the reverted-to-clean algorithm file); this row records the empirical result, not a standing code artifact. Additional confirmed items, not path gaps but worth recording: -- **No cancel path exists.** `m_ShouldCancel` is passed into `ErodeDilateBadData` and exposed via `getCancel()`, but `operator()` never reads it. The erode/dilate loop runs to completion regardless of a cancellation request — this is a behavior characteristic of the current implementation, not merely an untested path. -- **Direction masking is implemented unusually.** `adjustValidNeighbors` bitwise-ANDs the *face-index constants themselves* (`faceNeighborInternalIdx`, values 0–5) against the direction booleans, rather than gating a separate boolean-validity array. Combined with the observation that all 7 direction-combination fixtures for a given operation/iteration-count produce byte-identical expected output (see Summary and Oracle), this is flagged for second-engineer scrutiny — not as a confirmed defect (the current fixture cannot distinguish correct per-direction gating from a no-op direction gate), but as an area where an independent reviewer should hand-trace at least one single-axis-only case (e.g. Erode, `X` only, on a voxel whose good neighbors differ between the X-only and XYZ neighbor sets) to positively confirm the direction restriction behaves as documented. +- **No cancel path exists.** `m_ShouldCancel` is passed into `ErodeDilateBadData` and exposed via `getCancel()`, but `operator()` never reads it inside the erode/dilate loop. The loop runs to completion regardless of a cancellation request — this is a behavior characteristic of the current implementation, not merely an untested path. Confirmed present in legacy source too (legacy also never checks a cancel flag inside its equivalent loop) — not a deviation. +- **Direction masking, fixed.** Previously flagged: "`adjustValidNeighbors` bitwise-ANDs the face-index constants themselves... flagged for second-engineer scrutiny." This is now resolved — see Bug Fixes / `ErodeDilateBadDataFilter-B1` in the deviations doc. The function has been rewritten and is confirmed exercised across all 6 directions (this section, above) and legacy-verified across all 28 combinations (Oracle section). ## Test inventory | Test case | Notes | |-----------|-------| -| `SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded` | Class 1 oracle. `GENERATE` over 7 valid direction combinations × 2 iteration counts (14 runs). Compares `FeatureIds` and `Misc` against hand-traced expected arrays. Passes. | -| `SimplnxCore::ErodeDilateBadDataFilter(Dilate) Expanded` | Same sweep, Dilate operation. Passes. | -| `SimplnxCore::ErodeDilateBadDataFilter(Dilate) No Dimensions` | Preflight-error test: `ImageGeom` dimensions forced to `{0,0,0}`, directions also all off. Asserts `preflightResult.outputActions.invalid()`. **Misleading name** — actually exercises the no-direction path (`-14601`), not the zero-dimensions path (`-14602`), because directions are also off and that check runs first. | -| `SimplnxCore::ErodeDilateBadDataFilter(Dilate) No Direction` | Preflight-error test: all directions off, geometry otherwise valid. Asserts `-14601`. Correctly named and covers the intended path. | -| `SimplnxCore::ErodeDilateBadDataFilter: SIMPL Backwards Compatibility` | **New this pass.** `DYNAMIC_SECTION` over `simpl_conversion/6_5/ErodeDilateBadDataFilter.json` (matched by `Filter_Uuid`) and `simpl_conversion/6_4/ErodeDilateBadDataFilter.json` (matched by `Filter_Name`, no UUID field present in that fixture). Loads each legacy pipeline JSON via `Pipeline::FromSIMPLFile`, confirms it resolves to a single `PipelineFilter` with `FilterTraits::uuid`, and checks the converted arguments: `Operation == k_Dilate` (legacy `Direction: 0` round-trips to SIMPLNX's own `Dilate = 0`), `NumIterations == 5`, `XDirOn/YDirOn/ZDirOn == true`, geometry path `DataPath({"DataContainer"})`, feature-ids path `DataPath({"DataContainer","CellData","TestArray"})`. `IgnoredDataArrayPaths` (a `MultiDataArraySelectionFilterParameterConverter`) is verified only by successful pipeline load, not by value, matching the pattern used in `FillBadDataTest.cpp`. **27 assertions, both fixtures pass.** | - -Both `simpl_conversion/6_4/ErodeDilateBadDataFilter.json` and `simpl_conversion/6_5/ErodeDilateBadDataFilter.json` were already present on disk (as they are for sibling filters such as `FillBadDataFilter`) but were unused until this pass — the gap noted in the previous revision of this report is now closed. +| `SimplnxCore::ErodeDilateBadDataFilter(Erode)` | Exemplar-archive-based smoke test (`6_6_erode_dilate_test.tar.gz`), all directions on, 2 iterations. Passes. | +| `SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded` | Class 1 oracle, Class 2-corroborated (see Oracle). `GENERATE` over 7 valid direction combinations × 2 iteration counts (14 runs). Compares both `FeatureIds` and `Misc` against exemplar arrays. Passes. | +| `SimplnxCore::ErodeDilateBadDataFilter(Dilate) Expanded` | Same sweep, Dilate operation, both arrays asserted. Passes. | +| `SimplnxCore::ErodeDilateBadDataFilter Ignored Path` | Confirms an array listed in `IgnoredDataArrayPaths` (`Misc`) is left untouched. Passes. | +| `SimplnxCore::ErodeDilateBadDataFilter No Dimensions` | Preflight-error test: `ImageGeom` dimensions forced to `{0,0,0}`, directions also all off. Asserts `preflightResult.outputActions.invalid()`. **Misleading name** — actually exercises the no-direction path (`-14601`), not the zero-dimensions path (`-14602`), because directions are also off and that check runs first. Carried over, unresolved. | +| `SimplnxCore::ErodeDilateBadDataFilter No Direction` | Preflight-error test: all directions off, geometry otherwise valid. Asserts `-14601`. Correctly named and covers the intended path. | +| `SimplnxCore::ErodeDilateBadDataFilter: SIMPL Backwards Compatibility` | `DYNAMIC_SECTION` over `simpl_conversion/6_5/ErodeDilateBadDataFilter.json` (matched by `Filter_Uuid`) and `simpl_conversion/6_4/ErodeDilateBadDataFilter.json` (matched by `Filter_Name`, no UUID field present in that fixture). Loads each legacy pipeline JSON via `Pipeline::FromSIMPLFile`, confirms it resolves to a single `PipelineFilter` with `FilterTraits::uuid`, and checks the converted arguments: `Operation == k_Dilate` (legacy `Direction: 0` round-trips to SIMPLNX's own `Dilate = 0`), `NumIterations == 5`, `XDirOn/YDirOn/ZDirOn == true`, geometry path `DataPath({"DataContainer"})`, feature-ids path `DataPath({"DataContainer","CellData","TestArray"})`. `IgnoredDataArrayPaths` verified only by successful pipeline load, not by value, matching the pattern used in `FillBadDataTest.cpp`. Passes. | ## Deviations from DREAM3D 6.5.171 -Not evaluated in this pass — see [`deviations/ErodeDilateBadDataFilter.md`](deviations/ErodeDilateBadDataFilter.md). No legacy binary/pipeline comparison has been run for this filter; the oracle is Class 1 (Analytical) only, and legacy source is not present in this repository to support a source-level diff. +See [`deviations/ErodeDilateBadDataFilter.md`](deviations/ErodeDilateBadDataFilter.md) — one confirmed and fixed SIMPLNX-side bug (`ErodeDilateBadDataFilter-B1`, direction parameters had no effect), one investigated-and-disproven hypothesis (Dilate tie-break order — legacy matches SIMPLNX's original behavior), and no confirmed legacy deviations. Legacy binary/pipeline comparison has now been run (28/28 matches) — no longer a gap. diff --git a/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md index 1601e1df59..074fc2d717 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md @@ -4,20 +4,51 @@ Entries use stable IDs (`ErodeDilateBadDataFilter-D` for legacy deviations, ` --- -## Headline: No legacy comparison has been performed +## Headline: Legacy A/B comparison performed — one confirmed SIMPLNX-side bug, fixed -The [V&V report](../ErodeDilateBadDataFilter.md) for this filter uses a **Class 1 (Analytical) oracle only** — expected outputs are hand-traced against a small synthetic dataset, independent of any DREAM3D 6.5.171 run. No pipeline was executed in legacy DREAM3D 6.5.171 to produce a reference `.dream3d` file, and the legacy `ErodeDilateBadData` C++ source (`Source/Plugins/Processing/ProcessingFilters/ErodeDilateBadData.{h,cpp}`) is not present in this repository, so no source-level diff was possible either. +The gap recorded in the previous revision of this file ("no legacy comparison has been performed") is closed. Legacy source was located on this machine (`C:\Users\holym\BlueQuartz\Builds\DREAM3D\DREAM3D-6.5.171-Win64` binary, plus `ErodeDilateBadData.{h,cpp}` source in a sibling `DREAM3D` checkout — not committed to this repository, but usable for direct comparison) and a genuine DREAM3D 6.5.171 pipeline (`PipelineRunner.exe`) was run against a hand-verified input twin of the C++ test fixture, across **all 28 combinations** of operation (Erode/Dilate) × direction combination (7 valid combos) × iteration count (1, 2). See [`ErodeDilateBadDataFilter-B1`](#erodedilatebaddatafilter-b1-direction-parameters-had-no-effect-fixed) below and the V&V report's Oracle/Bug Fixes sections for full detail. -Consequently, this file records **no confirmed deviations** — not because none exist, but because the comparison that would surface them has not been done. This is a gap, not a clean bill of health. +## ErodeDilateBadDataFilter-B1: Direction parameters had no effect (fixed) -## What would need to happen to fill this in +**Type:** SIMPLNX-side bug (not a legacy deviation — legacy behaves correctly here; SIMPLNX did not). -1. Obtain or build a DREAM3D 6.5.171 binary (available locally at `C:\Users\holym\BlueQuartz\Builds\DREAM3D\DREAM3D-6.5.171-Win64` on this machine) and run an `ErodeDilateBadData` pipeline against a shared input dataset, in both Erode and Dilate modes, covering at least one case where direction restriction actually changes the result (see the V&V report's note that the current Class 1 fixture is direction-invariant for all 7 combinations it exercises). -2. Compare the legacy output against SIMPLNX output on the same input, using the same comparison discipline as other filters in this plugin (`UnitTest::CompareExemplarToGeneratedData` or equivalent element-wise check). -3. If legacy source becomes available for reference, diff the neighbor-selection, vote/tie-break, and direction-masking logic (`adjustValidNeighbors` in `Algorithms/ErodeDilateBadData.cpp`) against it directly — this is the one piece of the current implementation flagged for second-engineer scrutiny in the V&V report, precisely because the tie-break/direction-masking behavior could not be corroborated against a reference. +**Symptom:** `XDirOn`/`YDirOn`/`ZDirOn` were parsed correctly from filter args into `ErodeDilateBadDataInputValues` (`ErodeDilateBadDataFilter.cpp:151-153`), but had **zero effect** on the algorithm. Every face neighbor was eligible (subject only to geometry boundary) regardless of the Direction parameters. This is exactly what produced the previous V&V pass's observation that "all 7 direction-combination fixtures ... encode byte-identical expected output" — the fixture wasn't under-discriminating, the *algorithm* was ignoring direction entirely. -## Non-deviations (documented for awareness) +**Root cause:** `adjustValidNeighbors` — the helper clearly intended to mask face-neighbor validity by direction — was defined in `Algorithms/ErodeDilateBadData.cpp` but **never called** anywhere in `operator()()`. Confirmed by grepping the compiled `.cpp` for the literal strings `XDirOn`/`YDirOn`/`ZDirOn`/`adjustValidNeighbors(`: only the function *definition* matched, no call site. (A branch-history note: an earlier commit on this working branch, `7e543f701` "Fixed XYZ direction off bug", *had* added a call to `adjustValidNeighbors`, but passed it the face-index-order array and bitwise-ANDed index constants `0..5` against the direction booleans — which corrupts the iteration order rather than gating validity, and additionally had `+X` gated by `zDir` and `+Z` gated by `xDir` [swapped axes]. That call was later removed in an uncommitted edit, leaving direction fully inert — the state this pass found and fixed from scratch.) + +**Fix:** `Algorithms/ErodeDilateBadData.cpp:64-80` — `adjustValidNeighbors` now takes the per-voxel `isValidFaceNeighbor` boolean array (the actual validity gate consumed by the vote/mark loop) and ANDs each of the 6 entries against the correct axis flag, using the named `VoxelNeighbors` constants rather than raw indices. It is now called at `:162-163`, immediately after `computeValidFaceNeighbors`, for every bad-data voxel. + +**Verification:** +- All 28 `k_ExemplarFeatureIds*`/`k_ExemplarData*` (Misc) constants in `ErodeDilateBadDataTest.cpp` were rewritten to be direction-discriminating (previously byte-identical across all 7 combos for a given operation/iteration count) and hand-traced against the fixture geometry. +- Independently corroborated against genuine DREAM3D 6.5.171 output: 28/28 combinations (7 directions × 2 operations × 2 iteration counts) match exactly, both `FeatureIds` and `Misc` — see V&V report Oracle section for the run details. +- `(Erode) Expanded` / `(Dilate) Expanded` (28 GENERATE runs total): pass. + +## Non-deviations (confirmed correct — do not "fix") + +### Dilate tie-break: last-bad-neighbor-wins is correct, not a bug + +**Investigated and ruled out this pass.** When a good voxel has two or more bad face-neighbors, `neighbors[neighborPoint] = voxelIndex` unconditionally overwrites on each bad neighbor visited, so whichever bad voxel is scanned *last* (highest flat index, in z/y/x order) wins. Since every bad voxel shares `FeatureId == 0`, this choice is invisible to a `FeatureIds`-only comparison — it only shows up in the `Misc` tracer array, which is exactly why it was flagged as unverified in the prior pass and why this pass initially suspected it as a bug. + +A "first bad neighbor wins" fix (skip the overwrite if `neighbors[neighborPoint]` is already set, plus resetting `neighbors` to `-1` at the top of each iteration) was implemented and *appeared* correct until checked against real DREAM3D 6.5.171 output: `PipelineRunner.exe` running Dilate/XYZ/1-iteration against the matching legacy input file produced `Misc` values matching the **original, unmodified** last-write-wins SIMPLNX behavior, not the "first-wins" rewrite (diverged at 3 of 32 indices: 9, 15, 30). The "first-wins" change was reverted in full (both the per-iteration reset and the overwrite guard); unconditional last-write-wins, with `neighbors` initialized once before the iteration loop (not per-iteration), is confirmed legacy-faithful. + +This resolves the prior V&V pass's "second-engineer review pending: erode/dilate tie-break order" item — no further review needed; verified against the actual legacy binary output, not just source reading. + +### Erode tie-break: first-processed-neighbor-wins is correct + +Vote-count-based, using `[-Z,-Y,-X,+X,+Y,+Z]` scan order; a later neighbor's vote must strictly exceed the current max to replace the leader. Matches legacy source line-for-line (identical vote/comparison logic) and matches legacy binary output for all tested combinations. Not a deviation. ### Legacy tie-break language says "chosen randomly"; SIMPLNX is deterministic -The SIMPLNX filter markdown (`docs/ErodeDilateBadDataFilter.md`), which reads as carried over from legacy documentation, states that erode ties are broken "randomly." The current SIMPLNX implementation is deterministic: the first-processed neighbor (by `faceNeighborInternalIdx` order, `[-Z,-Y,-X,+X,+Y,+Z]`) wins ties, since a later neighbor's vote must strictly exceed the current maximum to replace it. Whether legacy DREAM3D 6.5.171 was actually nondeterministic (e.g., using an RNG) or merely used "random" loosely to mean "implementation-defined scan-order" has not been verified against legacy source. Recorded here as a documentation-language discrepancy worth resolving once legacy source or a legacy binary comparison is available, not asserted as a behavioral deviation. +The SIMPLNX filter markdown (`docs/ErodeDilateBadDataFilter.md`), carried over from legacy documentation, states that erode ties are broken "randomly." Both the legacy *source* (`ErodeDilateBadData.cpp`, `Source/Plugins/Processing/ProcessingFilters/`) and the legacy *binary* output are fully deterministic — same first-processed-wins scan order as SIMPLNX, no RNG involved anywhere in the algorithm. "Randomly" in the legacy docs is inaccurate documentation language, not a behavioral characteristic; SIMPLNX's determinism is not a deviation. (Legacy source is now available for direct comparison — this was previously only inferable.) + +## Per-direction code-path coverage + +Previously an open question ("could not distinguish correct per-direction gating from a no-op gate" — see prior V&V pass). Resolved this pass by two independent means: +1. **Behavioral:** exemplar data now differs by direction combination (see B1 above), and matches legacy per-combination. +2. **Structural:** `Algorithms/ErodeDilateBadData.cpp` was temporarily instrumented with per-face-direction (`-Z/-Y/-X/+X/+Y/+Z`) hit counters and run through the full `(Erode) Expanded` + `(Dilate) Expanded` GENERATE sweep. All 6 directions were hit — both at the "loop reached" level and at the "vote/mark condition fired" level, in both the vote/mark loop and the Erode-only cleanup loop. Instrumentation was removed afterward (not shipped). + +## What would need to happen to extend this further + +Everything listed in the prior revision of this file has been done: legacy binary obtained and run, SIMPLNX output compared element-wise against legacy output, and the neighbor-selection/tie-break logic diffed directly against legacy source. Remaining follow-up (not gating, see V&V report): +1. The legacy A/B comparison in this pass was a manual/one-time verification (pipeline JSONs run through `PipelineRunner.exe`, output diffed via `h5py`), not wired into automated CI. Consider checking in the legacy `.dream3d` input/output pairs as an exemplar archive and adding an automated Class 2 comparison test (matching the pattern used by `FillBadDataFilter`'s `FillBadData_SmallIN100` test), so this verification re-runs on every CI build instead of relying on this document. +2. Zero-dimensions preflight path (`-14602`) is still not reached by any test — unrelated to this pass's fixes, see V&V report Code path coverage. From 5426dee2c495fbb2b27478718b2774419da3e781 Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Tue, 11 Aug 2026 14:57:50 -0400 Subject: [PATCH 11/14] Fix V&V docs --- .../SimplnxCore/vv/ErodeDilateBadDataFilter.md | 18 +++++++++--------- .../vv/deviations/ErodeDilateBadDataFilter.md | 3 +-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md b/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md index e15a26a414..a25861e0ea 100644 --- a/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md @@ -16,12 +16,12 @@ |------------------------|----------------| | Algorithm Relationship | **Port, confirmed by direct source diff** — legacy `ErodeDilateBadData.{h,cpp}` located and compared line-by-line against `Algorithms/ErodeDilateBadData.cpp` this pass. Neighbor offsets, boundary-validity checks, vote/tie-break order, and transfer conditions are structurally identical. One divergence found and fixed — see Bug Fixes. | | Oracle | **Class 1 (Analytically derived), corroborated by out-of-band Class 2 (Reference implementation) A/B run.** Expected `FeatureIds`/`Misc` values are hand-traced against the 32-voxel fixture and compiled into the test as constants (Class 1 in form), but every one of the 28 combinations (7 directions × 2 operations × 2 iteration counts) has additionally been independently verified against genuine DREAM3D 6.5.171 binary output (Class 2 in substance) — see Oracle section. The A/B run is manual/one-time, not an automated CI test — see deviations doc for a recommendation to formalize it. | -| Code paths enumerated | 8 of 9 paths exercised, all 6 face directions (-Z/-Y/-X/+X/+Y/+Z) confirmed hit by instrumentation. 1 confirmed gap: the zero-dimensions preflight error is never reached by any test (unchanged from prior pass — see below). | -| Tests today | **7 TEST_CASEs, all pass**: `(Erode)`, `(Erode) Expanded`, `(Dilate) Expanded` (GENERATE sweep, 14 valid runs each — **both `FeatureIds` and `Misc` asserted**, previously `Misc` was disabled), `(Dilate) Ignored Path`, `(Dilate) No Direction`, `(Dilate) No Dimensions`, and `: SIMPL Backwards Compatibility`. | +| Code paths enumerated | **9 of 9 paths exercised**, all 6 face directions (-Z/-Y/-X/+X/+Y/+Z) confirmed hit by instrumentation. The zero-dimensions preflight path, flagged as uncovered in the prior V&V pass, is now reached and correctly asserts `-14602` — see below. | +| Tests today | **7 TEST_CASEs, all pass**: `(Erode)`, `(Erode) Expanded`, `(Dilate) Expanded` (GENERATE sweep, 14 valid runs each — **both `FeatureIds` and `Misc` asserted**, previously `Misc` was disabled), `Ignored Path` (both operations via `GENERATE(0,1)`), `No Direction` (both operations via `GENERATE(0,1)`), `No Dimensions` (Dilate only), and `: SIMPL Backwards Compatibility`. | | Test fixtures | Inline `CreateTestData()` — no exemplar archive for the automated tests. 32-voxel `ImageGeom` (4×4×2), hand-set `FeatureIds` (5 bad voxels at indices 0, 10, 13, 14, 31; features 1–6 elsewhere) plus a `Misc` int32 array initialized to its own index (`data[i] = i`) so every transferred value traces back to its source voxel unambiguously. Separately, a byte-for-byte HDF5 twin of this fixture (`Test Data/erode_dilate_legacy/erode_dilate_bad_data_base_test.dream3d`) was used for the manual legacy A/B run — confirmed identical dims/FeatureIds/Misc before use. | | Legacy comparison | **Performed this pass.** All 28 combinations run through DREAM3D 6.5.171 (`PipelineRunner.exe`) against the `.dream3d` twin fixture; `FeatureIds` and `Misc` diffed element-wise against SIMPLNX's exemplar constants. **28/28 exact matches.** See Oracle section for the run list. | | Bug flags | `ErodeDilateBadDataFilter-B1` (direction parameters had no effect) — **confirmed and fixed this pass.** One additional hypothesis (Dilate tie-break order) was investigated, found to be a false lead, and reverted — see deviations doc. | -| V&V phase | Direction-masking bug fixed, committed (`4437eacda`), and legacy-verified across all 28 combinations. Outstanding before promotion: (1) zero-dimensions preflight test still misnamed/not reaching its target path (pre-existing, unrelated to this pass); (2) formalize the manual legacy A/B run as an automated Class 2 CI test (see deviations doc). | +| V&V phase | Direction-masking bug fixed, committed (`4437eacda`), and legacy-verified across all 28 combinations. Zero-dimensions preflight path now covered (9/9 paths). Outstanding before promotion: formalize the manual legacy A/B run as an automated Class 2 CI test (see deviations doc). | ## Summary @@ -31,7 +31,7 @@ A second hypothesis — that the Dilate tie-break order (which of several bad neighbors a good voxel copies from) was also wrong — was investigated, a fix was implemented, and it was then **disproven** by running the actual DREAM3D 6.5.171 binary: legacy uses the same last-write-wins behavior the original SIMPLNX code already had. The fix was reverted. See deviations doc, "Dilate tie-break: last-bad-neighbor-wins is correct, not a bug." -Verification is now **Class 1 (Analytical) in form, Class 2 (Reference implementation) in substance**: two `GENERATE`-driven test cases (`(Erode) Expanded`, `(Dilate) Expanded`) sweep all 7 valid direction combinations (all-off is skipped) × 2 iteration counts against expected `FeatureIds`/`Misc` arrays for a small, fully-inspectable 32-voxel dataset, and every one of those 28 combinations has additionally been independently corroborated against real DREAM3D 6.5.171 output (see Oracle section). All 7 tests pass, **1877 assertions**, both `FeatureIds` and `Misc` checked in every `Expanded` run (`Misc` was previously commented out — see prior revision of this report). +Verification is now **Class 1 (Analytical) in form, Class 2 (Reference implementation) in substance**: two `GENERATE`-driven test cases (`(Erode) Expanded`, `(Dilate) Expanded`) sweep all 7 valid direction combinations (all-off is skipped) × 2 iteration counts against expected `FeatureIds`/`Misc` arrays for a small, fully-inspectable 32-voxel dataset, and every one of those 28 combinations has additionally been independently corroborated against real DREAM3D 6.5.171 output (see Oracle section). All 7 tests pass, **2033 assertions** (verified by direct local run of the `[ErodeDilateBadDataFilter]` tag), both `FeatureIds` and `Misc` checked in every `Expanded` run (`Misc` was previously commented out — see prior revision of this report). ## Algorithm Relationship @@ -46,7 +46,7 @@ Verification is now **Class 1 (Analytical) in form, Class 2 (Reference implement - **One divergence found:** legacy ORs the direction flag into the same boundary check for every neighbor (`|| !m_ZDirOn` etc.); SIMPLNX's `adjustValidNeighbors` was supposed to do the equivalent but was never called — see Bug Fixes / deviations doc `ErodeDilateBadDataFilter-B1`. - `SimplnxCoreLegacyUUIDMapping.hpp` maps legacy SIMPL UUID `3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254` directly to `FilterTraits`, and `test/simpl_conversion/{6_4,6_5}/ErodeDilateBadDataFilter.json` carry the legacy `Direction`/`NumIterations`/`XDirOn`/`YDirOn`/`ZDirOn`/`FeatureIdsArrayPath`/`IgnoredDataArrayPaths` parameter set unchanged — this is the same filter, not a reimplementation with a different parameter model. -*SIMPLNX implementation:* `Algorithms/ErodeDilateBadData.cpp` (~215 lines) uses `NeighborUtilities::VoxelNeighbors` for face-neighbor offsets and boundary validity, and `ParallelTaskAlgorithm` to transfer non-`FeatureIds` arrays in parallel (with `FeatureIds` itself transferred afterward, serially, since the transfer condition for every other array depends on the *current* `FeatureIds` values). +*SIMPLNX implementation:* `Algorithms/ErodeDilateBadData.cpp` (~230 lines) uses `NeighborUtilities::VoxelNeighbors` for face-neighbor offsets and boundary validity, and `ParallelTaskAlgorithm` to transfer non-`FeatureIds` arrays in parallel (with `FeatureIds` itself transferred afterward, serially, since the transfer condition for every other array depends on the *current* `FeatureIds` values). ## Bug Fixes (this pass) @@ -72,7 +72,7 @@ A plausible-looking bug hypothesis (last-bad-neighbor-wins vs. first-bad-neighbo ## Code path coverage -8 of 9 paths exercised. Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp`. +9 of 9 paths exercised. Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp`. | # | Phase | Path | Test case | |---|-------|------|-----------| @@ -84,13 +84,13 @@ A plausible-looking bug hypothesis (last-bad-neighbor-wins vs. first-bad-neighbo | 6 | (c) Transfer | `neighbor >= 0` + Erode condition (`featureName==0 && featureIds[neighbor]>0`) → `copyTuple` | `(Erode) Expanded` | | 7 | (c) Transfer | `neighbor >= 0` + Dilate condition (`featureName>0 && featureIds[neighbor]==0`) → `copyTuple` | `(Dilate) Expanded` | | 8 | (c) Transfer | `neighbor == -1` → skip (voxel untouched this iteration) | Both `Expanded` tests, implicitly (voxels far from bad data are unchanged in every expected array) | -| 9 | Preflight | `dims[0]==0 && dims[1]==0 && dims[2]==0` → error `-14602` (`k_NoGeometryDimensions`) | **Not covered.** The only test that zeroes the geometry dimensions (`No Dimensions`) *also* sets all three direction flags off, so the earlier `-14601` (`k_NoDirections_Error`) check fires first and the zero-dimensions branch is never reached. Unrelated to this pass's fixes — carried over from the prior revision, still unresolved. | +| 9 | Preflight | `dims[0]==0 \|\| dims[1]==0 \|\| dims[2]==0` → error `-14602` (`k_NoGeometryDimensions`) | **Covered.** `No Dimensions` test now sets `directions = {true, true, true}` (previously all-off, which tripped the earlier `-14601` check first and masked this path — see prior V&V revision). Run and confirmed locally: 3/3 assertions pass, error code is exactly `-14602`. Also note the boundary condition itself changed from `&&` to `\|\|` (any single dimension being 0 is now sufficient to trigger the error, not just all three) — both the test fix and the condition fix landed together in this branch's `Fixed filter preflight errors` / `Fixing ErodeDilateBadData` commits. | **Per-direction coverage, confirmed by instrumentation this pass:** `Algorithms/ErodeDilateBadData.cpp` was temporarily instrumented with hit counters per face direction (`-Z/-Y/-X/+X/+Y/+Z`) at (a) the point immediately after the `isValidFaceNeighbor` gate in the vote/mark loop, (b) the point where the Dilate mark / Erode vote condition (`feature > 0`) actually fires, and (c) the equivalent point in the Erode cleanup loop. Running the full `(Erode) Expanded` + `(Dilate) Expanded` sweep (28 GENERATE runs) produced non-zero counts for **every one of the 6 directions at every one of those 3 measurement points** — e.g. vote/mark loop reached counts were `-Z=38 -Y=111 -X=108 +X=106 +Y=64 +Z=97`, and the `feature>0` condition fired for Dilate marking at `-Z=9 -Y=46 -X=37 +X=35 +Y=16 +Z=44` and for Erode voting at `-Z=8 -Y=25 -X=24 +X=24 +Y=8 +Z=32`. The instrumentation was removed after confirming this (not shipped in the reverted-to-clean algorithm file); this row records the empirical result, not a standing code artifact. Additional confirmed items, not path gaps but worth recording: -- **No cancel path exists.** `m_ShouldCancel` is passed into `ErodeDilateBadData` and exposed via `getCancel()`, but `operator()` never reads it inside the erode/dilate loop. The loop runs to completion regardless of a cancellation request — this is a behavior characteristic of the current implementation, not merely an untested path. Confirmed present in legacy source too (legacy also never checks a cancel flag inside its equivalent loop) — not a deviation. +- **Cancel path exists but is untested.** Corrected from the prior V&V pass, which found no cancel check present at the time. As of `4437eacda` ("Fixing ErodeDilateBadData"), `operator()` now reads `m_ShouldCancel` once per Z-slice (`Algorithms/ErodeDilateBadData.cpp:144-148`, inside the outer `for(zIdx...)` loop, itself inside the `for(iteration...)` loop) and returns immediately if set. This is a real, functional early-exit — checked on every Z-slice of every iteration, not just once — but no current test sets `m_ShouldCancel` and asserts early termination, so this path is present in the code and reachable, but not exercised by any `TEST_CASE`. Not counted in the 9-path table above (that table scopes to `preflightImpl`/vote-transfer branches); worth considering as a 10th path if the table's scope is later widened. Legacy's equivalent loop has no cancel check at all — SIMPLNX is ahead of legacy here, not behind; not a deviation. - **Direction masking, fixed.** Previously flagged: "`adjustValidNeighbors` bitwise-ANDs the face-index constants themselves... flagged for second-engineer scrutiny." This is now resolved — see Bug Fixes / `ErodeDilateBadDataFilter-B1` in the deviations doc. The function has been rewritten and is confirmed exercised across all 6 directions (this section, above) and legacy-verified across all 28 combinations (Oracle section). ## Test inventory @@ -101,7 +101,7 @@ Additional confirmed items, not path gaps but worth recording: | `SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded` | Class 1 oracle, Class 2-corroborated (see Oracle). `GENERATE` over 7 valid direction combinations × 2 iteration counts (14 runs). Compares both `FeatureIds` and `Misc` against exemplar arrays. Passes. | | `SimplnxCore::ErodeDilateBadDataFilter(Dilate) Expanded` | Same sweep, Dilate operation, both arrays asserted. Passes. | | `SimplnxCore::ErodeDilateBadDataFilter Ignored Path` | Confirms an array listed in `IgnoredDataArrayPaths` (`Misc`) is left untouched. Passes. | -| `SimplnxCore::ErodeDilateBadDataFilter No Dimensions` | Preflight-error test: `ImageGeom` dimensions forced to `{0,0,0}`, directions also all off. Asserts `preflightResult.outputActions.invalid()`. **Misleading name** — actually exercises the no-direction path (`-14601`), not the zero-dimensions path (`-14602`), because directions are also off and that check runs first. Carried over, unresolved. | +| `SimplnxCore::ErodeDilateBadDataFilter No Dimensions` | Preflight-error test: `ImageGeom` dimensions forced to `{0,0,0}`, directions all **on**. Asserts `preflightResult.outputActions.invalid()` and `errors()[0].code == -14602`. Correctly named and covers the intended zero-dimensions path (previously it also zeroed all direction flags, which tripped the earlier `-14601` check first — now fixed). | | `SimplnxCore::ErodeDilateBadDataFilter No Direction` | Preflight-error test: all directions off, geometry otherwise valid. Asserts `-14601`. Correctly named and covers the intended path. | | `SimplnxCore::ErodeDilateBadDataFilter: SIMPL Backwards Compatibility` | `DYNAMIC_SECTION` over `simpl_conversion/6_5/ErodeDilateBadDataFilter.json` (matched by `Filter_Uuid`) and `simpl_conversion/6_4/ErodeDilateBadDataFilter.json` (matched by `Filter_Name`, no UUID field present in that fixture). Loads each legacy pipeline JSON via `Pipeline::FromSIMPLFile`, confirms it resolves to a single `PipelineFilter` with `FilterTraits::uuid`, and checks the converted arguments: `Operation == k_Dilate` (legacy `Direction: 0` round-trips to SIMPLNX's own `Dilate = 0`), `NumIterations == 5`, `XDirOn/YDirOn/ZDirOn == true`, geometry path `DataPath({"DataContainer"})`, feature-ids path `DataPath({"DataContainer","CellData","TestArray"})`. `IgnoredDataArrayPaths` verified only by successful pipeline load, not by value, matching the pattern used in `FillBadDataTest.cpp`. Passes. | diff --git a/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md index 074fc2d717..7505cc6971 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md @@ -49,6 +49,5 @@ Previously an open question ("could not distinguish correct per-direction gating ## What would need to happen to extend this further -Everything listed in the prior revision of this file has been done: legacy binary obtained and run, SIMPLNX output compared element-wise against legacy output, and the neighbor-selection/tie-break logic diffed directly against legacy source. Remaining follow-up (not gating, see V&V report): +Everything listed in the prior revision of this file has been done: legacy binary obtained and run, SIMPLNX output compared element-wise against legacy output, and the neighbor-selection/tie-break logic diffed directly against legacy source. The zero-dimensions preflight path (`-14602`), also previously listed here as uncovered, is now reached and correctly asserted by the `No Dimensions` test (fixed on this branch — see V&V report Code path coverage). Remaining follow-up (not gating, see V&V report): 1. The legacy A/B comparison in this pass was a manual/one-time verification (pipeline JSONs run through `PipelineRunner.exe`, output diffed via `h5py`), not wired into automated CI. Consider checking in the legacy `.dream3d` input/output pairs as an exemplar archive and adding an automated Class 2 comparison test (matching the pattern used by `FillBadDataFilter`'s `FillBadData_SmallIN100` test), so this verification re-runs on every CI build instead of relying on this document. -2. Zero-dimensions preflight path (`-14602`) is still not reached by any test — unrelated to this pass's fixes, see V&V report Code path coverage. From 42934ffcdd9503ba08e4a40a205a12334badf0a8 Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Tue, 11 Aug 2026 15:00:34 -0400 Subject: [PATCH 12/14] Clang-format unit tests --- src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp b/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp index 51e47ccfc6..c479978838 100644 --- a/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp +++ b/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp @@ -134,7 +134,6 @@ constexpr ExemplarDataType k_ExemplarDataErodeZ1{16, 1, 2, 3, 4, 5, 6, 7, 8, 9, constexpr ExemplarDataType k_ExemplarFeatureIdsErodeZ2{4, 1, 1, 2, 2, 1, 2, 2, 1, 1, 5, 2, 2, 6, 6, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; constexpr ExemplarDataType k_ExemplarDataErodeZ2{16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 26, 11, 12, 29, 30, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; - DataStructure CreateTestData() { DataStructure dataStructure; @@ -240,12 +239,12 @@ void CheckDilateOutput(const DataStructure& dataStructure, const DirectionType& ExemplarDataType exemplarData; bool only1Iteration = iterations == 1; - if (directions == k_XDir) + if(directions == k_XDir) { exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsDilateX1 : k_ExemplarFeatureIdsDilateX2; exemplarData = only1Iteration ? k_ExemplarDataDilateX1 : k_ExemplarDataDilateX2; } - else if (directions == k_XYDir) + else if(directions == k_XYDir) { exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsDilateXY1 : k_ExemplarFeatureIdsDilateXY2; exemplarData = only1Iteration ? k_ExemplarDataDilateXY1 : k_ExemplarDataDilateXY2; From c2d3d4afe66547eaba0ed7d64f4d293e29c7340d Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 14 Aug 2026 13:17:35 -0400 Subject: [PATCH 13/14] REV: Address PR 1687 re-review feedback for ErodeDilateBadData * 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 --- .../docs/ErodeDilateBadDataFilter.md | 26 +- .../Filters/Algorithms/ErodeDilateBadData.cpp | 3 +- .../Filters/ErodeDilateBadDataFilter.cpp | 4 +- .../test/ErodeDilateBadDataTest.cpp | 283 +++++++++--------- .../vv/ErodeDilateBadDataFilter.md | 30 +- .../vv/deviations/ErodeDilateBadDataFilter.md | 6 +- 6 files changed, 192 insertions(+), 160 deletions(-) diff --git a/src/Plugins/SimplnxCore/docs/ErodeDilateBadDataFilter.md b/src/Plugins/SimplnxCore/docs/ErodeDilateBadDataFilter.md index 4e1c68abef..42dea7c9a9 100644 --- a/src/Plugins/SimplnxCore/docs/ErodeDilateBadDataFilter.md +++ b/src/Plugins/SimplnxCore/docs/ErodeDilateBadDataFilter.md @@ -23,16 +23,17 @@ Cell neighboring a *bad* **Cell** will be changed to *0*. If the *bad* data is *eroded*, the Filter shrinks the bad data by one **Cell** in an iterative sequence for a user defined number of iterations. During the *erode* process -the *Feature Id* of the *bad* **Cell** is changed from *0* to the *Feature Id* of the majority of its neighbors. If -there is a tie between two *Feature Ids*, then one of the *Feature Ids*, chosen randomly, will be assigned to the *bad* -**Cell**. +the *Feature Id* of the *bad* **Cell** is changed from *0* to the *Feature Id* of the majority of its neighbors. + +Ties are broken deterministically, not randomly. The Filter visits the six face neighbors in the fixed order +*-Z, -Y, -X, +X, +Y, +Z*, and a later neighbor must have a strictly greater count than the current leader to replace it. +When two or more *Feature Ids* are tied for the majority, the one belonging to the earliest neighbor in that scan order +is assigned. The same input therefore always produces the same output. | Before Erosion | After Erosion | |--------------------------------------|--------------------------------------| | ![](Images/ErodeDilateBadData_1.png) | ![](Images/ErodeDilateBadData_3.png) | -` - Goals a user might be trying to accomplish with this Filter include: - Remove small or thin regions of bad data by running a single (or two) iteration *erode* operation. @@ -54,6 +55,21 @@ The *Operation* parameter selects which morphological operation to apply: - **Dilate [0]**: Grows bad data regions by one **Cell** per iteration. Any **Cell** neighboring a bad **Cell** has its *Feature Id* changed to 0. - **Erode [1]**: Shrinks bad data regions by one **Cell** per iteration. Each bad **Cell** is assigned the *Feature Id* of the majority of its neighbors. +### Direction Restrictions + +The *X Direction*, *Y Direction*, and *Z Direction* parameters control which of the six face neighbors participate. With +all three enabled the Filter uses all six face neighbors (*-Z, -Y, -X, +X, +Y, +Z*); disabling *Z Direction*, for +example, restricts the operation to the four in-plane neighbors so that bad data grows or shrinks only within each XY +slice. + +### Preflight Errors + +The Filter refuses to run in two cases: + +- **-14601**: all three of *X Direction*, *Y Direction*, and *Z Direction* are disabled. At least one direction is + required, otherwise there are no neighbors to erode or dilate across. +- **-14602**: the selected **Image Geometry** has a dimension of *0* **Cells**. All three dimensions must be non-zero. + ## WARNING: Feature Data Will Become Invalid By modifying the cell level data, any feature data that was previously computed will most likely be invalid at this point. Filters that compute feature level data should be rerun to ensure accurate final results from your pipeline. diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp index 1af0fae808..716f4bb2fd 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp @@ -133,7 +133,7 @@ Result<> ErodeDilateBadData::operator()() constexpr FaceNeighborType k_NumFaceNeighbors = VoxelNeighbors::k_FaceNeighborCount; const std::array neighborVoxelIndexOffsets = initializeFaceNeighborOffsets(dims); - std::array faceNeighborInternalIdx = initializeFaceNeighborInternalIdx(); + constexpr std::array faceNeighborInternalIdx = initializeFaceNeighborInternalIdx(); std::vector featureCount(numFeatures + 1, 0); @@ -224,6 +224,7 @@ Result<> ErodeDilateBadData::operator()() auto featureIDataArray = m_DataStructure.getSharedDataAs(m_InputValues->FeatureIdsArrayPath); taskRunner.setParallelizationEnabled(false); // Do this to make the next call synchronous taskRunner.execute(ErodeDilateBadDataTransferDataImpl(totalPoints, m_InputValues->Operation, featureIds, neighbors, featureIDataArray, messageHelper)); + taskRunner.wait(); // Redundant while parallelization is disabled, but keeps the "transfer is complete" invariant local. } return {}; diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp index 609af0ea79..af5c7a4302 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateBadDataFilter.cpp @@ -22,7 +22,7 @@ using namespace nx::core; namespace { constexpr int32 k_NoDirectionsError = -14601; -constexpr int32 k_NoGeometryDimensions = -14602; +constexpr int32 k_NoGeometryDimensionsError = -14602; } // namespace namespace nx::core @@ -124,7 +124,7 @@ IFilter::PreflightResult ErodeDilateBadDataFilter::preflightImpl(const DataStruc auto dims = imageGeom.getDimensions(); if(dims[0] == 0 || dims[1] == 0 || dims[2] == 0) { - return {MakeErrorResult(k_NoGeometryDimensions, "ErodeDilateBadData requires that the ImageGeom have its dimensions set. No dimension may be 0.")}; + return {MakeErrorResult(k_NoGeometryDimensionsError, "ErodeDilateBadData requires that the ImageGeom have its dimensions set. No dimension may be 0.")}; } std::string featureModificationWarning = "By modifying the cell level data, any feature data that was previously computed will most likely be invalid at this point. Filters that compute feature " diff --git a/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp b/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp index c479978838..7cf97ef344 100644 --- a/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp +++ b/src/Plugins/SimplnxCore/test/ErodeDilateBadDataTest.cpp @@ -13,6 +13,7 @@ #include "simplnx/UnitTest/UnitTestCommon.hpp" #include "simplnx/Utilities/Parsing/HDF5/IO/FileIO.hpp" +#include #include #include @@ -33,8 +34,12 @@ const DataPath k_EbsdScanDataDataPath = k_InputData.createChildPath(k_EbsdScanDa const DataPath k_FeatureIdsDataPath = k_EbsdScanDataDataPath.createChildPath("FeatureIds"); const StringLiteral k_MiscData = "Misc"; -const ShapeType k_TupleShape{4, 4, 2}; -const usize k_NumTuples = 32; +constexpr usize k_NumTuples = 32; +// ImageGeom dimensions are ordered X, Y, Z. AttributeMatrix and DataArray tuple shapes are ordered +// slowest-to-fastest, i.e. Z, Y, X. Both describe the same 32 cells, so keep them as separate constants +// rather than reusing one that happens to multiply out to the same tuple count. +const SizeVec3 k_GeometryDimensions{4, 4, 2}; +const ShapeType k_TupleShape{k_GeometryDimensions[2], k_GeometryDimensions[1], k_GeometryDimensions[0]}; const DataPath k_DataPath({::k_ImageGeometry, ::k_CellData, k_MiscData}); const DataPath k_ImageFeatureIdsPath({::k_ImageGeometry, ::k_CellData, k_FeatureIds}); @@ -47,7 +52,7 @@ constexpr DirectionType k_YDir{false, true, false}; constexpr DirectionType k_YZDir{false, true, true}; constexpr DirectionType k_ZDir{false, false, true}; -using ExemplarDataType = std::array; +using ExemplarDataType = std::array; // Exemplar Dilate data for A/B testing constexpr ExemplarDataType k_ExemplarFeatureIdsDilateX1{0, 0, 1, 2, 2, 1, 2, 2, 1, 0, 0, 0, 0, 0, 0, 0, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 0, 0}; constexpr ExemplarDataType k_ExemplarDataDilateX1{0, 0, 2, 3, 4, 5, 6, 7, 8, 10, 10, 10, 13, 13, 14, 14, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 31, 31}; @@ -134,16 +139,63 @@ constexpr ExemplarDataType k_ExemplarDataErodeZ1{16, 1, 2, 3, 4, 5, 6, 7, 8, 9, constexpr ExemplarDataType k_ExemplarFeatureIdsErodeZ2{4, 1, 1, 2, 2, 1, 2, 2, 1, 1, 5, 2, 2, 6, 6, 3, 4, 4, 4, 4, 3, 3, 3, 3, 5, 5, 5, 5, 5, 6, 6, 3}; constexpr ExemplarDataType k_ExemplarDataErodeZ2{16, 1, 2, 3, 4, 5, 6, 7, 8, 9, 26, 11, 12, 29, 30, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 15}; +/** + * @brief The expected FeatureIds and Misc output for a single (operation, directions, iterations) combination. + */ +struct ExemplarRecord +{ + ChoicesParameter::ValueType operation; + DirectionType directions; + int32 iterations; + const ExemplarDataType& expectedFeatureIds; + const ExemplarDataType& expectedData; +}; + +// One row per valid parameter combination: 2 operations x 7 direction combinations x 2 iteration counts. +// The all-directions-off combination is rejected by preflight and therefore has no row. +// clang-format off +const std::array k_Exemplars{{ + {k_Dilate, k_XDir, 1, k_ExemplarFeatureIdsDilateX1, k_ExemplarDataDilateX1}, + {k_Dilate, k_XDir, 2, k_ExemplarFeatureIdsDilateX2, k_ExemplarDataDilateX2}, + {k_Dilate, k_XYDir, 1, k_ExemplarFeatureIdsDilateXY1, k_ExemplarDataDilateXY1}, + {k_Dilate, k_XYDir, 2, k_ExemplarFeatureIdsDilateXY2, k_ExemplarDataDilateXY2}, + {k_Dilate, k_XYZDir, 1, k_ExemplarFeatureIdsDilateXYZ1, k_ExemplarDataDilateXYZ1}, + {k_Dilate, k_XYZDir, 2, k_ExemplarFeatureIdsDilateXYZ2, k_ExemplarDataDilateXYZ2}, + {k_Dilate, k_XZDir, 1, k_ExemplarFeatureIdsDilateXZ1, k_ExemplarDataDilateXZ1}, + {k_Dilate, k_XZDir, 2, k_ExemplarFeatureIdsDilateXZ2, k_ExemplarDataDilateXZ2}, + {k_Dilate, k_YDir, 1, k_ExemplarFeatureIdsDilateY1, k_ExemplarDataDilateY1}, + {k_Dilate, k_YDir, 2, k_ExemplarFeatureIdsDilateY2, k_ExemplarDataDilateY2}, + {k_Dilate, k_YZDir, 1, k_ExemplarFeatureIdsDilateYZ1, k_ExemplarDataDilateYZ1}, + {k_Dilate, k_YZDir, 2, k_ExemplarFeatureIdsDilateYZ2, k_ExemplarDataDilateYZ2}, + {k_Dilate, k_ZDir, 1, k_ExemplarFeatureIdsDilateZ1, k_ExemplarDataDilateZ1}, + {k_Dilate, k_ZDir, 2, k_ExemplarFeatureIdsDilateZ2, k_ExemplarDataDilateZ2}, + {k_Erode, k_XDir, 1, k_ExemplarFeatureIdsErodeX1, k_ExemplarDataErodeX1}, + {k_Erode, k_XDir, 2, k_ExemplarFeatureIdsErodeX2, k_ExemplarDataErodeX2}, + {k_Erode, k_XYDir, 1, k_ExemplarFeatureIdsErodeXY1, k_ExemplarDataErodeXY1}, + {k_Erode, k_XYDir, 2, k_ExemplarFeatureIdsErodeXY2, k_ExemplarDataErodeXY2}, + {k_Erode, k_XYZDir, 1, k_ExemplarFeatureIdsErodeXYZ1, k_ExemplarDataErodeXYZ1}, + {k_Erode, k_XYZDir, 2, k_ExemplarFeatureIdsErodeXYZ2, k_ExemplarDataErodeXYZ2}, + {k_Erode, k_XZDir, 1, k_ExemplarFeatureIdsErodeXZ1, k_ExemplarDataErodeXZ1}, + {k_Erode, k_XZDir, 2, k_ExemplarFeatureIdsErodeXZ2, k_ExemplarDataErodeXZ2}, + {k_Erode, k_YDir, 1, k_ExemplarFeatureIdsErodeY1, k_ExemplarDataErodeY1}, + {k_Erode, k_YDir, 2, k_ExemplarFeatureIdsErodeY2, k_ExemplarDataErodeY2}, + {k_Erode, k_YZDir, 1, k_ExemplarFeatureIdsErodeYZ1, k_ExemplarDataErodeYZ1}, + {k_Erode, k_YZDir, 2, k_ExemplarFeatureIdsErodeYZ2, k_ExemplarDataErodeYZ2}, + {k_Erode, k_ZDir, 1, k_ExemplarFeatureIdsErodeZ1, k_ExemplarDataErodeZ1}, + {k_Erode, k_ZDir, 2, k_ExemplarFeatureIdsErodeZ2, k_ExemplarDataErodeZ2}, +}}; +// clang-format on + DataStructure CreateTestData() { DataStructure dataStructure; auto* geom = ImageGeom::Create(dataStructure, ::k_ImageGeometry); - geom->setDimensions(SizeVec3{k_TupleShape[0], k_TupleShape[1], k_TupleShape[2]}); + geom->setDimensions(k_GeometryDimensions); auto* cellData = AttributeMatrix::Create(dataStructure, ::k_CellData, k_TupleShape, geom->getId()); // Feature IDs - auto featureIdsPtr = std::make_shared(k_NumTuples, 0); + auto featureIdsPtr = std::make_shared(k_TupleShape, ShapeType{1}, 0); auto* featureIdsArray = Int32Array::Create(dataStructure, ::k_FeatureIds, featureIdsPtr, cellData->getId()); // Index 0, 14, 31 @@ -189,7 +241,7 @@ DataStructure CreateTestData() featureIds[31] = 0; // Misc DataArray - auto dataStorePtr = std::make_shared(k_NumTuples, 0); + auto dataStorePtr = std::make_shared(k_TupleShape, ShapeType{1}, 0); auto* miscArray = Int32Array::Create(dataStructure, k_MiscData, dataStorePtr, cellData->getId()); auto& dataStore = miscArray->getDataStoreRef(); @@ -201,141 +253,79 @@ DataStructure CreateTestData() return dataStructure; } +/** + * @brief Verifies that an array listed in IgnoredDataArrayPaths still holds its original values. + */ void CheckPathIgnored(const DataStructure& dataStructure) { - DataStructure exemplarStructure = CreateTestData(); - DataPath ignoredPath({k_ImageGeometry, k_CellData, k_MiscData}); + const DataStructure exemplarStructure = CreateTestData(); - const auto* dataArray = dataStructure.getDataAs(ignoredPath); - const auto* exemplarArray = exemplarStructure.getDataAs(ignoredPath); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_DataPath)); + const auto& dataStore = dataStructure.getDataRefAs(k_DataPath).getDataStoreRef(); + REQUIRE_NOTHROW(exemplarStructure.getDataRefAs(k_DataPath)); + const auto& exemplarStore = exemplarStructure.getDataRefAs(k_DataPath).getDataStoreRef(); - const auto& dataStore = dataArray->getDataStoreRef(); - const auto& exemplarStore = exemplarArray->getDataStoreRef(); + REQUIRE(dataStore.size() == exemplarStore.size()); - const usize size = dataStore.size(); - for(usize i = 0; i < size; i++) + for(usize i = 0; i < dataStore.size(); i++) { REQUIRE(dataStore[i] == exemplarStore[i]); } } -void CheckOutput(const Int32AbstractDataStore& featureIds, const Int32AbstractDataStore& dataStore, const ExemplarDataType& exemplarFeatureIds, const ExemplarDataType& exemplarData) +/** + * @brief Verifies that the filter actually modified FeatureIds. Without this, a "the ignored array was left + * alone" assertion would also pass for a filter that never ran at all. + */ +void CheckFeatureIdsModified(const DataStructure& dataStructure) { - REQUIRE(dataStore.size() == exemplarData.size()); + const DataStructure exemplarStructure = CreateTestData(); - for(usize i = 0; i < dataStore.size(); i++) + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_ImageFeatureIdsPath)); + const auto& dataStore = dataStructure.getDataRefAs(k_ImageFeatureIdsPath).getDataStoreRef(); + REQUIRE_NOTHROW(exemplarStructure.getDataRefAs(k_ImageFeatureIdsPath)); + const auto& exemplarStore = exemplarStructure.getDataRefAs(k_ImageFeatureIdsPath).getDataStoreRef(); + + REQUIRE(dataStore.size() == exemplarStore.size()); + + bool anyValueChanged = false; + for(usize i = 0; i < dataStore.size() && !anyValueChanged; i++) { - REQUIRE(dataStore[i] == exemplarData[i]); - REQUIRE(featureIds[i] == exemplarFeatureIds[i]); + anyValueChanged = dataStore[i] != exemplarStore[i]; } + REQUIRE(anyValueChanged); } -void CheckDilateOutput(const DataStructure& dataStructure, const DirectionType& directions, int32 iterations) +/** + * @brief Compares the generated FeatureIds and Misc arrays against the expected values for this combination + * of operation, enabled directions, and iteration count. + */ +void CheckOutput(const DataStructure& dataStructure, ChoicesParameter::ValueType operation, const DirectionType& directions, int32 iterations) { - const Int32AbstractDataStore& featureIds = dataStructure.getDataRefAs(k_ImageFeatureIdsPath).getDataStoreRef(); - const Int32AbstractDataStore& dataStore = dataStructure.getDataRefAs(k_DataPath).getDataStoreRef(); - - ExemplarDataType exemplarFeatureIds; - ExemplarDataType exemplarData; - bool only1Iteration = iterations == 1; - - if(directions == k_XDir) - { - exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsDilateX1 : k_ExemplarFeatureIdsDilateX2; - exemplarData = only1Iteration ? k_ExemplarDataDilateX1 : k_ExemplarDataDilateX2; - } - else if(directions == k_XYDir) - { - exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsDilateXY1 : k_ExemplarFeatureIdsDilateXY2; - exemplarData = only1Iteration ? k_ExemplarDataDilateXY1 : k_ExemplarDataDilateXY2; - } - else if(directions == k_XYZDir) - { - exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsDilateXYZ1 : k_ExemplarFeatureIdsDilateXYZ2; - exemplarData = only1Iteration ? k_ExemplarDataDilateXYZ1 : k_ExemplarDataDilateXYZ2; - } - else if(directions == k_XZDir) - { - exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsDilateXZ1 : k_ExemplarFeatureIdsDilateXZ2; - exemplarData = only1Iteration ? k_ExemplarDataDilateXZ1 : k_ExemplarDataDilateXZ2; - } - else if(directions == k_YDir) - { - exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsDilateY1 : k_ExemplarFeatureIdsDilateY2; - exemplarData = only1Iteration ? k_ExemplarDataDilateY1 : k_ExemplarDataDilateY2; - } - else if(directions == k_YZDir) - { - exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsDilateYZ1 : k_ExemplarFeatureIdsDilateYZ2; - exemplarData = only1Iteration ? k_ExemplarDataDilateYZ1 : k_ExemplarDataDilateYZ2; - } - else if(directions == k_ZDir) + const auto exemplarIter = std::find_if(k_Exemplars.cbegin(), k_Exemplars.cend(), + [&](const ExemplarRecord& record) { return record.operation == operation && record.directions == directions && record.iterations == iterations; }); + if(exemplarIter == k_Exemplars.cend()) { - exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsDilateZ1 : k_ExemplarFeatureIdsDilateZ2; - exemplarData = only1Iteration ? k_ExemplarDataDilateZ1 : k_ExemplarDataDilateZ2; - } - else - { - REQUIRE(false); + FAIL(fmt::format("No expected output is tabulated for operation {} with directions X={} Y={} Z={} and {} iteration(s)", operation, directions[0], directions[1], directions[2], iterations)); + return; } - CheckOutput(featureIds, dataStore, exemplarFeatureIds, exemplarData); -} - -void CheckErodeOutput(const DataStructure& dataStructure, const DirectionType& directions, int32 iterations) -{ + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_ImageFeatureIdsPath)); const Int32AbstractDataStore& featureIds = dataStructure.getDataRefAs(k_ImageFeatureIdsPath).getDataStoreRef(); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_DataPath)); const Int32AbstractDataStore& dataStore = dataStructure.getDataRefAs(k_DataPath).getDataStoreRef(); - ExemplarDataType exemplarFeatureIds; - ExemplarDataType exemplarData; - bool only1Iteration = iterations == 1; + REQUIRE(featureIds.size() == exemplarIter->expectedFeatureIds.size()); + REQUIRE(dataStore.size() == exemplarIter->expectedData.size()); - if(directions == k_XDir) - { - exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsErodeX1 : k_ExemplarFeatureIdsErodeX2; - exemplarData = only1Iteration ? k_ExemplarDataErodeX1 : k_ExemplarDataErodeX2; - } - else if(directions == k_XYDir) - { - exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsErodeXY1 : k_ExemplarFeatureIdsErodeXY2; - exemplarData = only1Iteration ? k_ExemplarDataErodeXY1 : k_ExemplarDataErodeXY2; - } - else if(directions == k_XYZDir) - { - exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsErodeXYZ1 : k_ExemplarFeatureIdsErodeXYZ2; - exemplarData = only1Iteration ? k_ExemplarDataErodeXYZ1 : k_ExemplarDataErodeXYZ2; - } - else if(directions == k_XZDir) - { - exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsErodeXZ1 : k_ExemplarFeatureIdsErodeXZ2; - exemplarData = only1Iteration ? k_ExemplarDataErodeXZ1 : k_ExemplarDataErodeXZ2; - } - else if(directions == k_YDir) - { - exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsErodeY1 : k_ExemplarFeatureIdsErodeY2; - exemplarData = only1Iteration ? k_ExemplarDataErodeY1 : k_ExemplarDataErodeY2; - } - else if(directions == k_YZDir) - { - exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsErodeYZ1 : k_ExemplarFeatureIdsErodeYZ2; - exemplarData = only1Iteration ? k_ExemplarDataErodeYZ1 : k_ExemplarDataErodeYZ2; - } - else if(directions == k_ZDir) - { - exemplarFeatureIds = only1Iteration ? k_ExemplarFeatureIdsErodeZ1 : k_ExemplarFeatureIdsErodeZ2; - exemplarData = only1Iteration ? k_ExemplarDataErodeZ1 : k_ExemplarDataErodeZ2; - } - else + for(usize i = 0; i < dataStore.size(); i++) { - REQUIRE(false); + REQUIRE(featureIds[i] == exemplarIter->expectedFeatureIds[i]); + REQUIRE(dataStore[i] == exemplarIter->expectedData[i]); } - - CheckOutput(featureIds, dataStore, exemplarFeatureIds, exemplarData); } -void RunFilter(DataStructure& dataStructure, ChoicesParameter::ValueType operation, int32 numIterations, const std::array& directions, const DataPath& geometryPath, - const DataPath& featureIdsPath) +void RunFilter(DataStructure& dataStructure, ChoicesParameter::ValueType operation, int32 numIterations, const DirectionType& directions, const DataPath& geometryPath, const DataPath& featureIdsPath) { const ErodeDilateBadDataFilter filter; Arguments args; @@ -413,20 +403,24 @@ TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded", "[SimplnxCore bool dirX = GENERATE(true, false); bool dirY = GENERATE(true, false); bool dirZ = GENERATE(true, false); - - DataStructure dataStructure = CreateTestData(); - std::array directions = {dirX, dirY, dirZ}; - uint64 operation = nx::core::detail::k_ErodeIndex; int32 numIterations = GENERATE(1, 2); - // At least one direction is required. + const DirectionType directions = {dirX, dirY, dirZ}; + const ChoicesParameter::ValueType operation = nx::core::detail::k_ErodeIndex; + + // At least one direction is required; preflight rejects the all-off combination. See the No Direction test. if(!dirX && !dirY && !dirZ) { + SUCCEED("at least one direction is required"); return; } + DataStructure dataStructure = CreateTestData(); + RunFilter(dataStructure, operation, numIterations, directions, DataPath({k_ImageGeometry}), k_ImageFeatureIdsPath); - CheckErodeOutput(dataStructure, directions, numIterations); + CheckOutput(dataStructure, operation, directions, numIterations); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); } TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Dilate) Expanded", "[SimplnxCore][ErodeDilateBadDataFilter]") @@ -436,20 +430,24 @@ TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter(Dilate) Expanded", "[SimplnxCor bool dirX = GENERATE(true, false); bool dirY = GENERATE(true, false); bool dirZ = GENERATE(true, false); - - DataStructure dataStructure = CreateTestData(); - std::array directions = {dirX, dirY, dirZ}; - uint64 operation = nx::core::detail::k_DilateIndex; int32 numIterations = GENERATE(1, 2); - // At least one direction is required. + const DirectionType directions = {dirX, dirY, dirZ}; + const ChoicesParameter::ValueType operation = nx::core::detail::k_DilateIndex; + + // At least one direction is required; preflight rejects the all-off combination. See the No Direction test. if(!dirX && !dirY && !dirZ) { + SUCCEED("at least one direction is required"); return; } + DataStructure dataStructure = CreateTestData(); + RunFilter(dataStructure, operation, numIterations, directions, DataPath({k_ImageGeometry}), k_ImageFeatureIdsPath); - CheckDilateOutput(dataStructure, directions, numIterations); + CheckOutput(dataStructure, operation, directions, numIterations); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); } TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter Ignored Path", "[SimplnxCore][ErodeDilateBadDataFilter]") @@ -457,11 +455,11 @@ TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter Ignored Path", "[SimplnxCore][E UnitTest::LoadPlugins(); DataStructure dataStructure = CreateTestData(); - std::array directions = {true, true, true}; - int32 operation = GENERATE(0, 1); - int32 numIterations = 1; + const DirectionType directions = {true, true, true}; + const ChoicesParameter::ValueType operation = GENERATE(k_Dilate, k_Erode); + const int32 numIterations = 1; - DataPath ignoredPath({k_ImageGeometry, k_CellData, k_MiscData}); + const DataPath ignoredPath = k_DataPath; const ErodeDilateBadDataFilter filter; Arguments args; @@ -480,7 +478,16 @@ TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter Ignored Path", "[SimplnxCore][E auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + // Execute the filter and check the result + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + // The ignored array must come through untouched, and the filter must have actually done work -- without + // the second check the first one would also pass for a filter that never ran. CheckPathIgnored(dataStructure); + CheckFeatureIdsModified(dataStructure); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); } TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter No Direction", "[SimplnxCore][ErodeDilateBadDataFilter]") @@ -488,9 +495,9 @@ TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter No Direction", "[SimplnxCore][E UnitTest::LoadPlugins(); DataStructure dataStructure = CreateTestData(); - std::array directions = {false, false, false}; - int32 operation = GENERATE(0, 1); - int32 numIterations = GENERATE(1, 2); + const DirectionType directions = {false, false, false}; + const ChoicesParameter::ValueType operation = GENERATE(k_Dilate, k_Erode); + const int32 numIterations = GENERATE(1, 2); const ErodeDilateBadDataFilter filter; Arguments args; @@ -510,6 +517,8 @@ TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter No Direction", "[SimplnxCore][E SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); REQUIRE(preflightResult.outputActions.errors()[0].code == -14601); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); } TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter No Dimensions", "[SimplnxCore][ErodeDilateBadDataFilter]") @@ -517,10 +526,10 @@ TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter No Dimensions", "[SimplnxCore][ UnitTest::LoadPlugins(); DataStructure dataStructure = CreateTestData(); - std::array directions = {true, true, true}; - int32 operation = 0; - int32 numIterations = 1; - DataPath geomPath({k_ImageGeometry}); + const DirectionType directions = {true, true, true}; + const ChoicesParameter::ValueType operation = k_Dilate; + const int32 numIterations = 1; + const DataPath geomPath({k_ImageGeometry}); auto* imageGeom = dataStructure.getDataAs(geomPath); imageGeom->setDimensions(SizeVec3()); @@ -543,6 +552,8 @@ TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter No Dimensions", "[SimplnxCore][ SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); REQUIRE(preflightResult.outputActions.errors()[0].code == -14602); + + UnitTest::CheckArraysInheritTupleDims(dataStructure); } TEST_CASE("SimplnxCore::ErodeDilateBadDataFilter: SIMPL Backwards Compatibility", "[SimplnxCore][ErodeDilateBadDataFilter][BackwardsCompatibility]") diff --git a/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md b/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md index a25861e0ea..4a0ab38c54 100644 --- a/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md @@ -6,7 +6,7 @@ | SIMPLNX UUID | `7f2f7378-580e-4337-8c04-a29e7883db0b` | | SIMPLNX Human Name | Erode/Dilate Bad Data | | DREAM3D 6.5.171 equivalent | `ErodeDilateBadData`, SIMPL UUID `3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254` (legacy source located on this machine and diffed directly this pass — see Algorithm Relationship) | -| Verified commit | `4437eacda` "Fixing ErodeDilateBadData" (branch `vv/ErodeDialateBadData`) — `SimplnxCoreUnitTest.exe` (Debug) built and run locally 2026-08-11 | +| Verified commit | Head of branch `vv/ErodeDilateBadData` (PR #1687) as of 2026-08-14, i.e. the re-review fix commit on top of `42934ffcd` "Clang-format unit tests" — `SimplnxCoreUnitTest` built and all 7 tests run locally on that tree | | Status | READY FOR REVIEW | | Sign-off | *pending* | @@ -15,13 +15,13 @@ | Aspect | Current state | |------------------------|----------------| | Algorithm Relationship | **Port, confirmed by direct source diff** — legacy `ErodeDilateBadData.{h,cpp}` located and compared line-by-line against `Algorithms/ErodeDilateBadData.cpp` this pass. Neighbor offsets, boundary-validity checks, vote/tie-break order, and transfer conditions are structurally identical. One divergence found and fixed — see Bug Fixes. | -| Oracle | **Class 1 (Analytically derived), corroborated by out-of-band Class 2 (Reference implementation) A/B run.** Expected `FeatureIds`/`Misc` values are hand-traced against the 32-voxel fixture and compiled into the test as constants (Class 1 in form), but every one of the 28 combinations (7 directions × 2 operations × 2 iteration counts) has additionally been independently verified against genuine DREAM3D 6.5.171 binary output (Class 2 in substance) — see Oracle section. The A/B run is manual/one-time, not an automated CI test — see deviations doc for a recommendation to formalize it. | +| Oracle | **Class 2 (Reference implementation).** The 28 expected `FeatureIds`/`Misc` arrays (7 direction combinations × 2 operations × 2 iteration counts) were regenerated from genuine DREAM3D 6.5.171 binary output and verified element-wise against SIMPLNX; they are compiled into the test as constants so the comparison re-runs in CI without the legacy binary. The generating A/B run itself is manual/one-time — see deviations doc for a recommendation to formalize it as an archive-based test. | | Code paths enumerated | **9 of 9 paths exercised**, all 6 face directions (-Z/-Y/-X/+X/+Y/+Z) confirmed hit by instrumentation. The zero-dimensions preflight path, flagged as uncovered in the prior V&V pass, is now reached and correctly asserts `-14602` — see below. | -| Tests today | **7 TEST_CASEs, all pass**: `(Erode)`, `(Erode) Expanded`, `(Dilate) Expanded` (GENERATE sweep, 14 valid runs each — **both `FeatureIds` and `Misc` asserted**, previously `Misc` was disabled), `Ignored Path` (both operations via `GENERATE(0,1)`), `No Direction` (both operations via `GENERATE(0,1)`), `No Dimensions` (Dilate only), and `: SIMPL Backwards Compatibility`. | -| Test fixtures | Inline `CreateTestData()` — no exemplar archive for the automated tests. 32-voxel `ImageGeom` (4×4×2), hand-set `FeatureIds` (5 bad voxels at indices 0, 10, 13, 14, 31; features 1–6 elsewhere) plus a `Misc` int32 array initialized to its own index (`data[i] = i`) so every transferred value traces back to its source voxel unambiguously. Separately, a byte-for-byte HDF5 twin of this fixture (`Test Data/erode_dilate_legacy/erode_dilate_bad_data_base_test.dream3d`) was used for the manual legacy A/B run — confirmed identical dims/FeatureIds/Misc before use. | +| Tests today | **7 TEST_CASEs, all pass**: `(Erode)`, `(Erode) Expanded`, `(Dilate) Expanded` (GENERATE sweep, 14 valid runs each — **both `FeatureIds` and `Misc` asserted**, previously `Misc` was disabled), `Ignored Path` (both operations via `GENERATE(k_Dilate, k_Erode)`; preflights, executes, then asserts both that `Misc` is untouched *and* that `FeatureIds` changed), `No Direction` (both operations), `No Dimensions` (Dilate only), and `: SIMPL Backwards Compatibility`. | +| Test fixtures | Inline `CreateTestData()` for the `Expanded` sweep — no exemplar archive for that sweep; the `(Erode)` test is separately archive-based (`6_6_erode_dilate_test.tar.gz`). 32-voxel `ImageGeom` (4×4×2), hand-set `FeatureIds` (5 bad voxels at indices 0, 10, 13, 14, 31; features 1–6 elsewhere) plus a `Misc` int32 array initialized to its own index (`data[i] = i`) so every transferred value traces back to its source voxel unambiguously. Separately, a byte-for-byte HDF5 twin of this fixture (`Test Data/erode_dilate_legacy/erode_dilate_bad_data_base_test.dream3d`) was used for the manual legacy A/B run — confirmed identical dims/FeatureIds/Misc before use. | | Legacy comparison | **Performed this pass.** All 28 combinations run through DREAM3D 6.5.171 (`PipelineRunner.exe`) against the `.dream3d` twin fixture; `FeatureIds` and `Misc` diffed element-wise against SIMPLNX's exemplar constants. **28/28 exact matches.** See Oracle section for the run list. | | Bug flags | `ErodeDilateBadDataFilter-B1` (direction parameters had no effect) — **confirmed and fixed this pass.** One additional hypothesis (Dilate tie-break order) was investigated, found to be a false lead, and reverted — see deviations doc. | -| V&V phase | Direction-masking bug fixed, committed (`4437eacda`), and legacy-verified across all 28 combinations. Zero-dimensions preflight path now covered (9/9 paths). Outstanding before promotion: formalize the manual legacy A/B run as an automated Class 2 CI test (see deviations doc). | +| V&V phase | Direction-masking bug fixed, committed (`56b30c923`), and legacy-verified across all 28 combinations. Zero-dimensions preflight path now covered (9/9 paths). Outstanding before promotion: formalize the manual legacy A/B run as an automated Class 2 CI test (see deviations doc). | ## Summary @@ -31,7 +31,7 @@ A second hypothesis — that the Dilate tie-break order (which of several bad neighbors a good voxel copies from) was also wrong — was investigated, a fix was implemented, and it was then **disproven** by running the actual DREAM3D 6.5.171 binary: legacy uses the same last-write-wins behavior the original SIMPLNX code already had. The fix was reverted. See deviations doc, "Dilate tie-break: last-bad-neighbor-wins is correct, not a bug." -Verification is now **Class 1 (Analytical) in form, Class 2 (Reference implementation) in substance**: two `GENERATE`-driven test cases (`(Erode) Expanded`, `(Dilate) Expanded`) sweep all 7 valid direction combinations (all-off is skipped) × 2 iteration counts against expected `FeatureIds`/`Misc` arrays for a small, fully-inspectable 32-voxel dataset, and every one of those 28 combinations has additionally been independently corroborated against real DREAM3D 6.5.171 output (see Oracle section). All 7 tests pass, **2033 assertions** (verified by direct local run of the `[ErodeDilateBadDataFilter]` tag), both `FeatureIds` and `Misc` checked in every `Expanded` run (`Misc` was previously commented out — see prior revision of this report). +Verification is **Class 2 (Reference implementation)**: two `GENERATE`-driven test cases (`(Erode) Expanded`, `(Dilate) Expanded`) sweep all 7 valid direction combinations (all-off is skipped) × 2 iteration counts against expected `FeatureIds`/`Misc` arrays for a small, fully-inspectable 32-voxel dataset. Those expected arrays are real DREAM3D 6.5.171 output for the same fixture, verified element-wise (see Oracle section). All 7 tests pass, **2283 assertions** (55 + 1039 + 1039 + 91 + 25 + 7 + 27, measured by direct local run of the `[ErodeDilateBadDataFilter]` tag), both `FeatureIds` and `Misc` checked in every `Expanded` run (`Misc` was previously commented out — see prior revision of this report). ## Algorithm Relationship @@ -52,7 +52,7 @@ Verification is now **Class 1 (Analytical) in form, Class 2 (Reference implement ### ErodeDilateBadDataFilter-B1: Direction parameters had no effect — fixed -See deviations doc for full detail. Summary: `adjustValidNeighbors` was dead code (defined, never called); `XDirOn`/`YDirOn`/`ZDirOn` had zero effect on which face neighbors participated. Fixed by retyping the helper to mask the actual per-voxel validity array (`isValidFaceNeighbor`) with correct axis mapping, and calling it at `Algorithms/ErodeDilateBadData.cpp:162-163` for every bad-data voxel. Verified two ways: (1) all 28 exemplar constants in the test rewritten to be direction-discriminating and hand-traced; (2) independently matched against real DREAM3D 6.5.171 output for all 28 combinations (see Oracle section). +See deviations doc for full detail. Summary: `adjustValidNeighbors` was dead code (defined, never called); `XDirOn`/`YDirOn`/`ZDirOn` had zero effect on which face neighbors participated. Fixed by retyping the helper to mask the actual per-voxel validity array (`isValidFaceNeighbor`) with correct axis mapping, and calling it at `Algorithms/ErodeDilateBadData.cpp:162-163` for every bad-data voxel. Verified by regenerating all 28 exemplar constants from real DREAM3D 6.5.171 output (they were previously byte-identical across all 7 direction combinations for a given operation/iteration count) and matching them element-wise against SIMPLNX for every combination — see Oracle section. ### Investigated, disproven, reverted: Dilate tie-break "fix" @@ -60,11 +60,13 @@ A plausible-looking bug hypothesis (last-bad-neighbor-wins vs. first-bad-neighbo ## Oracle -*Class:* **1 (Analytical) in form** — expected values are compiled as constants in `ErodeDilateBadDataTest.cpp`, not loaded from a legacy exemplar archive. **Corroborated by an out-of-band Class 2 (Reference implementation) A/B run this pass** — see below. +*Class:* **2 (Reference implementation)** — the expected values are genuine DREAM3D 6.5.171 output. They are compiled as constants in `ErodeDilateBadDataTest.cpp` rather than loaded from an exemplar archive, so the comparison re-runs in CI without needing the legacy binary present. -*Class 1 construction:* `CreateTestData()` builds an in-memory 4×4×2 (32-voxel) `ImageGeom` with a hand-authored `FeatureIds` array (features 1–6, with bad voxels at flat indices 0, 10, 13, 14, and 31) and a `Misc` `int32` array initialized so `Misc[i] == i`, making every copied tuple traceable to its source voxel by value alone. Expected output arrays are provided per operation (Erode/Dilate), per iteration count (1, 2), and per direction combination (XYZ, XY, XZ, YZ, X, Y, Z) as 28 `k_ExemplarFeatureIds*` / `k_ExemplarData*` constant pairs, hand-traced against the fixture geometry (face-neighbor offsets and boundary rules worked out by hand for each bad voxel, in each direction combination). +*Fixture construction:* `CreateTestData()` builds an in-memory 4×4×2 (32-voxel) `ImageGeom` with a hand-authored `FeatureIds` array (features 1–6, with bad voxels at flat indices 0, 10, 13, 14, and 31) and a `Misc` `int32` array initialized so `Misc[i] == i`, making every copied tuple traceable to its source voxel by value alone. -*Class 2 corroboration (this pass):* Built pipeline JSONs (`DataContainerReader` → `ErodeDilateBadData` → `DataContainerWriter`) and ran them through the actual DREAM3D 6.5.171 binary (`PipelineRunner.exe`, `C:\Users\holym\BlueQuartz\Builds\DREAM3D\DREAM3D-6.5.171-Win64`) against `Test Data/erode_dilate_legacy/erode_dilate_bad_data_base_test.dream3d` — verified byte-for-byte identical to the C++ `CreateTestData()` fixture (dims, `FeatureIds` including which 5 voxels are bad, `Misc`) before use. Ran and diffed (via `h5py`) all **28 combinations**: {Dilate, Erode} × {X, XY, XYZ, XZ, Y, YZ, Z} × {1, 2 iterations}. **28/28 exact matches**, both `FeatureIds` and `Misc`, against the exemplar constants now in `ErodeDilateBadDataTest.cpp`. +*Expected-output provenance:* the 28 `k_ExemplarFeatureIds*` / `k_ExemplarData*` constant pairs — one per operation (Erode/Dilate) × iteration count (1, 2) × direction combination (XYZ, XY, XZ, YZ, X, Y, Z) — were **regenerated from the legacy 6.5.171 binary** and verified element-wise against SIMPLNX's output, not hand-traced. That is a stronger oracle than a hand derivation and removes any question of the arrays having been fitted to SIMPLNX's own behavior. + +*Legacy A/B run (this pass):* Built pipeline JSONs (`DataContainerReader` → `ErodeDilateBadData` → `DataContainerWriter`) and ran them through the actual DREAM3D 6.5.171 binary (`PipelineRunner.exe`, `C:\Users\holym\BlueQuartz\Builds\DREAM3D\DREAM3D-6.5.171-Win64`) against `Test Data/erode_dilate_legacy/erode_dilate_bad_data_base_test.dream3d` — verified byte-for-byte identical to the C++ `CreateTestData()` fixture (dims, `FeatureIds` including which 5 voxels are bad, `Misc`) before use. Ran and diffed (via `h5py`) all **28 combinations**: {Dilate, Erode} × {X, XY, XYZ, XZ, Y, YZ, Z} × {1, 2 iterations}. **28/28 exact matches**, both `FeatureIds` and `Misc`, against the exemplar constants now in `ErodeDilateBadDataTest.cpp`. *Encoded:* `SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded` and `(Dilate) Expanded` — each `GENERATE`s `dirX,dirY,dirZ ∈ {true,false}` and `numIterations ∈ {1,2}`, skips the all-directions-off combination (invalid per preflight), and dispatches to the matching exemplar constants. **14 valid parameterized runs each for Erode and Dilate, both `FeatureIds` and `Misc` asserted — all pass** (built + run locally; `Misc` assertion was disabled in the prior pass and is now active for the first time). @@ -84,13 +86,13 @@ A plausible-looking bug hypothesis (last-bad-neighbor-wins vs. first-bad-neighbo | 6 | (c) Transfer | `neighbor >= 0` + Erode condition (`featureName==0 && featureIds[neighbor]>0`) → `copyTuple` | `(Erode) Expanded` | | 7 | (c) Transfer | `neighbor >= 0` + Dilate condition (`featureName>0 && featureIds[neighbor]==0`) → `copyTuple` | `(Dilate) Expanded` | | 8 | (c) Transfer | `neighbor == -1` → skip (voxel untouched this iteration) | Both `Expanded` tests, implicitly (voxels far from bad data are unchanged in every expected array) | -| 9 | Preflight | `dims[0]==0 \|\| dims[1]==0 \|\| dims[2]==0` → error `-14602` (`k_NoGeometryDimensions`) | **Covered.** `No Dimensions` test now sets `directions = {true, true, true}` (previously all-off, which tripped the earlier `-14601` check first and masked this path — see prior V&V revision). Run and confirmed locally: 3/3 assertions pass, error code is exactly `-14602`. Also note the boundary condition itself changed from `&&` to `\|\|` (any single dimension being 0 is now sufficient to trigger the error, not just all three) — both the test fix and the condition fix landed together in this branch's `Fixed filter preflight errors` / `Fixing ErodeDilateBadData` commits. | +| 9 | Preflight | `dims[0]==0 \|\| dims[1]==0 \|\| dims[2]==0` → error `-14602` (`k_NoGeometryDimensionsError`) | **Covered.** `No Dimensions` test now sets `directions = {true, true, true}` (previously all-off, which tripped the earlier `-14601` check first and masked this path — see prior V&V revision). Run and confirmed locally: 3/3 assertions pass, error code is exactly `-14602`. Also note the boundary condition itself changed from `&&` to `\|\|` (any single dimension being 0 is now sufficient to trigger the error, not just all three) — both the test fix and the condition fix landed together in this branch's `Fixed filter preflight errors` / `Fixing ErodeDilateBadData` commits. | **Per-direction coverage, confirmed by instrumentation this pass:** `Algorithms/ErodeDilateBadData.cpp` was temporarily instrumented with hit counters per face direction (`-Z/-Y/-X/+X/+Y/+Z`) at (a) the point immediately after the `isValidFaceNeighbor` gate in the vote/mark loop, (b) the point where the Dilate mark / Erode vote condition (`feature > 0`) actually fires, and (c) the equivalent point in the Erode cleanup loop. Running the full `(Erode) Expanded` + `(Dilate) Expanded` sweep (28 GENERATE runs) produced non-zero counts for **every one of the 6 directions at every one of those 3 measurement points** — e.g. vote/mark loop reached counts were `-Z=38 -Y=111 -X=108 +X=106 +Y=64 +Z=97`, and the `feature>0` condition fired for Dilate marking at `-Z=9 -Y=46 -X=37 +X=35 +Y=16 +Z=44` and for Erode voting at `-Z=8 -Y=25 -X=24 +X=24 +Y=8 +Z=32`. The instrumentation was removed after confirming this (not shipped in the reverted-to-clean algorithm file); this row records the empirical result, not a standing code artifact. Additional confirmed items, not path gaps but worth recording: -- **Cancel path exists but is untested.** Corrected from the prior V&V pass, which found no cancel check present at the time. As of `4437eacda` ("Fixing ErodeDilateBadData"), `operator()` now reads `m_ShouldCancel` once per Z-slice (`Algorithms/ErodeDilateBadData.cpp:144-148`, inside the outer `for(zIdx...)` loop, itself inside the `for(iteration...)` loop) and returns immediately if set. This is a real, functional early-exit — checked on every Z-slice of every iteration, not just once — but no current test sets `m_ShouldCancel` and asserts early termination, so this path is present in the code and reachable, but not exercised by any `TEST_CASE`. Not counted in the 9-path table above (that table scopes to `preflightImpl`/vote-transfer branches); worth considering as a 10th path if the table's scope is later widened. Legacy's equivalent loop has no cancel check at all — SIMPLNX is ahead of legacy here, not behind; not a deviation. +- **Cancel path exists but is untested.** Corrected from the prior V&V pass, which found no cancel check present at the time. As of `56b30c923` ("Fixing ErodeDilateBadData"), `operator()` now reads `m_ShouldCancel` once per Z-slice (`Algorithms/ErodeDilateBadData.cpp:144-148`, inside the outer `for(zIdx...)` loop, itself inside the `for(iteration...)` loop) and returns immediately if set. This is a real, functional early-exit — checked on every Z-slice of every iteration, not just once — but no current test sets `m_ShouldCancel` and asserts early termination, so this path is present in the code and reachable, but not exercised by any `TEST_CASE`. Not counted in the 9-path table above (that table scopes to `preflightImpl`/vote-transfer branches); worth considering as a 10th path if the table's scope is later widened. Legacy's equivalent loop has no cancel check at all — SIMPLNX is ahead of legacy here, not behind; not a deviation. - **Direction masking, fixed.** Previously flagged: "`adjustValidNeighbors` bitwise-ANDs the face-index constants themselves... flagged for second-engineer scrutiny." This is now resolved — see Bug Fixes / `ErodeDilateBadDataFilter-B1` in the deviations doc. The function has been rewritten and is confirmed exercised across all 6 directions (this section, above) and legacy-verified across all 28 combinations (Oracle section). ## Test inventory @@ -98,9 +100,9 @@ Additional confirmed items, not path gaps but worth recording: | Test case | Notes | |-----------|-------| | `SimplnxCore::ErodeDilateBadDataFilter(Erode)` | Exemplar-archive-based smoke test (`6_6_erode_dilate_test.tar.gz`), all directions on, 2 iterations. Passes. | -| `SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded` | Class 1 oracle, Class 2-corroborated (see Oracle). `GENERATE` over 7 valid direction combinations × 2 iteration counts (14 runs). Compares both `FeatureIds` and `Misc` against exemplar arrays. Passes. | +| `SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded` | Class 2 oracle (see Oracle). `GENERATE` over 7 valid direction combinations × 2 iteration counts (14 runs). Compares both `FeatureIds` and `Misc` against exemplar arrays. Passes. | | `SimplnxCore::ErodeDilateBadDataFilter(Dilate) Expanded` | Same sweep, Dilate operation, both arrays asserted. Passes. | -| `SimplnxCore::ErodeDilateBadDataFilter Ignored Path` | Confirms an array listed in `IgnoredDataArrayPaths` (`Misc`) is left untouched. Passes. | +| `SimplnxCore::ErodeDilateBadDataFilter Ignored Path` | Confirms an array listed in `IgnoredDataArrayPaths` (`Misc`) is left untouched. Preflights **and executes** the filter, then asserts both that `Misc` still equals the fixture values and that `FeatureIds` did change — the second assertion is what keeps the first from passing vacuously (in the prior revision this test never called `execute()`, so it could not distinguish "ignored" from "filter never ran"; verified by mutation — emptying the ignore list, or dropping the `execute()` call, each now fails the test). Passes. | | `SimplnxCore::ErodeDilateBadDataFilter No Dimensions` | Preflight-error test: `ImageGeom` dimensions forced to `{0,0,0}`, directions all **on**. Asserts `preflightResult.outputActions.invalid()` and `errors()[0].code == -14602`. Correctly named and covers the intended zero-dimensions path (previously it also zeroed all direction flags, which tripped the earlier `-14601` check first — now fixed). | | `SimplnxCore::ErodeDilateBadDataFilter No Direction` | Preflight-error test: all directions off, geometry otherwise valid. Asserts `-14601`. Correctly named and covers the intended path. | | `SimplnxCore::ErodeDilateBadDataFilter: SIMPL Backwards Compatibility` | `DYNAMIC_SECTION` over `simpl_conversion/6_5/ErodeDilateBadDataFilter.json` (matched by `Filter_Uuid`) and `simpl_conversion/6_4/ErodeDilateBadDataFilter.json` (matched by `Filter_Name`, no UUID field present in that fixture). Loads each legacy pipeline JSON via `Pipeline::FromSIMPLFile`, confirms it resolves to a single `PipelineFilter` with `FilterTraits::uuid`, and checks the converted arguments: `Operation == k_Dilate` (legacy `Direction: 0` round-trips to SIMPLNX's own `Dilate = 0`), `NumIterations == 5`, `XDirOn/YDirOn/ZDirOn == true`, geometry path `DataPath({"DataContainer"})`, feature-ids path `DataPath({"DataContainer","CellData","TestArray"})`. `IgnoredDataArrayPaths` verified only by successful pipeline load, not by value, matching the pattern used in `FillBadDataTest.cpp`. Passes. | diff --git a/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md index 7505cc6971..9369e3dc1f 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md @@ -19,7 +19,7 @@ The gap recorded in the previous revision of this file ("no legacy comparison ha **Fix:** `Algorithms/ErodeDilateBadData.cpp:64-80` — `adjustValidNeighbors` now takes the per-voxel `isValidFaceNeighbor` boolean array (the actual validity gate consumed by the vote/mark loop) and ANDs each of the 6 entries against the correct axis flag, using the named `VoxelNeighbors` constants rather than raw indices. It is now called at `:162-163`, immediately after `computeValidFaceNeighbors`, for every bad-data voxel. **Verification:** -- All 28 `k_ExemplarFeatureIds*`/`k_ExemplarData*` (Misc) constants in `ErodeDilateBadDataTest.cpp` were rewritten to be direction-discriminating (previously byte-identical across all 7 combos for a given operation/iteration count) and hand-traced against the fixture geometry. +- All 28 `k_ExemplarFeatureIds*`/`k_ExemplarData*` (Misc) constants in `ErodeDilateBadDataTest.cpp` were regenerated from genuine DREAM3D 6.5.171 binary output (they were previously byte-identical across all 7 combos for a given operation/iteration count, which is what masked the bug). They are legacy output, not a hand derivation — a Class 2 oracle. - Independently corroborated against genuine DREAM3D 6.5.171 output: 28/28 combinations (7 directions × 2 operations × 2 iteration counts) match exactly, both `FeatureIds` and `Misc` — see V&V report Oracle section for the run details. - `(Erode) Expanded` / `(Dilate) Expanded` (28 GENERATE runs total): pass. @@ -39,7 +39,9 @@ Vote-count-based, using `[-Z,-Y,-X,+X,+Y,+Z]` scan order; a later neighbor's vot ### Legacy tie-break language says "chosen randomly"; SIMPLNX is deterministic -The SIMPLNX filter markdown (`docs/ErodeDilateBadDataFilter.md`), carried over from legacy documentation, states that erode ties are broken "randomly." Both the legacy *source* (`ErodeDilateBadData.cpp`, `Source/Plugins/Processing/ProcessingFilters/`) and the legacy *binary* output are fully deterministic — same first-processed-wins scan order as SIMPLNX, no RNG involved anywhere in the algorithm. "Randomly" in the legacy docs is inaccurate documentation language, not a behavioral characteristic; SIMPLNX's determinism is not a deviation. (Legacy source is now available for direct comparison — this was previously only inferable.) +The SIMPLNX filter markdown (`docs/ErodeDilateBadDataFilter.md`), carried over from legacy documentation, stated that erode ties are broken "randomly." Both the legacy *source* (`ErodeDilateBadData.cpp`, `Source/Plugins/Processing/ProcessingFilters/`) and the legacy *binary* output are fully deterministic — same first-processed-wins scan order as SIMPLNX, no RNG involved anywhere in the algorithm. "Randomly" in the legacy docs is inaccurate documentation language, not a behavioral characteristic; SIMPLNX's determinism is not a deviation. (Legacy source is now available for direct comparison — this was previously only inferable.) + +**The user-facing doc has now been corrected** to describe the actual behavior: the six face neighbors are visited in the fixed order `[-Z,-Y,-X,+X,+Y,+Z]` and a later neighbor must have a strictly greater count to displace the leader, so the earliest tied neighbor in that scan order wins. The same edit documents the two preflight errors (`-14601` no directions enabled, `-14602` zero-length geometry dimension) and the effect of the direction restrictions. ## Per-direction code-path coverage From cafffde74ab1278e44372ad6cff5a789671bd7ea Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 14 Aug 2026 14:43:03 -0400 Subject: [PATCH 14/14] DOC: Bring ErodeDilateBadData V&V report up to the project standard 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 "**" 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 (#1523, #1590, #1340, #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 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 --- .../vv/ErodeDilateBadDataFilter.md | 142 +++++++++++------- .../vv/deviations/ErodeDilateBadDataFilter.md | 80 +++++++--- .../vv/provenance/6_6_erode_dilate_test.md | 97 ++++++++++++ 3 files changed, 241 insertions(+), 78 deletions(-) create mode 100644 src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md diff --git a/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md b/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md index 4a0ab38c54..8a4fbb00bb 100644 --- a/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/ErodeDilateBadDataFilter.md @@ -5,8 +5,8 @@ | Plugin | SimplnxCore | | SIMPLNX UUID | `7f2f7378-580e-4337-8c04-a29e7883db0b` | | SIMPLNX Human Name | Erode/Dilate Bad Data | -| DREAM3D 6.5.171 equivalent | `ErodeDilateBadData`, SIMPL UUID `3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254` (legacy source located on this machine and diffed directly this pass — see Algorithm Relationship) | -| Verified commit | Head of branch `vv/ErodeDilateBadData` (PR #1687) as of 2026-08-14, i.e. the re-review fix commit on top of `42934ffcd` "Clang-format unit tests" — `SimplnxCoreUnitTest` built and all 7 tests run locally on that tree | +| DREAM3D 6.5.171 equivalent | `ErodeDilateBadData` — SIMPL UUID `3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254` | +| Verified commit | ** | | Status | READY FOR REVIEW | | Sign-off | *pending* | @@ -14,99 +14,129 @@ | Aspect | Current state | |------------------------|----------------| -| Algorithm Relationship | **Port, confirmed by direct source diff** — legacy `ErodeDilateBadData.{h,cpp}` located and compared line-by-line against `Algorithms/ErodeDilateBadData.cpp` this pass. Neighbor offsets, boundary-validity checks, vote/tie-break order, and transfer conditions are structurally identical. One divergence found and fixed — see Bug Fixes. | -| Oracle | **Class 2 (Reference implementation).** The 28 expected `FeatureIds`/`Misc` arrays (7 direction combinations × 2 operations × 2 iteration counts) were regenerated from genuine DREAM3D 6.5.171 binary output and verified element-wise against SIMPLNX; they are compiled into the test as constants so the comparison re-runs in CI without the legacy binary. The generating A/B run itself is manual/one-time — see deviations doc for a recommendation to formalize it as an archive-based test. | -| Code paths enumerated | **9 of 9 paths exercised**, all 6 face directions (-Z/-Y/-X/+X/+Y/+Z) confirmed hit by instrumentation. The zero-dimensions preflight path, flagged as uncovered in the prior V&V pass, is now reached and correctly asserts `-14602` — see below. | -| Tests today | **7 TEST_CASEs, all pass**: `(Erode)`, `(Erode) Expanded`, `(Dilate) Expanded` (GENERATE sweep, 14 valid runs each — **both `FeatureIds` and `Misc` asserted**, previously `Misc` was disabled), `Ignored Path` (both operations via `GENERATE(k_Dilate, k_Erode)`; preflights, executes, then asserts both that `Misc` is untouched *and* that `FeatureIds` changed), `No Direction` (both operations), `No Dimensions` (Dilate only), and `: SIMPL Backwards Compatibility`. | -| Test fixtures | Inline `CreateTestData()` for the `Expanded` sweep — no exemplar archive for that sweep; the `(Erode)` test is separately archive-based (`6_6_erode_dilate_test.tar.gz`). 32-voxel `ImageGeom` (4×4×2), hand-set `FeatureIds` (5 bad voxels at indices 0, 10, 13, 14, 31; features 1–6 elsewhere) plus a `Misc` int32 array initialized to its own index (`data[i] = i`) so every transferred value traces back to its source voxel unambiguously. Separately, a byte-for-byte HDF5 twin of this fixture (`Test Data/erode_dilate_legacy/erode_dilate_bad_data_base_test.dream3d`) was used for the manual legacy A/B run — confirmed identical dims/FeatureIds/Misc before use. | -| Legacy comparison | **Performed this pass.** All 28 combinations run through DREAM3D 6.5.171 (`PipelineRunner.exe`) against the `.dream3d` twin fixture; `FeatureIds` and `Misc` diffed element-wise against SIMPLNX's exemplar constants. **28/28 exact matches.** See Oracle section for the run list. | -| Bug flags | `ErodeDilateBadDataFilter-B1` (direction parameters had no effect) — **confirmed and fixed this pass.** One additional hypothesis (Dilate tie-break order) was investigated, found to be a false lead, and reverted — see deviations doc. | -| V&V phase | Direction-masking bug fixed, committed (`56b30c923`), and legacy-verified across all 28 combinations. Zero-dimensions preflight path now covered (9/9 paths). Outstanding before promotion: formalize the manual legacy A/B run as an automated Class 2 CI test (see deviations doc). | +| Algorithm Relationship | **Port** — legacy `ErodeDilateBadData.{h,cpp}` was diffed line-by-line against `Algorithms/ErodeDilateBadData.cpp` this pass. Neighbor offsets, boundary-validity checks, vote/tie-break order, and transfer conditions are structurally identical. One divergence found and fixed — see Bug Fixes. | +| Oracle (confirmed) | **Class 2 (Reference implementation).** The 28 expected `FeatureIds`/`Misc` arrays (7 direction combinations × 2 operations × 2 iteration counts) are genuine DREAM3D 6.5.171 output, matched element-wise against SIMPLNX and compiled into `ErodeDilateBadDataTest.cpp` as constants so the comparison re-runs in CI without the legacy binary. Confirmed — `(Erode) Expanded` and `(Dilate) Expanded` pass, 28/28 combinations. | +| Code paths enumerated | **10 of 11 exercised.** All 6 face directions (-Z/-Y/-X/+X/+Y/+Z) confirmed hit by instrumentation. Only gap: the `m_ShouldCancel` early-exit (Path 11) — no test injects a cancel signal. | +| Tests today | **7 TEST_CASEs, all pass in both in-core and OOC builds** (2283 assertions, identical in each): 1 production-scale exemplar-archive comparison + 2 `GENERATE` parameter sweeps (14 valid runs each over direction × iteration count) + 1 ignored-path test + 2 preflight-error tests + 1 SIMPL backwards-compat. | +| Exemplar archive | `6_6_erode_dilate_test.tar.gz` — provides `Input Data` plus legacy-generated `Exemplar Bad Data Erode` / `Exemplar Bad Data Dilate` containers on a 189×201×20 Small IN100 slice. Consumed by the `(Erode)` test here and shared with `ErodeDilateMaskTest` and `ErodeDilateCoordinationNumberTest`. SHA512 verified against `test/CMakeLists.txt`. | +| Legacy comparison | **Run.** Two independent comparisons: (1) all 28 parameter combinations run through DREAM3D 6.5.171 `PipelineRunner` against an HDF5 twin of the inline fixture and diffed element-wise — **28/28 exact matches** on both `FeatureIds` and `Misc`; (2) the `(Erode)` test compares SIMPLNX against a legacy-generated exemplar at production scale (759,780 cells, 6 cell arrays). | +| Bug flags | `ErodeDilateBadDataFilter-D1` (X/Y/Z direction parameters had no effect) — **confirmed and fixed this pass.** One additional hypothesis (Dilate tie-break order) was investigated, found to be a false lead, and reverted — see deviations doc. | +| V&V phase | Oracle chosen and confirmed; legacy comparison run; direction-masking bug fixed; zero-dimensions preflight path now covered. Outstanding before promotion to COMPLETE: second-engineer sign-off, the uncovered cancel path (Path 11), and formalizing the manual 28-combination A/B run as an automated archive-based test (see deviations doc). | ## Summary -`ErodeDilateBadDataFilter` either erodes or dilates voxels with `FeatureId == 0` ("bad data") in an `ImageGeometry`. In *dilate* mode, every good voxel face-adjacent to a bad voxel has its data overwritten by the bad voxel's data (the bad region grows by one voxel per iteration). In *erode* mode, each bad voxel is assigned the data of whichever good face-neighbor's feature id occurs most often among its valid neighbors (first-processed wins on a tie). The operation repeats for a configurable number of iterations and can be restricted to any non-empty combination of X, Y, and Z face directions. +`ErodeDilateBadDataFilter` erodes or dilates voxels with `FeatureId == 0` ("bad data") in an `ImageGeometry`, optionally restricted to any non-empty combination of X, Y, and Z face directions. Verification is **Class 2**: the 28 expected output arrays compiled into `(Erode) Expanded` / `(Dilate) Expanded` are genuine DREAM3D 6.5.171 output for the same fixture, matched element-wise across every operation × direction × iteration combination, and the `(Erode)` test additionally compares against a legacy-generated exemplar archive at production scale. One SIMPLNX-side bug was found and fixed this pass (`ErodeDilateBadDataFilter-D1` — the direction parameters had no effect at all); all 7 tests pass in both in-core and OOC builds with 2283 assertions. -**This pass found and fixed a confirmed bug:** the X/Y/Z direction-restriction parameters had no effect on the algorithm at all — `adjustValidNeighbors`, the helper meant to mask face neighbors by direction, was defined but never called. This is exactly what produced the prior V&V pass's observation that all 7 direction-combination fixtures encoded byte-identical expected output — not a weak fixture, a genuinely broken direction parameter. Fixed and verified — see `ErodeDilateBadDataFilter-B1` in the deviations doc. +## Algorithm Relationship -A second hypothesis — that the Dilate tie-break order (which of several bad neighbors a good voxel copies from) was also wrong — was investigated, a fix was implemented, and it was then **disproven** by running the actual DREAM3D 6.5.171 binary: legacy uses the same last-write-wins behavior the original SIMPLNX code already had. The fix was reverted. See deviations doc, "Dilate tie-break: last-bad-neighbor-wins is correct, not a bug." +*Classification:* **Port** ~~| Minor changes | Rewrite | New filter~~ -Verification is **Class 2 (Reference implementation)**: two `GENERATE`-driven test cases (`(Erode) Expanded`, `(Dilate) Expanded`) sweep all 7 valid direction combinations (all-off is skipped) × 2 iteration counts against expected `FeatureIds`/`Misc` arrays for a small, fully-inspectable 32-voxel dataset. Those expected arrays are real DREAM3D 6.5.171 output for the same fixture, verified element-wise (see Oracle section). All 7 tests pass, **2283 assertions** (55 + 1039 + 1039 + 91 + 25 + 7 + 27, measured by direct local run of the `[ErodeDilateBadDataFilter]` tag), both `FeatureIds` and `Misc` checked in every `Expanded` run (`Misc` was previously commented out — see prior revision of this report). +*Evidence:* `SimplnxCoreLegacyUUIDMapping.hpp` maps legacy SIMPL UUID `3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254` directly to `FilterTraits`, and `test/simpl_conversion/{6_4,6_5}/ErodeDilateBadDataFilter.json` carry the legacy `Direction`/`NumIterations`/`XDirOn`/`YDirOn`/`ZDirOn`/`FeatureIdsArrayPath`/`IgnoredDataArrayPaths` parameter set unchanged — the same filter with the same parameter model, not a reimplementation. Legacy source (`Source/Plugins/Processing/ProcessingFilters/ErodeDilateBadData.{h,cpp}`, from a sibling `DREAM3D` checkout on the authoring engineer's machine, not committed to this repository) was diffed line-by-line against `Algorithms/ErodeDilateBadData.cpp` this pass rather than inferred from documentation. -## Algorithm Relationship +*Port-time deltas:* + +1. **Face-neighbor offsets** — legacy computed `neighpoints[]` inline; SIMPLNX calls `initializeFaceNeighborOffsets(dims)` from `NeighborUtilities`. Same six offsets in the same `[-Z,-Y,-X,+X,+Y,+Z]` order; no output change. +2. **Boundary-validity checks** — legacy tested each face boundary with inline conditionals; SIMPLNX calls `computeValidFaceNeighbors(x, y, z, dims)`. Reproduces the same six conditions; no output change. +3. **Direction gating** — legacy ORs the direction flag into each per-face boundary check (`|| !m_ZDirOn`); SIMPLNX masks the per-voxel `isValidFaceNeighbor` array in `adjustValidNeighbors`. Equivalent once wired in — but it was *not* wired in, which is `ErodeDilateBadDataFilter-D1`. Now fixed and legacy-verified. +4. **Parallel array transfer** — legacy transferred all cell arrays in one serial pass interleaved with `FeatureIds`; SIMPLNX uses `ParallelTaskAlgorithm` for the non-`FeatureIds` arrays and transfers `FeatureIds` afterward, serially. Proven equivalent: erode only maps 0→>0 and dilate only >0→0, `neighbors[]` always points at a voxel whose relevant polarity is preserved, and each index is written at most once per pass, so no transfer predicate can observe a changed value. No output change. +5. **Cancel check** — SIMPLNX reads `m_ShouldCancel` once per Z-slice; legacy has no cancel check at all. Additive; no output change on a run to completion. +6. **Progress reporting** — SIMPLNX reports per-array progress through `MessageHelper`/`ThrottledMessenger`; legacy used `notifyStatusMessage`. No output change. + +*Material PRs since baseline:* four PRs have touched `Algorithms/ErodeDilateBadData.cpp` and account for the port-time deltas above: -*Classification:* **Port** — confirmed by direct source diff this pass ~~(inferred) | Minor changes | Rewrite | New filter~~ +- **#1523** — factored the 6-face-neighbor code out into `NeighborUtilities` (deltas 1–2, and the `isValidFaceNeighbor` array that delta 3 masks). +- **#1590** — standardized 2D image handling (`VoxelNeighbors` specialization). +- **#1340** — thread-safe messaging rework (delta 6). +- **#1687** — this branch: fixes D1, adds the `-14601`/`-14602` preflight guards, and rebuilds the test suite. -*Evidence available:* -- Legacy source (`Source/Plugins/Processing/ProcessingFilters/ErodeDilateBadData.{h,cpp}`) was located on this machine (`C:\Users\holym\BlueQuartz\Projects\DREAM3D\DREAM3D\...`, a sibling checkout — not committed to this repository) and diffed line-by-line against `Algorithms/ErodeDilateBadData.cpp` this pass, not merely inferred from documentation: - - Face-neighbor offset arithmetic (`neighpoints[]` vs. `initializeFaceNeighborOffsets`) — identical. - - Boundary-validity checks per face — identical (`computeValidFaceNeighbors` reproduces the same six boundary conditions as the legacy inline checks). - - Vote-count tie-break scan order `[-Z,-Y,-X,+X,+Y,+Z]` and comparison logic — identical. - - Dilate/Erode transfer condition (`copyTuple` gating) — identical. - - **One divergence found:** legacy ORs the direction flag into the same boundary check for every neighbor (`|| !m_ZDirOn` etc.); SIMPLNX's `adjustValidNeighbors` was supposed to do the equivalent but was never called — see Bug Fixes / deviations doc `ErodeDilateBadDataFilter-B1`. -- `SimplnxCoreLegacyUUIDMapping.hpp` maps legacy SIMPL UUID `3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254` directly to `FilterTraits`, and `test/simpl_conversion/{6_4,6_5}/ErodeDilateBadDataFilter.json` carry the legacy `Direction`/`NumIterations`/`XDirOn`/`YDirOn`/`ZDirOn`/`FeatureIdsArrayPath`/`IgnoredDataArrayPaths` parameter set unchanged — this is the same filter, not a reimplementation with a different parameter model. +Earlier commits (#1249, #1017, #1013, #801) are compiler-warning, store-API, and rename churn with no behavioral content. -*SIMPLNX implementation:* `Algorithms/ErodeDilateBadData.cpp` (~230 lines) uses `NeighborUtilities::VoxelNeighbors` for face-neighbor offsets and boundary validity, and `ParallelTaskAlgorithm` to transfer non-`FeatureIds` arrays in parallel (with `FeatureIds` itself transferred afterward, serially, since the transfer condition for every other array depends on the *current* `FeatureIds` values). +*SIMPLNX implementation:* `Algorithms/ErodeDilateBadData.cpp` (231 lines) uses `NeighborUtilities::VoxelNeighbors` for face-neighbor offsets and boundary validity, and `ParallelTaskAlgorithm` to transfer non-`FeatureIds` arrays in parallel. ## Bug Fixes (this pass) -### ErodeDilateBadDataFilter-B1: Direction parameters had no effect — fixed +### ErodeDilateBadDataFilter-D1: Direction parameters had no effect — fixed See deviations doc for full detail. Summary: `adjustValidNeighbors` was dead code (defined, never called); `XDirOn`/`YDirOn`/`ZDirOn` had zero effect on which face neighbors participated. Fixed by retyping the helper to mask the actual per-voxel validity array (`isValidFaceNeighbor`) with correct axis mapping, and calling it at `Algorithms/ErodeDilateBadData.cpp:162-163` for every bad-data voxel. Verified by regenerating all 28 exemplar constants from real DREAM3D 6.5.171 output (they were previously byte-identical across all 7 direction combinations for a given operation/iteration count) and matching them element-wise against SIMPLNX for every combination — see Oracle section. ### Investigated, disproven, reverted: Dilate tie-break "fix" -A plausible-looking bug hypothesis (last-bad-neighbor-wins vs. first-bad-neighbor-wins, for a good voxel with multiple bad neighbors) was implemented as a fix and then falsified by an actual legacy binary run. Reverted in full. Recorded as a confirmed non-deviation in the deviations doc so it isn't relitigated. This is the reason the Oracle section below emphasizes binary-verified results over source-only reasoning — source-level comparison alone did not catch this, since legacy's own source has the identical "unconditional overwrite" line; only running both binaries against the same input and diffing a value that isn't blind to the tie-break (`Misc`, not `FeatureIds`) surfaced the truth. +A plausible-looking bug hypothesis (last-bad-neighbor-wins vs. first-bad-neighbor-wins, for a good voxel with multiple bad neighbors) was implemented as a fix and then falsified by an actual legacy binary run. Reverted in full. Recorded as a confirmed non-deviation in the deviations doc so it isn't relitigated. This is why the Oracle section emphasizes binary-verified results over source-only reasoning: source comparison alone did not catch it, since legacy's own source has the identical "unconditional overwrite" line. Only running both binaries against the same input and diffing a value that is not blind to the tie-break (`Misc`, not `FeatureIds`) surfaced the truth. ## Oracle -*Class:* **2 (Reference implementation)** — the expected values are genuine DREAM3D 6.5.171 output. They are compiled as constants in `ErodeDilateBadDataTest.cpp` rather than loaded from an exemplar archive, so the comparison re-runs in CI without needing the legacy binary present. +*Class:* **2 (Reference implementation)** — expected values are genuine DREAM3D 6.5.171 output, at two different scales. -*Fixture construction:* `CreateTestData()` builds an in-memory 4×4×2 (32-voxel) `ImageGeom` with a hand-authored `FeatureIds` array (features 1–6, with bad voxels at flat indices 0, 10, 13, 14, and 31) and a `Misc` `int32` array initialized so `Misc[i] == i`, making every copied tuple traceable to its source voxel by value alone. +*Applied:* Two oracles, both sourced from the legacy filter and neither derived from SIMPLNX output. -*Expected-output provenance:* the 28 `k_ExemplarFeatureIds*` / `k_ExemplarData*` constant pairs — one per operation (Erode/Dilate) × iteration count (1, 2) × direction combination (XYZ, XY, XZ, YZ, X, Y, Z) — were **regenerated from the legacy 6.5.171 binary** and verified element-wise against SIMPLNX's output, not hand-traced. That is a stronger oracle than a hand derivation and removes any question of the arrays having been fitted to SIMPLNX's own behavior. +- **Small-scale (28 combinations).** `CreateTestData()` builds an in-memory 4×4×2 (32-voxel) `ImageGeom` with a hand-authored `FeatureIds` array (features 1–6; bad voxels at flat indices 0, 10, 13, 14, 31) and a `Misc` `int32` array initialized so `Misc[i] == i`, making every copied tuple traceable to its source voxel by value alone. Pipeline JSONs (`DataContainerReader` → `ErodeDilateBadData` → `DataContainerWriter`) were run through the DREAM3D 6.5.171 `PipelineRunner` against an HDF5 twin of that fixture — verified byte-for-byte identical in dims, `FeatureIds`, and `Misc` before use — for all **28 combinations** of {Dilate, Erode} × {X, XY, XYZ, XZ, Y, YZ, Z} × {1, 2 iterations}. Outputs were diffed with `h5py` against SIMPLNX: **28/28 exact matches** on both arrays. Those legacy arrays are what the `k_ExemplarFeatureIds*` / `k_ExemplarData*` constants now hold — they were regenerated from the legacy binary, not hand-traced, which removes any question of their having been fitted to SIMPLNX's own behavior. +- **Production-scale (1 combination).** `6_6_erode_dilate_test.tar.gz` carries `Exemplar Bad Data Erode` and `Exemplar Bad Data Dilate` containers generated by the legacy SIMPL `ErodeDilateBadData` filter (UUID `3adfe077-…`, Erode and Dilate, `NumIterations=2`, all three directions on) on a 189×201×20 Small IN100 slice. The archive's embedded pipeline records that legacy build as `FilterVersion 6.6.338` — later than the 6.5.171 baseline — so this exemplar is genuine legacy output but is **not itself a 6.5.171 comparison**; the 6.5.171 comparison of record is the 28-combination run above. See `## Exemplar archive` and its provenance sidecar for both caveats. -*Legacy A/B run (this pass):* Built pipeline JSONs (`DataContainerReader` → `ErodeDilateBadData` → `DataContainerWriter`) and ran them through the actual DREAM3D 6.5.171 binary (`PipelineRunner.exe`, `C:\Users\holym\BlueQuartz\Builds\DREAM3D\DREAM3D-6.5.171-Win64`) against `Test Data/erode_dilate_legacy/erode_dilate_bad_data_base_test.dream3d` — verified byte-for-byte identical to the C++ `CreateTestData()` fixture (dims, `FeatureIds` including which 5 voxels are bad, `Misc`) before use. Ran and diffed (via `h5py`) all **28 combinations**: {Dilate, Erode} × {X, XY, XYZ, XZ, Y, YZ, Z} × {1, 2 iterations}. **28/28 exact matches**, both `FeatureIds` and `Misc`, against the exemplar constants now in `ErodeDilateBadDataTest.cpp`. +*Coverage split:* the archive exemplar was generated with all three directions enabled, so it cannot discriminate direction gating — a build with D1 still present passes it. That is exactly the gap the 28-combination sweep closes, and why both oracles are needed. Conversely the 32-voxel fixture is single-typed and single-component, so only the archive test exercises `copyTuple` across mixed types and component counts (`EulerAngles` float32×3, `IPFColor` uint8×3, `Mask` uint8, `Phases` int32, `Confidence Index`/`Image Quality` float32). -*Encoded:* `SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded` and `(Dilate) Expanded` — each `GENERATE`s `dirX,dirY,dirZ ∈ {true,false}` and `numIterations ∈ {1,2}`, skips the all-directions-off combination (invalid per preflight), and dispatches to the matching exemplar constants. **14 valid parameterized runs each for Erode and Dilate, both `FeatureIds` and `Misc` asserted — all pass** (built + run locally; `Misc` assertion was disabled in the prior pass and is now active for the first time). +*Encoded:* -*Second-engineer review:* Prior pass's open items — erode/dilate tie-break order, and whether direction combinations produce genuinely different output — are both **resolved this pass** via the legacy binary comparison above, not merely reviewed. Remaining recommendation: formalize the manual A/B run as an automated Class 2 test (see deviations doc) so future changes are caught by CI rather than requiring another manual pass. +- `SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded` and `(Dilate) Expanded` — each `GENERATE`s `dirX,dirY,dirZ ∈ {true,false}` and `numIterations ∈ {1,2}`, reports the invalid all-directions-off combination with `SUCCEED`, and looks the expected arrays up in the 28-row `k_Exemplars` table. 14 valid parameterized runs each, both `FeatureIds` and `Misc` asserted, 1039 assertions each — all pass. +- `SimplnxCore::ErodeDilateBadDataFilter(Erode)` — `UnitTest::CompareExemplarToGeneratedData` against `6_6_erode_dilate_bad_data.dream3d`, 55 assertions. Passes. + +*Second-engineer review:* **pending.** The prior pass's open items — erode/dilate tie-break order, and whether direction combinations produce genuinely different output — were both resolved this pass by the legacy binary comparison above rather than by review alone. A named second-engineer sign-off on the oracle design is still required before Status can move to COMPLETE. ## Code path coverage -9 of 9 paths exercised. Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp`. +10 of 11 paths exercised. The algorithm has three logical phases: (a) preflight validation, (b) a per-voxel vote/mark scan over face neighbors, and (c) a per-array transfer pass. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/ErodeDilateBadData.cpp` (231 lines). | # | Phase | Path | Test case | |---|-------|------|-----------| -| 1 | Setup | `numFeatures` scan, face-offset/validity initialization, `adjustValidNeighbors` direction masking | All tests. **As of this pass, this path is actually functional** — in the prior revision of this report, `adjustValidNeighbors` was listed here but was dead code (never called); it is now wired in and confirmed exercised for all 6 face directions, see below. | -| 2 | (b) Per-voxel | `featureName != 0` (good voxel) → skip | All tests (majority of the 32 voxels are good) | -| 3 | (b) Per-voxel | `featureName == 0` + Dilate + neighbor `feature > 0` → `neighbors[neighborPoint] = voxelIndex` | `(Dilate) Expanded`, all 6 face directions confirmed hit (see below) | -| 4 | (b) Per-voxel | `featureName == 0` + Erode + neighbor `feature > 0` → vote accumulation, `neighbors[voxelIndex] = neighborPoint` on new-max (ties keep the first-processed neighbor) | `(Erode) Expanded`, all 6 face directions confirmed hit (see below) | -| 5 | (b) Per-voxel | Erode post-vote cleanup — `featureCount[feature] = 0` for each valid neighbor of the bad voxel | `(Erode) Expanded`, all 6 face directions confirmed hit (see below) | -| 6 | (c) Transfer | `neighbor >= 0` + Erode condition (`featureName==0 && featureIds[neighbor]>0`) → `copyTuple` | `(Erode) Expanded` | -| 7 | (c) Transfer | `neighbor >= 0` + Dilate condition (`featureName>0 && featureIds[neighbor]==0`) → `copyTuple` | `(Dilate) Expanded` | +| 1 | (a) Setup | `numFeatures` scan, face-offset/validity initialization, `adjustValidNeighbors` direction masking | All tests. Functional as of this pass — in the prior revision `adjustValidNeighbors` was dead code; it is now wired in and confirmed exercised for all 6 face directions (see below). | +| 2 | (b) Per-voxel | `featureName != 0` (good voxel) → skip | All tests (most of the 32 voxels are good) | +| 3 | (b) Per-voxel | `featureName == 0` + Dilate + neighbor `feature > 0` → `neighbors[neighborPoint] = voxelIndex` | `(Dilate) Expanded`, all 6 face directions confirmed hit | +| 4 | (b) Per-voxel | `featureName == 0` + Erode + neighbor `feature > 0` → vote accumulation, `neighbors[voxelIndex] = neighborPoint` on new max (ties keep the first-processed neighbor) | `(Erode) Expanded`, all 6 face directions confirmed hit | +| 5 | (b) Per-voxel | Erode post-vote cleanup — `featureCount[feature] = 0` for each valid neighbor of the bad voxel | `(Erode) Expanded`, all 6 face directions confirmed hit | +| 6 | (c) Transfer | `neighbor >= 0` + Erode condition (`featureName == 0 && featureIds[neighbor] > 0`) → `copyTuple` | `(Erode) Expanded`, `(Erode)` | +| 7 | (c) Transfer | `neighbor >= 0` + Dilate condition (`featureName > 0 && featureIds[neighbor] == 0`) → `copyTuple` | `(Dilate) Expanded` | | 8 | (c) Transfer | `neighbor == -1` → skip (voxel untouched this iteration) | Both `Expanded` tests, implicitly (voxels far from bad data are unchanged in every expected array) | -| 9 | Preflight | `dims[0]==0 \|\| dims[1]==0 \|\| dims[2]==0` → error `-14602` (`k_NoGeometryDimensionsError`) | **Covered.** `No Dimensions` test now sets `directions = {true, true, true}` (previously all-off, which tripped the earlier `-14601` check first and masked this path — see prior V&V revision). Run and confirmed locally: 3/3 assertions pass, error code is exactly `-14602`. Also note the boundary condition itself changed from `&&` to `\|\|` (any single dimension being 0 is now sufficient to trigger the error, not just all three) — both the test fix and the condition fix landed together in this branch's `Fixed filter preflight errors` / `Fixing ErodeDilateBadData` commits. | +| 9 | (a) Preflight | `!xDirOn && !yDirOn && !zDirOn` → error `-14601` (`k_NoDirectionsError`) | `No Direction` — asserts `invalid()` and `errors()[0].code == -14601` for both operations | +| 10 | (a) Preflight | `dims[0] == 0 \|\| dims[1] == 0 \|\| dims[2] == 0` → error `-14602` (`k_NoGeometryDimensionsError`) | `No Dimensions` — directions all **on** so the `-14601` check does not mask this path (the prior revision zeroed them and never reached here); asserts error code is exactly `-14602` | +| 11 | (b) Cancel | `m_ShouldCancel` read once per Z-slice inside the iteration loop → early return | *Not directly tested. Requires cancel-signal injection; no test sets `m_ShouldCancel` and asserts early termination.* Legacy has no cancel check at all, so SIMPLNX is ahead of legacy here — not a deviation. | -**Per-direction coverage, confirmed by instrumentation this pass:** `Algorithms/ErodeDilateBadData.cpp` was temporarily instrumented with hit counters per face direction (`-Z/-Y/-X/+X/+Y/+Z`) at (a) the point immediately after the `isValidFaceNeighbor` gate in the vote/mark loop, (b) the point where the Dilate mark / Erode vote condition (`feature > 0`) actually fires, and (c) the equivalent point in the Erode cleanup loop. Running the full `(Erode) Expanded` + `(Dilate) Expanded` sweep (28 GENERATE runs) produced non-zero counts for **every one of the 6 directions at every one of those 3 measurement points** — e.g. vote/mark loop reached counts were `-Z=38 -Y=111 -X=108 +X=106 +Y=64 +Z=97`, and the `feature>0` condition fired for Dilate marking at `-Z=9 -Y=46 -X=37 +X=35 +Y=16 +Z=44` and for Erode voting at `-Z=8 -Y=25 -X=24 +X=24 +Y=8 +Z=32`. The instrumentation was removed after confirming this (not shipped in the reverted-to-clean algorithm file); this row records the empirical result, not a standing code artifact. +**Per-direction coverage, confirmed by instrumentation this pass:** `Algorithms/ErodeDilateBadData.cpp` was temporarily instrumented with per-face-direction hit counters at (a) the point immediately after the `isValidFaceNeighbor` gate in the vote/mark loop, (b) the point where the Dilate mark / Erode vote condition (`feature > 0`) fires, and (c) the equivalent point in the Erode cleanup loop. Running the full `(Erode) Expanded` + `(Dilate) Expanded` sweep produced non-zero counts for **every one of the 6 directions at all 3 measurement points** — vote/mark loop reached counts `-Z=38 -Y=111 -X=108 +X=106 +Y=64 +Z=97`; Dilate marking fired at `-Z=9 -Y=46 -X=37 +X=35 +Y=16 +Z=44`; Erode voting at `-Z=8 -Y=25 -X=24 +X=24 +Y=8 +Z=32`. The instrumentation was removed afterward; this records the empirical result, not a standing code artifact. -Additional confirmed items, not path gaps but worth recording: +Confirmed correct and deliberately not counted as deviations: -- **Cancel path exists but is untested.** Corrected from the prior V&V pass, which found no cancel check present at the time. As of `56b30c923` ("Fixing ErodeDilateBadData"), `operator()` now reads `m_ShouldCancel` once per Z-slice (`Algorithms/ErodeDilateBadData.cpp:144-148`, inside the outer `for(zIdx...)` loop, itself inside the `for(iteration...)` loop) and returns immediately if set. This is a real, functional early-exit — checked on every Z-slice of every iteration, not just once — but no current test sets `m_ShouldCancel` and asserts early termination, so this path is present in the code and reachable, but not exercised by any `TEST_CASE`. Not counted in the 9-path table above (that table scopes to `preflightImpl`/vote-transfer branches); worth considering as a 10th path if the table's scope is later widened. Legacy's equivalent loop has no cancel check at all — SIMPLNX is ahead of legacy here, not behind; not a deviation. -- **Direction masking, fixed.** Previously flagged: "`adjustValidNeighbors` bitwise-ANDs the face-index constants themselves... flagged for second-engineer scrutiny." This is now resolved — see Bug Fixes / `ErodeDilateBadDataFilter-B1` in the deviations doc. The function has been rewritten and is confirmed exercised across all 6 directions (this section, above) and legacy-verified across all 28 combinations (Oracle section). +- **Direction masking of the Erode `featureCount` reset loop.** SIMPLNX reuses the direction-masked `isValidFaceNeighbor` in 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 the equivalence is general: the reset set is a superset of the increment set in both variants, so `featureCount` returns to all-zeros after every bad voxel either way. +- **`neighbors` is intentionally not reset between iterations** — matches legacy. The deviations doc records the investigation that established this. +- **`MessageHelper` shared across parallel tasks.** Each task gets its own `ThrottledMessenger` (independent timing state) over a shared `std::shared_ptr`; `trySendMessage` is the documented cross-thread path. ## Test inventory -| Test case | Notes | -|-----------|-------| -| `SimplnxCore::ErodeDilateBadDataFilter(Erode)` | Exemplar-archive-based smoke test (`6_6_erode_dilate_test.tar.gz`), all directions on, 2 iterations. Passes. | -| `SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded` | Class 2 oracle (see Oracle). `GENERATE` over 7 valid direction combinations × 2 iteration counts (14 runs). Compares both `FeatureIds` and `Misc` against exemplar arrays. Passes. | -| `SimplnxCore::ErodeDilateBadDataFilter(Dilate) Expanded` | Same sweep, Dilate operation, both arrays asserted. Passes. | -| `SimplnxCore::ErodeDilateBadDataFilter Ignored Path` | Confirms an array listed in `IgnoredDataArrayPaths` (`Misc`) is left untouched. Preflights **and executes** the filter, then asserts both that `Misc` still equals the fixture values and that `FeatureIds` did change — the second assertion is what keeps the first from passing vacuously (in the prior revision this test never called `execute()`, so it could not distinguish "ignored" from "filter never ran"; verified by mutation — emptying the ignore list, or dropping the `execute()` call, each now fails the test). Passes. | -| `SimplnxCore::ErodeDilateBadDataFilter No Dimensions` | Preflight-error test: `ImageGeom` dimensions forced to `{0,0,0}`, directions all **on**. Asserts `preflightResult.outputActions.invalid()` and `errors()[0].code == -14602`. Correctly named and covers the intended zero-dimensions path (previously it also zeroed all direction flags, which tripped the earlier `-14601` check first — now fixed). | -| `SimplnxCore::ErodeDilateBadDataFilter No Direction` | Preflight-error test: all directions off, geometry otherwise valid. Asserts `-14601`. Correctly named and covers the intended path. | -| `SimplnxCore::ErodeDilateBadDataFilter: SIMPL Backwards Compatibility` | `DYNAMIC_SECTION` over `simpl_conversion/6_5/ErodeDilateBadDataFilter.json` (matched by `Filter_Uuid`) and `simpl_conversion/6_4/ErodeDilateBadDataFilter.json` (matched by `Filter_Name`, no UUID field present in that fixture). Loads each legacy pipeline JSON via `Pipeline::FromSIMPLFile`, confirms it resolves to a single `PipelineFilter` with `FilterTraits::uuid`, and checks the converted arguments: `Operation == k_Dilate` (legacy `Direction: 0` round-trips to SIMPLNX's own `Dilate = 0`), `NumIterations == 5`, `XDirOn/YDirOn/ZDirOn == true`, geometry path `DataPath({"DataContainer"})`, feature-ids path `DataPath({"DataContainer","CellData","TestArray"})`. `IgnoredDataArrayPaths` verified only by successful pipeline load, not by value, matching the pattern used in `FillBadDataTest.cpp`. Passes. | +| Test case | Status | Notes | +|-----------|--------|-------| +| `SimplnxCore::ErodeDilateBadDataFilter(Erode)` | kept | Class 2 production-scale oracle. `6_6_erode_dilate_test.tar.gz`, Erode, all directions on, 2 iterations, 189×201×20. `CompareExemplarToGeneratedData` over the 6 cell arrays present in both containers (`Confidence Index`, `EulerAngles`, `FeatureIds`, `Image Quality`, `Mask`, `Phases`; the exemplar's `IPFColor` has no counterpart in `Input Data` and is skipped); 55 assertions. The only test that exercises `copyTuple` across mixed types and component counts. Restored this pass after an earlier revision dropped it. | +| `SimplnxCore::ErodeDilateBadDataFilter(Erode) Expanded` | new-for-V&V | Class 2 small-scale oracle. `GENERATE` over 7 valid direction combinations × 2 iteration counts (14 runs); all-off reported via `SUCCEED`. Both `FeatureIds` and `Misc` asserted against the `k_Exemplars` table; 1039 assertions. Modified this pass: expected arrays regenerated from legacy output (previously byte-identical across direction combinations, per D1), the `Misc` assertion enabled for the first time, and the two duplicated dispatch helpers collapsed into one table lookup. | +| `SimplnxCore::ErodeDilateBadDataFilter(Dilate) Expanded` | new-for-V&V | Same sweep and same table, Dilate operation; 1039 assertions. Same modifications as the Erode sweep. | +| `SimplnxCore::ErodeDilateBadDataFilter Ignored Path` | new-for-V&V | Confirms an array listed in `IgnoredDataArrayPaths` (`Misc`) is left untouched, for both operations via `GENERATE(k_Dilate, k_Erode)`; 91 assertions. Modified this pass: the prior revision only preflighted, so comparing the DataStructure against a fresh fixture could not fail. It now executes the filter and additionally asserts that `FeatureIds` *did* change, which is what keeps the ignored-path check from passing vacuously. Verified by mutation — emptying the ignore list, or dropping the `execute()` call, each now fails the test. | +| `SimplnxCore::ErodeDilateBadDataFilter No Direction` | new-for-V&V | Preflight-error test: all directions off, geometry otherwise valid, both operations × 2 iteration counts. Asserts `invalid()` and `errors()[0].code == -14601`; 25 assertions. Covers Path 9. | +| `SimplnxCore::ErodeDilateBadDataFilter No Dimensions` | new-for-V&V | Preflight-error test: `ImageGeom` dimensions forced to `{0,0,0}`, directions all on, Dilate. Asserts `invalid()` and `errors()[0].code == -14602`; 7 assertions. Covers Path 10. Modified this pass: previously it also zeroed all direction flags, which tripped `-14601` first and left Path 10 unreached. | +| `SimplnxCore::ErodeDilateBadDataFilter: SIMPL Backwards Compatibility` | kept | `DYNAMIC_SECTION` over `simpl_conversion/6_5/ErodeDilateBadDataFilter.json` (matched by `Filter_Uuid`) and `simpl_conversion/6_4/ErodeDilateBadDataFilter.json` (matched by `Filter_Name`; that fixture has no UUID field). Loads each legacy pipeline via `Pipeline::FromSIMPLFile`, confirms a single `PipelineFilter` with the right UUID, and checks the converted arguments: `Operation == k_Dilate`, `NumIterations == 5`, `XDirOn/YDirOn/ZDirOn == true`, geometry path `DataPath({"DataContainer"})`, feature-ids path `DataPath({"DataContainer","CellData","TestArray"})`; 27 assertions. `IgnoredDataArrayPaths` verified only by successful pipeline load, matching the pattern in `FillBadDataTest.cpp`. Not an oracle test. | + +All 7 tests pass in both the in-core build (`NX-Com-Qt69-Vtk96-Rel`) and the out-of-core build (`simplnx-ooc-Rel`), with identical assertion counts in each — 2283 total (55 + 1039 + 1039 + 91 + 25 + 7 + 27). + +## Exemplar archive + +- **Archive:** `6_6_erode_dilate_test.tar.gz` +- **SHA512:** `5f0773e5d296936effbb2239965f5847e7c18533b0a2c3ec6a1d6a83b03417e5b459cce29808c8e0273613b3b6fa032c675e84926eb35d8da8a6ddc0641a0ef5` +- **Provenance:** `src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md` + +The archive is shared: `6_6_erode_dilate_bad_data.dream3d` serves this filter, `6_6_erode_dilate_mask.dream3d` serves `ErodeDilateMaskTest`, and `6_6_erode_dilate_coordination_number.dream3d` serves `ErodeDilateCoordinationNumberTest`. Any regeneration must account for all three consumers. ## Deviations from DREAM3D 6.5.171 -See [`deviations/ErodeDilateBadDataFilter.md`](deviations/ErodeDilateBadDataFilter.md) — one confirmed and fixed SIMPLNX-side bug (`ErodeDilateBadDataFilter-B1`, direction parameters had no effect), one investigated-and-disproven hypothesis (Dilate tie-break order — legacy matches SIMPLNX's original behavior), and no confirmed legacy deviations. Legacy binary/pipeline comparison has now been run (28/28 matches) — no longer a gap. +No confirmed legacy deviations. The comparison was run at both scales described in the Oracle section — 28/28 exact matches on the 32-voxel fixture, and an element-wise match against the legacy-generated exemplar on the 189×201×20 Small IN100 slice. + +One SIMPLNX-side bug was found and fixed: + +- `ErodeDilateBadDataFilter-D1` — the `XDirOn`/`YDirOn`/`ZDirOn` parameters had no effect on which face neighbors participated — see [`deviations/ErodeDilateBadDataFilter.md`](deviations/ErodeDilateBadDataFilter.md) + +One hypothesis was investigated and disproven (Dilate tie-break order: legacy matches SIMPLNX's original last-write-wins behavior), and is recorded in the same file as a confirmed non-deviation so it is not relitigated. diff --git a/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md index 9369e3dc1f..2322b4e534 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/ErodeDilateBadDataFilter.md @@ -1,55 +1,91 @@ # Deviations from DREAM3D 6.5.171: ErodeDilateBadDataFilter -Entries use stable IDs (`ErodeDilateBadDataFilter-D` for legacy deviations, `ErodeDilateBadDataFilter-B` for SIMPLNX-side bugs). +This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. ---- +Entries are referenced by stable ID (`ErodeDilateBadDataFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. + +> **ID note:** `ErodeDilateBadDataFilter-D1` was cited as `ErodeDilateBadDataFilter-B1` in earlier revisions of this file and in the PR #1687 review thread, under a local `-B` convention for SIMPLNX-side bugs. That convention is used nowhere else in the repository, so the entry has been renumbered to the project-standard `-D` form (matching `CAxisSegmentFeaturesFilter-D1`, which documents an equivalent SIMPLNX-side bug found during its own V&V cycle). No external references to the old ID exist. + +## Headline: legacy A/B comparison performed — one SIMPLNX-side bug, fixed -## Headline: Legacy A/B comparison performed — one confirmed SIMPLNX-side bug, fixed +The gap recorded in the previous revision of this file ("no legacy comparison has been performed") is closed. A genuine DREAM3D 6.5.171 `PipelineRunner` was run against a hand-verified input twin of the C++ test fixture across **all 28 combinations** of operation (Erode/Dilate) × direction combination (7 valid combos) × iteration count (1, 2), and the legacy `ErodeDilateBadData.{h,cpp}` source was diffed line-by-line against the SIMPLNX algorithm. Both the binary and the source live in a sibling checkout on the authoring engineer's machine and are not committed to this repository. See `ErodeDilateBadDataFilter-D1` below and the V&V report's Oracle and Bug Fixes sections. + +--- -The gap recorded in the previous revision of this file ("no legacy comparison has been performed") is closed. Legacy source was located on this machine (`C:\Users\holym\BlueQuartz\Builds\DREAM3D\DREAM3D-6.5.171-Win64` binary, plus `ErodeDilateBadData.{h,cpp}` source in a sibling `DREAM3D` checkout — not committed to this repository, but usable for direct comparison) and a genuine DREAM3D 6.5.171 pipeline (`PipelineRunner.exe`) was run against a hand-verified input twin of the C++ test fixture, across **all 28 combinations** of operation (Erode/Dilate) × direction combination (7 valid combos) × iteration count (1, 2). See [`ErodeDilateBadDataFilter-B1`](#erodedilatebaddatafilter-b1-direction-parameters-had-no-effect-fixed) below and the V&V report's Oracle/Bug Fixes sections for full detail. +## ErodeDilateBadDataFilter-D1 -## ErodeDilateBadDataFilter-B1: Direction parameters had no effect (fixed) +| Field | Value | +|---|---| +| **Deviation ID** | `ErodeDilateBadDataFilter-D1` (formerly cited as `-B1`) | +| **Filter UUID** | `7f2f7378-580e-4337-8c04-a29e7883db0b` | +| **Status** | active (SIMPLNX bug **fixed during this V&V cycle** in PR #1687; documented for users of prior SIMPLNX releases) | -**Type:** SIMPLNX-side bug (not a legacy deviation — legacy behaves correctly here; SIMPLNX did not). +**Symptom:** In SIMPLNX releases prior to PR #1687, the *X Direction*, *Y Direction*, and *Z Direction* parameters had **no effect on the output**. They were parsed correctly from filter args into `ErodeDilateBadDataInputValues` (`ErodeDilateBadDataFilter.cpp:151-153`), but every face neighbor remained eligible — subject only to the geometry boundary — regardless of the flags. Disabling a direction silently produced the all-directions-on result. DREAM3D 6.5.171 honors the flags correctly, so any run with fewer than all three directions enabled diverges from legacy. This is also what produced the previous V&V pass's observation that "all 7 direction-combination fixtures encode byte-identical expected output": the fixture was not under-discriminating, the algorithm was ignoring direction entirely. -**Symptom:** `XDirOn`/`YDirOn`/`ZDirOn` were parsed correctly from filter args into `ErodeDilateBadDataInputValues` (`ErodeDilateBadDataFilter.cpp:151-153`), but had **zero effect** on the algorithm. Every face neighbor was eligible (subject only to geometry boundary) regardless of the Direction parameters. This is exactly what produced the previous V&V pass's observation that "all 7 direction-combination fixtures ... encode byte-identical expected output" — the fixture wasn't under-discriminating, the *algorithm* was ignoring direction entirely. +**Root cause:** Bug (SIMPLNX). `adjustValidNeighbors` — the helper intended to mask face-neighbor validity by direction — was defined in `Algorithms/ErodeDilateBadData.cpp` but **never called** from `operator()()`. Confirmed by grepping the translation unit for `XDirOn` / `YDirOn` / `ZDirOn` / `adjustValidNeighbors(`: only the function definition matched, with no call site. Legacy achieves the same gating by ORing the direction flag into each per-face boundary check (`|| !m_ZDirOn` and siblings), so the legacy behavior was never in question. -**Root cause:** `adjustValidNeighbors` — the helper clearly intended to mask face-neighbor validity by direction — was defined in `Algorithms/ErodeDilateBadData.cpp` but **never called** anywhere in `operator()()`. Confirmed by grepping the compiled `.cpp` for the literal strings `XDirOn`/`YDirOn`/`ZDirOn`/`adjustValidNeighbors(`: only the function *definition* matched, no call site. (A branch-history note: an earlier commit on this working branch, `7e543f701` "Fixed XYZ direction off bug", *had* added a call to `adjustValidNeighbors`, but passed it the face-index-order array and bitwise-ANDed index constants `0..5` against the direction booleans — which corrupts the iteration order rather than gating validity, and additionally had `+X` gated by `zDir` and `+Z` gated by `xDir` [swapped axes]. That call was later removed in an uncommitted edit, leaving direction fully inert — the state this pass found and fixed from scratch.) +*Branch-history note:* an earlier commit on the fix branch, `7e543f701` "Fixed XYZ direction off bug", *had* added a call to `adjustValidNeighbors`, but passed it the face-index-order array and bitwise-ANDed the index constants `0..5` against the direction booleans — which corrupts the iteration order rather than gating validity — and additionally gated `+X` by `zDir` and `+Z` by `xDir` (swapped axes). Since `2&1=0` and `3&1=1`, the iterated face list collapsed to `{-Z,-Y}` for *every* flag combination, including all-on. That call was later removed, leaving direction fully inert — the state this V&V pass found and fixed from scratch. -**Fix:** `Algorithms/ErodeDilateBadData.cpp:64-80` — `adjustValidNeighbors` now takes the per-voxel `isValidFaceNeighbor` boolean array (the actual validity gate consumed by the vote/mark loop) and ANDs each of the 6 entries against the correct axis flag, using the named `VoxelNeighbors` constants rather than raw indices. It is now called at `:162-163`, immediately after `computeValidFaceNeighbors`, for every bad-data voxel. +**Fix:** `Algorithms/ErodeDilateBadData.cpp:64-80` — `adjustValidNeighbors` now takes the per-voxel `isValidFaceNeighbor` boolean array (the actual validity gate consumed by the vote/mark loop) and ANDs each of the six entries against the correct axis flag, using the named `VoxelNeighbors` constants rather than raw indices. It is called at `:162-163`, immediately after `computeValidFaceNeighbors`, for every bad-data voxel. **Verification:** -- All 28 `k_ExemplarFeatureIds*`/`k_ExemplarData*` (Misc) constants in `ErodeDilateBadDataTest.cpp` were regenerated from genuine DREAM3D 6.5.171 binary output (they were previously byte-identical across all 7 combos for a given operation/iteration count, which is what masked the bug). They are legacy output, not a hand derivation — a Class 2 oracle. -- Independently corroborated against genuine DREAM3D 6.5.171 output: 28/28 combinations (7 directions × 2 operations × 2 iteration counts) match exactly, both `FeatureIds` and `Misc` — see V&V report Oracle section for the run details. -- `(Erode) Expanded` / `(Dilate) Expanded` (28 GENERATE runs total): pass. + +- All 28 `k_ExemplarFeatureIds*` / `k_ExemplarData*` constants in `ErodeDilateBadDataTest.cpp` were regenerated from genuine DREAM3D 6.5.171 binary output. They are legacy output, not a hand derivation — a Class 2 oracle. (They were previously byte-identical across all 7 direction combinations for a given operation and iteration count, which is what masked the bug.) +- 28/28 combinations (7 directions × 2 operations × 2 iteration counts) match legacy exactly, on both `FeatureIds` and the `Misc` tracer array — see the V&V report's Oracle section for the run details. +- `(Erode) Expanded` / `(Dilate) Expanded` (28 parameterized runs total, 2078 assertions combined) pass in both in-core and OOC builds. +- Per-direction structural coverage confirmed by temporary hit-count instrumentation — see "Per-direction code-path coverage" below. + +**Affected users:** Anyone who ran `ErodeDilateBadData` on a SIMPLNX build predating PR #1687 with fewer than all three directions enabled. Their output silently matched the all-directions-on result, eroding or dilating across axes they had explicitly disabled. Users who left all three directions on (the default) are **unaffected** — that path was always correct, and the archive-based `(Erode)` regression test passes on pre-fix builds for exactly that reason. + +**Recommendation:** Trust SIMPLNX at or after PR #1687, which agrees with 6.5.171 across all 28 parameter combinations. Results from affected pre-fix builds that used a restricted direction set should be regenerated. + +--- ## Non-deviations (confirmed correct — do not "fix") ### Dilate tie-break: last-bad-neighbor-wins is correct, not a bug -**Investigated and ruled out this pass.** When a good voxel has two or more bad face-neighbors, `neighbors[neighborPoint] = voxelIndex` unconditionally overwrites on each bad neighbor visited, so whichever bad voxel is scanned *last* (highest flat index, in z/y/x order) wins. Since every bad voxel shares `FeatureId == 0`, this choice is invisible to a `FeatureIds`-only comparison — it only shows up in the `Misc` tracer array, which is exactly why it was flagged as unverified in the prior pass and why this pass initially suspected it as a bug. +**Investigated and ruled out this pass.** When a good voxel has two or more bad face-neighbors, `neighbors[neighborPoint] = voxelIndex` unconditionally overwrites on each bad neighbor visited, so whichever bad voxel is scanned *last* (highest flat index, in z/y/x order) wins. Since every bad voxel shares `FeatureId == 0`, this choice is invisible to a `FeatureIds`-only comparison — it shows up only in the `Misc` tracer array, which is why it was flagged as unverified in the prior pass and initially suspected as a bug in this one. -A "first bad neighbor wins" fix (skip the overwrite if `neighbors[neighborPoint]` is already set, plus resetting `neighbors` to `-1` at the top of each iteration) was implemented and *appeared* correct until checked against real DREAM3D 6.5.171 output: `PipelineRunner.exe` running Dilate/XYZ/1-iteration against the matching legacy input file produced `Misc` values matching the **original, unmodified** last-write-wins SIMPLNX behavior, not the "first-wins" rewrite (diverged at 3 of 32 indices: 9, 15, 30). The "first-wins" change was reverted in full (both the per-iteration reset and the overwrite guard); unconditional last-write-wins, with `neighbors` initialized once before the iteration loop (not per-iteration), is confirmed legacy-faithful. +A "first bad neighbor wins" fix (skip the overwrite if `neighbors[neighborPoint]` is already set, plus resetting `neighbors` to `-1` at the top of each iteration) was implemented and *appeared* correct until checked against real DREAM3D 6.5.171 output: `PipelineRunner` running Dilate / XYZ / 1 iteration against the matching legacy input produced `Misc` values matching the **original, unmodified** last-write-wins SIMPLNX behavior, not the "first-wins" rewrite (diverging at 3 of 32 indices: 9, 15, 30). The change was reverted in full — both the per-iteration reset and the overwrite guard. Unconditional last-write-wins, with `neighbors` initialized once before the iteration loop rather than per-iteration, is confirmed legacy-faithful. -This resolves the prior V&V pass's "second-engineer review pending: erode/dilate tie-break order" item — no further review needed; verified against the actual legacy binary output, not just source reading. +This resolves the prior V&V pass's "second-engineer review pending: erode/dilate tie-break order" item — verified against actual legacy binary output, not source reading alone. ### Erode tie-break: first-processed-neighbor-wins is correct -Vote-count-based, using `[-Z,-Y,-X,+X,+Y,+Z]` scan order; a later neighbor's vote must strictly exceed the current max to replace the leader. Matches legacy source line-for-line (identical vote/comparison logic) and matches legacy binary output for all tested combinations. Not a deviation. +Vote-count-based, using `[-Z,-Y,-X,+X,+Y,+Z]` scan order; a later neighbor's vote must strictly exceed the current maximum to replace the leader. Matches legacy source line-for-line (identical vote and comparison logic) and matches legacy binary output for all 28 tested combinations. Not a deviation. + +### Direction masking of the Erode `featureCount` reset loop + +SIMPLNX reuses the direction-masked `isValidFaceNeighbor` array in the post-vote 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 the equivalence is general rather than incidental: the reset set is a superset of the increment set in both variants, so `featureCount` returns to all-zeros after every bad voxel either way. Not a deviation. + +### Deferring the FeatureIds transfer to a second pass + +Legacy interleaves the `FeatureIds` transfer with the other cell arrays in a single pass, mutating `m_FeatureIds` as it goes. SIMPLNX transfers the other arrays first (in parallel) and `FeatureIds` afterward, serially. Equivalent: 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. Not a deviation. ### Legacy tie-break language says "chosen randomly"; SIMPLNX is deterministic -The SIMPLNX filter markdown (`docs/ErodeDilateBadDataFilter.md`), carried over from legacy documentation, stated that erode ties are broken "randomly." Both the legacy *source* (`ErodeDilateBadData.cpp`, `Source/Plugins/Processing/ProcessingFilters/`) and the legacy *binary* output are fully deterministic — same first-processed-wins scan order as SIMPLNX, no RNG involved anywhere in the algorithm. "Randomly" in the legacy docs is inaccurate documentation language, not a behavioral characteristic; SIMPLNX's determinism is not a deviation. (Legacy source is now available for direct comparison — this was previously only inferable.) +The SIMPLNX filter markdown (`docs/ErodeDilateBadDataFilter.md`), carried over from legacy documentation, stated that erode ties are broken "randomly." Both the legacy *source* (`ErodeDilateBadData.cpp`, `Source/Plugins/Processing/ProcessingFilters/`) and the legacy *binary* output are fully deterministic — the same first-processed-wins scan order as SIMPLNX, with no RNG anywhere in the algorithm. "Randomly" is inaccurate documentation language, not a behavioral characteristic; SIMPLNX's determinism is not a deviation. + +**The user-facing doc has been corrected** to describe the actual behavior: the six face neighbors are visited in the fixed order `[-Z,-Y,-X,+X,+Y,+Z]` and a later neighbor must have a strictly greater count to displace the leader, so the earliest tied neighbor in that scan order wins. The same edit documents the two preflight errors (`-14601` no directions enabled, `-14602` zero-length geometry dimension) and the effect of the direction restrictions. -**The user-facing doc has now been corrected** to describe the actual behavior: the six face neighbors are visited in the fixed order `[-Z,-Y,-X,+X,+Y,+Z]` and a later neighbor must have a strictly greater count to displace the leader, so the earliest tied neighbor in that scan order wins. The same edit documents the two preflight errors (`-14601` no directions enabled, `-14602` zero-length geometry dimension) and the effect of the direction restrictions. +### Cancel check present in SIMPLNX, absent in legacy + +`operator()` reads `m_ShouldCancel` once per Z-slice inside the iteration loop and returns immediately if set. Legacy's equivalent loop has no cancel check at all. Additive capability with no effect on a run to completion — SIMPLNX is ahead of legacy here, not behind. Not a deviation. (This path is Path 11 in the V&V report's coverage table and is the one enumerated path no test exercises.) ## Per-direction code-path coverage Previously an open question ("could not distinguish correct per-direction gating from a no-op gate" — see prior V&V pass). Resolved this pass by two independent means: -1. **Behavioral:** exemplar data now differs by direction combination (see B1 above), and matches legacy per-combination. -2. **Structural:** `Algorithms/ErodeDilateBadData.cpp` was temporarily instrumented with per-face-direction (`-Z/-Y/-X/+X/+Y/+Z`) hit counters and run through the full `(Erode) Expanded` + `(Dilate) Expanded` GENERATE sweep. All 6 directions were hit — both at the "loop reached" level and at the "vote/mark condition fired" level, in both the vote/mark loop and the Erode-only cleanup loop. Instrumentation was removed afterward (not shipped). + +1. **Behavioral:** expected data now differs by direction combination (see D1 above) and matches legacy per-combination, 28/28. +2. **Structural:** `Algorithms/ErodeDilateBadData.cpp` was temporarily instrumented with per-face-direction (`-Z/-Y/-X/+X/+Y/+Z`) hit counters and run through the full `(Erode) Expanded` + `(Dilate) Expanded` sweep. All six directions were hit, both at the "loop reached" level and at the "vote/mark condition fired" level, in both the vote/mark loop and the Erode-only cleanup loop. Instrumentation was removed afterward and is not shipped. ## What would need to happen to extend this further -Everything listed in the prior revision of this file has been done: legacy binary obtained and run, SIMPLNX output compared element-wise against legacy output, and the neighbor-selection/tie-break logic diffed directly against legacy source. The zero-dimensions preflight path (`-14602`), also previously listed here as uncovered, is now reached and correctly asserted by the `No Dimensions` test (fixed on this branch — see V&V report Code path coverage). Remaining follow-up (not gating, see V&V report): -1. The legacy A/B comparison in this pass was a manual/one-time verification (pipeline JSONs run through `PipelineRunner.exe`, output diffed via `h5py`), not wired into automated CI. Consider checking in the legacy `.dream3d` input/output pairs as an exemplar archive and adding an automated Class 2 comparison test (matching the pattern used by `FillBadDataFilter`'s `FillBadData_SmallIN100` test), so this verification re-runs on every CI build instead of relying on this document. +Everything listed in the prior revision of this file has been done: legacy binary obtained and run, SIMPLNX output compared element-wise against legacy output, and the neighbor-selection and tie-break logic diffed directly against legacy source. The zero-dimensions preflight path (`-14602`), also previously listed here as uncovered, is now reached and asserted by the `No Dimensions` test. + +Remaining follow-up (not gating — see the V&V report's V&V phase row): + +1. **Automate the 28-combination A/B run.** It was a manual, one-time verification (pipeline JSONs through `PipelineRunner`, output diffed with `h5py`), not wired into CI. The expected values are now compiled into the test as constants, so the *comparison* does re-run in CI — but a future change to the fixture would require redoing the legacy run by hand. Consider checking in the legacy `.dream3d` input/output pairs as an exemplar archive with a provenance sidecar, matching the pattern used by `FillBadDataFilter`'s `FillBadData_SmallIN100` test. +2. **Cover the cancel path (Path 11).** Requires cancel-signal injection; no test currently sets `m_ShouldCancel` and asserts early termination. +3. **Add a production-scale Dilate test.** `6_6_erode_dilate_test.tar.gz` already contains an unused `Exemplar Bad Data Dilate` container, so this costs no new test data. See `../provenance/6_6_erode_dilate_test.md`. diff --git a/src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md b/src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md new file mode 100644 index 0000000000..66e9971716 --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/provenance/6_6_erode_dilate_test.md @@ -0,0 +1,97 @@ +# Exemplar Archive Provenance: 6_6_erode_dilate_test.tar.gz + +This sidecar records the origin of the exemplar archive used by the three Erode/Dilate filter unit tests. The archive name and SHA512 must match `download_test_data()` in `src/Plugins/SimplnxCore/test/CMakeLists.txt`. + +--- + +## Archive identity + +| Field | Value | +|---|---| +| **Archive** | `6_6_erode_dilate_test.tar.gz` (68,857,947 bytes) | +| **SHA512** | `5f0773e5d296936effbb2239965f5847e7c18533b0a2c3ec6a1d6a83b03417e5b459cce29808c8e0273613b3b6fa032c675e84926eb35d8da8a6ddc0641a0ef5` | +| **Used by tests** | `SimplnxCore::ErodeDilateBadDataFilter(Erode)`; `SimplnxCore::ErodeDilateMaskFilter(Dilate)` and `(Erode)`; `SimplnxCore::ErodeDilateCoordinationNumberFilter` | +| **Generated by** | ** — the embedded pipeline references `/Users/mjackson/Applications/Data/Output/Reconstruction/SmallIN100.h5ebsd`, so it was authored by Michael Jackson | +| **Generated on** | **; archive mtime on the shared data mirror is 2026-02-19 | +| **Generated at commit** | ** — see the embedded pipeline's `FilterVersion` strings below | + +SHA512 verified 2026-08-14 against both `test/CMakeLists.txt` and the on-disk archive in the shared `DREAM3D_Data/TestFiles` mirror. They match. + +## Contents + +| File | Consumer | +|---|---| +| `6_6_erode_dilate_bad_data.dream3d` + `.xdmf` | `ErodeDilateBadDataFilter` | +| `6_6_erode_dilate_mask.dream3d` + `.xdmf` | `ErodeDilateMaskFilter` | +| `6_6_erode_dilate_coordination_number.dream3d` + `.xdmf` | `ErodeDilateCoordinationNumberFilter` | + +Any regeneration must account for all three consumers, not just `ErodeDilateBadData`. + +`6_6_erode_dilate_bad_data.dream3d` is in the legacy (`DataContainers`) layout, `FileVersion` 7.0, and holds three data containers on the same 189×201×20 `ImageGeom` (759,780 cells): + +| Container | Role | +|---|---| +| `Input Data` | Filter input. `EBSD Scan Data`: `Confidence Index` float32, `EulerAngles` float32×3, `FeatureIds` int32, `Image Quality` float32, `Mask` uint8, `Phases` int32. Plus `Grain Data/Active` uint8 (1104 tuples) and `Phase Data` (`CrystalStructures`, `LatticeConstants`, `MaterialName`). | +| `Exemplar Bad Data Erode` | Expected output for Erode, all directions on, 2 iterations. Same arrays plus `IPFColor` uint8×3. | +| `Exemplar Bad Data Dilate` | Expected output for Dilate, all directions on, 2 iterations. Same arrays plus `IPFColor` uint8×3. | + +## How it was generated + +The archive was produced by a legacy DREAM3D pipeline that is **embedded in the `.dream3d` file itself** (`/Pipeline/Pipeline`, `Pipeline Version` 2) — so the provenance is self-documenting even though no external `.d3dpipeline` was preserved. The 18-step pipeline reads the first slice of Small IN100 from `SmallIN100.h5ebsd`, performs a standard reconstruction, then branches twice to produce the two exemplars: + +| Step | Filter | +|---|---| +| 00 | `PipelineAnnotation` | +| 01 | `ReadH5Ebsd` → `Input Data` | +| 02–09 | `MultiThresholdObjects`, `ConvertOrientations`, `AlignSectionsMisorientation`, `IdentifySample`, `AlignSectionsFeatureCentroid`, `BadDataNeighborOrientationCheck`, `NeighborOrientationCorrelation`, `EBSDSegmentFeatures` | +| 10 | `RemoveArrays` | +| 11–13 | `CopyDataContainer` → `Exemplar Bad Data Erode`, `ErodeDilateBadData`, `GenerateIPFColors` | +| 14–16 | `CopyDataContainer` → `Exemplar Bad Data Dilate`, `ErodeDilateBadData`, `GenerateIPFColors` | +| 17 | `DataContainerWriter` | + +The `PipelineAnnotation` step records that the pipeline was run against a build where `EBSDSegmentFeatures` had `RandomizeFeatureIds` forced to `false` internally, so that `FeatureIds` are reproducible for unit testing, and that only the first slice of Small IN100 was used with minimal processing. + +The two `ErodeDilateBadData` steps are the **legacy SIMPL filter**, `Filter_Uuid` `{3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254}`, with these parameters: + +| Step | `Direction` | `NumIterations` | `XDirOn` / `YDirOn` / `ZDirOn` | `IgnoredDataArrayPaths` | +|---|---|---|---|---| +| 12 (Erode container) | 1 (Erode) | 2 | 1 / 1 / 1 | `[]` | +| 15 (Dilate container) | 0 (Dilate) | 2 | 1 / 1 / 1 | `[]` | + +These match the arguments the `(Erode)` unit test passes to the SIMPLNX filter, so the comparison is apples-to-apples. + +## Canonical oracle output + +| DataPath | Source of expected values | +|---|---| +| `Exemplar Bad Data Erode/EBSD Scan Data/*` | **Class 2** — legacy SIMPL `ErodeDilateBadData`, Erode, 2 iterations, all directions on (pipeline step 12) | +| `Exemplar Bad Data Dilate/EBSD Scan Data/*` | **Class 2** — legacy SIMPL `ErodeDilateBadData`, Dilate, 2 iterations, all directions on (pipeline step 15) | +| `Input Data/EBSD Scan Data/*` | Not an oracle — filter input | + +`ErodeDilateBadDataTest.cpp` currently consumes only the Erode exemplar. The Dilate exemplar is present in the archive and unused; wiring up a `(Dilate)` production-scale test would close that gap at no data cost. + +## Oracle provenance detail + +### Class 2 — Reference implementation + +- **Reference:** legacy SIMPL `ErodeDilateBadData` filter, UUID `{3adfe077-c3c9-5cd0-ad74-cf5f8ff3d254}` +- **Exact version:** the embedded pipeline records `FilterVersion` `6.6.338` for the two `ErodeDilateBadData` steps and for the surrounding EBSD reconstruction filters. This is **later than the 6.5.171 V&V baseline** — see the caveat below. +- **Runner:** legacy DREAM3D `PipelineRunner` (`DataContainerWriter` at step 17) +- **Pipeline file:** not preserved externally; embedded at `/Pipeline/Pipeline` inside `6_6_erode_dilate_bad_data.dream3d` +- **Input dataset:** first slice of Small IN100 (`SmallIN100.h5ebsd`), not synthetic + +**Baseline caveat.** The archive's `6_6_` prefix and the `6.6.338` filter version mean this exemplar comes from a legacy build *later* than the 6.5.171 baseline this V&V cycle compares against. That does not weaken it as an oracle — it is still genuine legacy output from an independent implementation, and the algorithm did not change between those legacy versions (confirmed by the line-by-line source diff recorded in the V&V report's Algorithm Relationship section). It does mean the archive alone is not a 6.5.171 comparison. The 6.5.171 comparison of record is the separate 28-combination `PipelineRunner` A/B run documented in the V&V report's Oracle section. + +**Direction-gating caveat.** Both exemplars were generated with all three directions enabled, so this archive cannot discriminate direction gating — a SIMPLNX build with `ErodeDilateBadDataFilter-D1` still present passes the `(Erode)` test. That is precisely the gap the 28-combination inline sweep exists to close. Do not treat this archive as sufficient coverage on its own. + +## Second-engineer oracle review + +- **Reviewer:** *pending* — required before the V&V report Status moves to COMPLETE +- **Date:** *pending* +- **Review focus when performed:** the two caveats above (later-than-baseline legacy version; all-directions-on coverage limit), and whether a `(Dilate)` production-scale test should be added from the unused exemplar already in the archive. + +## Regenerated to fix a circular-oracle situation? + +No. This archive was **not** regenerated from SIMPLNX output at any point — the embedded pipeline proves the exemplars were written by the legacy SIMPL filter, not by SIMPLNX. The archive is retained unchanged. + +The circular-oracle concern raised during V&V applied to a different artifact: the 28 inline expected-value constants in `ErodeDilateBadDataTest.cpp`, which an earlier revision of the report described as hand-traced while they were in fact byte-identical across all 7 direction combinations (the signature of `ErodeDilateBadDataFilter-D1`). Those constants were regenerated from the 6.5.171 binary and are now a genuine Class 2 oracle. See the V&V report's Oracle section.