From bcedd816026f923b791155d3a64e4604dd10ddd8 Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Wed, 22 Apr 2026 07:50:06 -0400 Subject: [PATCH 1/6] 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 7e5cde97fb8cfa0d9e9c7b036f2d9df49a5015bd Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Tue, 9 Jun 2026 12:42:34 -0400 Subject: [PATCH 2/6] 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 83a5ddf2ec98ceb836744a8c1f75c02dda42d33a Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Fri, 12 Jun 2026 11:11:30 -0400 Subject: [PATCH 3/6] 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 6fa173997275734dd53480388c0ff29b4028a08d Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Fri, 12 Jun 2026 11:29:25 -0400 Subject: [PATCH 4/6] 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 52a6f435893ace1fad673adbd188ccfed97d0cd5 Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Thu, 23 Jul 2026 16:06:31 -0400 Subject: [PATCH 5/6] 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 0ca6ab44a7..1647f6705e 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 f4655103579ae1a53be900cb25da4b852c35095e Mon Sep 17 00:00:00 2001 From: Matthew Marine Date: Thu, 23 Jul 2026 17:04:38 -0400 Subject: [PATCH 6/6] 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 1647f6705e..c79c641a3c 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")}; }