From 5f1c7a65958e9f49553aa94379f19a5e33149880 Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Tue, 28 Jul 2026 10:43:06 -0400 Subject: [PATCH 1/5] Removed unused variables Signed-off-by: Jared Duffey --- .../src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp index 6d625dfc69..2ce8a9a4e8 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp @@ -119,10 +119,8 @@ IFilter::PreflightResult RequireMinNumNeighborsFilter::preflightImpl(const DataS std::vector dataArrayPaths; - ShapeType cDims = {1}; auto& featureIds = dataStructure.getDataRefAs(featureIdsPath); - auto& numNeighborsArray = dataStructure.getDataRefAs(numNeighborsPath); dataArrayPaths.push_back(numNeighborsPath); if(applyToSinglePhase) From fed77d7505a82ddf3331f230be64e96f7de1cca4 Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Tue, 28 Jul 2026 10:43:06 -0400 Subject: [PATCH 2/5] Added feature ids/image geom tuple mismatch check Signed-off-by: Jared Duffey --- .../SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp index 2ce8a9a4e8..04dd3018a7 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp @@ -19,6 +19,7 @@ namespace nx::core namespace { constexpr int32 k_InconsistentTupleCount = -252; +constexpr int32 k_FeatureIdsTupleCountMismatch = -55571; } // namespace @@ -120,6 +121,13 @@ IFilter::PreflightResult RequireMinNumNeighborsFilter::preflightImpl(const DataS std::vector dataArrayPaths; auto& featureIds = dataStructure.getDataRefAs(featureIdsPath); + auto& imageGeom = dataStructure.getDataRefAs(imageGeomPath); + if(featureIds.getNumberOfTuples() != imageGeom.getNumberOfCells()) + { + return MakePreflightErrorResult(k_FeatureIdsTupleCountMismatch, + fmt::format("FeatureIds array '{}' contains {} tuples, but Image Geometry '{}' contains {} cells. Select a FeatureIds array that matches the Image Geometry.", + featureIdsPath.toString(), featureIds.getNumberOfTuples(), imageGeomPath.toString(), imageGeom.getNumberOfCells())); + } dataArrayPaths.push_back(numNeighborsPath); From 12f51b514c606e9104a52cc451b3196ff8afb94b Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Tue, 28 Jul 2026 10:43:06 -0400 Subject: [PATCH 3/5] Moved feature id range check to initial iteration to prevent OOB Signed-off-by: Jared Duffey --- .../Algorithms/RequireMinNumNeighbors.cpp | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RequireMinNumNeighbors.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RequireMinNumNeighbors.cpp index af442599fb..d50f984a53 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RequireMinNumNeighbors.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RequireMinNumNeighbors.cpp @@ -148,7 +148,18 @@ Result<> RequireMinNumNeighbors::operator()() // Mark all features to be removed with a -1 value. for(usize i = 0; i < totalPoints; i++) { - int32 featureId = featureIds[i]; + const int32 featureId = featureIds[i]; + + if(featureId < 0) + { + continue; + } + + if(static_cast(featureId) >= totalFeatures) + { + return MakeErrorResult(-55567, fmt::format("Feature ID '{}' in array '{}' is outside the valid range [0, {}).", featureId, m_InputValues->FeatureIdsPath.toString(), totalFeatures)); + } + if(!activeObjects[featureId]) { featureIds[i] = -1; @@ -235,12 +246,6 @@ Result<> RequireMinNumNeighbors::operator()() // Reset the voteCount array to all zeros std::fill(voteCount.begin(), voteCount.end(), 0); } - else if(featureName >= numFeatures) - { - std::string message = fmt::format("Error: Found a feature Id '{}' that is >= the number of features '{}' at voxel index X={},Y={},Z={}.", featureName, numFeatures, xIdx, yIdx, zIdx); - m_MessageHandler(nx::core::IFilter::Message{nx::core::IFilter::Message::Type::Info, message}); - return MakeErrorResult(-55567, message); - } } } } From 3b18263614db82ecd562c8923d0558d21fe98bde Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Tue, 28 Jul 2026 10:43:07 -0400 Subject: [PATCH 4/5] Added defensive checks Signed-off-by: Jared Duffey --- .../Filters/Algorithms/RequireMinNumNeighbors.cpp | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RequireMinNumNeighbors.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RequireMinNumNeighbors.cpp index d50f984a53..c114bc4a05 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RequireMinNumNeighbors.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RequireMinNumNeighbors.cpp @@ -267,7 +267,11 @@ Result<> RequireMinNumNeighbors::operator()() { return {}; } - CopyTupleFromArray(m_DataStructure, cellArrayPath, badFeatureIdIndexes, featureIds, neighbors, m_MessageHandler); + auto copyResult = CopyTupleFromArray(m_DataStructure, cellArrayPath, badFeatureIdIndexes, featureIds, neighbors, m_MessageHandler); + if(copyResult.invalid()) + { + return copyResult; + } } } @@ -282,7 +286,11 @@ Result<> RequireMinNumNeighbors::operator()() m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Feature Count Changed: Previous: {} New: {}", totalFeatures, count)); DataPath cellFeatureGroupPath = m_InputValues->NumNeighborsPath.getParent(); - nx::core::RemoveInactiveObjects(m_DataStructure, cellFeatureGroupPath, activeObjects, featureIds, totalFeatures, m_MessageHandler, m_ShouldCancel); + if(!nx::core::RemoveInactiveObjects(m_DataStructure, cellFeatureGroupPath, activeObjects, featureIds, totalFeatures, m_MessageHandler, m_ShouldCancel)) + { + return MakeErrorResult(-55570, fmt::format("Failed to remove inactive feature tuples from feature group '{}'. Check that its arrays match the tuple count of '{}'.", + cellFeatureGroupPath.toString(), m_InputValues->NumNeighborsPath.toString())); + } return {}; } From 4aa82bc5461ec462356f2637c65ad66c46865f11 Mon Sep 17 00:00:00 2001 From: Jared Duffey Date: Tue, 28 Jul 2026 10:43:07 -0400 Subject: [PATCH 5/5] VV: "Require Minimum Number of Neighbors" fully V&V'ed Summary: - Confirmed no bugs (Minor changes port) - Documented 0 deviations from DREAM3D 6.5.171 - Retired 0 tests - Augmented existing tests with 6 new test cases including a templated Class 1 (Analytical) + Class 4 (Invariant) oracle - Added V&V source-tree deliverables (report, deviations) - Added several new error checks - Removed some unused code Signed-off-by: Jared Duffey --- .../test/RequireMinNumNeighborsTest.cpp | 297 +++++++++++++++++- .../vv/RequireMinNumNeighborsFilter.md | 133 ++++++++ .../RequireMinNumNeighborsFilter.md | 3 + 3 files changed, 432 insertions(+), 1 deletion(-) create mode 100644 src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md create mode 100644 src/Plugins/SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md diff --git a/src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTest.cpp b/src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTest.cpp index f5f21de4cb..83b34536cf 100644 --- a/src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTest.cpp +++ b/src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTest.cpp @@ -13,6 +13,7 @@ #include +#include #include #include #include @@ -23,7 +24,6 @@ using namespace nx::core::Constants; namespace { - const std::vector k_NumberElements = { 0, 1858, 152, 31, 70, 4183, 691, 8359, 646, 38, 124, 1064, 25, 34, 640, 1361, 74, 4014, 4, 129, 952, 2623, 17, 667, 6, 724, 3709, 344, 1149, 432, 114, 439, 25, 64, 591, 1, 191, 6752, 374, 29, 32, 2695, 28, 44, 88, 231, 22, 412, 62, 180, 2371, 681, 753, 286, 1818, 50, 744, 648, 2464, 150, 277, 1303, @@ -54,6 +54,93 @@ const std::vector k_NumberElements = { } // namespace +TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter: Analytical Oracle", "[SimplnxCore][RequireMinNumNeighborsFilter]") +{ + UnitTest::LoadPlugins(); + + const bool singlePhase = GENERATE(false, true); + DYNAMIC_SECTION("ApplyToSinglePhase=" << singlePhase) + { + // Class 1 oracle: feature 1 occupies the two middle cells of [2, 1, 1, 3] + // and is rejected. Its left/right face-neighbor votes select features 2 and 3, + // yielding FeatureIds [1, 1, 2, 2] after inactive-feature removal remaps IDs. + DataStructure dataStructure; + + const SizeVec3 imageSize = {4, 1, 1}; + const ShapeType arraySize(std::reverse_iterator(imageSize.end()), std::reverse_iterator(imageSize.begin())); + + auto* imageGeom = ImageGeom::Create(dataStructure, "ImageGeometry"); + imageGeom->setDimensions(imageSize); + auto* cellAM = AttributeMatrix::Create(dataStructure, "CellData", arraySize, imageGeom->getId()); + imageGeom->setCellData(*cellAM); + auto* featureAM = AttributeMatrix::Create(dataStructure, "FeatureData", {4}, imageGeom->getId()); + + auto* featureIds = UnitTest::CreateTestDataArray(dataStructure, "FeatureIds", arraySize, {1}, cellAM->getId()); + auto* copiedValues = UnitTest::CreateTestDataArray(dataStructure, "CopiedValues", arraySize, {1}, cellAM->getId()); + auto* ignoredValues = UnitTest::CreateTestDataArray(dataStructure, "IgnoredValues", arraySize, {1}, cellAM->getId()); + auto* numNeighbors = UnitTest::CreateTestDataArray(dataStructure, "NumNeighbors", {4}, {1}, featureAM->getId()); + auto* phases = UnitTest::CreateTestDataArray(dataStructure, "Phases", {4}, {1}, featureAM->getId()); + + const std::array inputFeatureIds = {2, 1, 1, 3}; + const std::array inputCopiedValues = {20, 101, 102, 30}; + const std::array inputIgnoredValues = {200, 101, 102, 300}; + const std::array inputNumNeighbors = singlePhase ? std::array{0, 0, 0, 3} : std::array{0, 0, 3, 3}; + const std::array inputPhases = {0, 1, 2, 1}; + for(usize i = 0; i < inputFeatureIds.size(); i++) + { + featureIds->getDataStoreRef()[i] = inputFeatureIds[i]; + copiedValues->getDataStoreRef()[i] = inputCopiedValues[i]; + ignoredValues->getDataStoreRef()[i] = inputIgnoredValues[i]; + numNeighbors->getDataStoreRef()[i] = inputNumNeighbors[i]; + phases->getDataStoreRef()[i] = inputPhases[i]; + } + + RequireMinNumNeighborsFilter filter; + Arguments args; + args.insertOrAssign(RequireMinNumNeighborsFilter::k_MinNumNeighbors_Key, std::make_any(2)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_ApplyToSinglePhase_Key, std::make_any(singlePhase)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_PhaseNumber_Key, std::make_any(1)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"ImageGeometry"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_FeatureIdsPath_Key, std::make_any(DataPath({"ImageGeometry", "CellData", "FeatureIds"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_FeaturePhasesPath_Key, std::make_any(DataPath({"ImageGeometry", "FeatureData", "Phases"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_NumNeighborsPath_Key, std::make_any(DataPath({"ImageGeometry", "FeatureData", "NumNeighbors"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_IgnoredVoxelArrays_Key, std::make_any>(std::vector{DataPath({"ImageGeometry", "CellData", "IgnoredValues"})})); + + SIMPLNX_RESULT_REQUIRE_VALID(filter.preflight(dataStructure, args).outputActions); + SIMPLNX_RESULT_REQUIRE_VALID(filter.execute(dataStructure, args).result); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({"ImageGeometry", "CellData", "FeatureIds"}))); + const auto& outputFeatureIds = dataStructure.getDataRefAs(DataPath({"ImageGeometry", "CellData", "FeatureIds"})); + const auto& outputCopiedValues = dataStructure.getDataRefAs(DataPath({"ImageGeometry", "CellData", "CopiedValues"})); + const auto& outputIgnoredValues = dataStructure.getDataRefAs(DataPath({"ImageGeometry", "CellData", "IgnoredValues"})); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({"ImageGeometry", "FeatureData", "NumNeighbors"}))); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({"ImageGeometry", "FeatureData", "Phases"}))); + const auto& outputNumNeighbors = dataStructure.getDataRefAs(DataPath({"ImageGeometry", "FeatureData", "NumNeighbors"})); + const auto& outputPhases = dataStructure.getDataRefAs(DataPath({"ImageGeometry", "FeatureData", "Phases"})); + const std::array expectedFeatureIds = {1, 1, 2, 2}; + const std::array expectedCopiedValues = {20, 20, 30, 30}; + const std::array expectedNumNeighbors = singlePhase ? std::array{0, 0, 3} : std::array{0, 3, 3}; + const std::array expectedPhases = {0, 2, 1}; + for(usize i = 0; i < expectedFeatureIds.size(); i++) + { + CHECK(outputFeatureIds[i] == expectedFeatureIds[i]); + CHECK(outputCopiedValues[i] == expectedCopiedValues[i]); + CHECK(outputIgnoredValues[i] == inputIgnoredValues[i]); + // Class 4 invariants: reassignment leaves no rejected IDs and IDs remain valid. + CHECK(outputFeatureIds[i] >= 0); + CHECK(outputFeatureIds[i] < 3); + } + REQUIRE(outputNumNeighbors.getNumberOfTuples() == expectedNumNeighbors.size()); + REQUIRE(outputPhases.getNumberOfTuples() == expectedPhases.size()); + for(usize i = 0; i < expectedNumNeighbors.size(); i++) + { + CHECK(outputNumNeighbors[i] == expectedNumNeighbors[i]); + CHECK(outputPhases[i] == expectedPhases[i]); + } + UnitTest::CheckArraysInheritTupleDims(dataStructure); + } +} + TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter", "[SimplnxCore][RequireMinNumNeighborsFilter]") { UnitTest::LoadPlugins(); @@ -118,6 +205,8 @@ TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter", "[SimplnxCore][RequireMin // Preflight the filter and check result auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.warnings().size() == 1); + CHECK(preflightResult.outputActions.warnings()[0].code == -5558); // Execute the filter and check the result auto executeResult = filter.execute(dataStructure, args); @@ -268,6 +357,212 @@ TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - tuple co REQUIRE(preflightResult.outputActions.errors()[0].code == -252); } +TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - FeatureIds tuple count mismatch (-55571)", "[SimplnxCore][RequireMinNumNeighborsFilter][preflight]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + auto* imageGeom = ImageGeom::Create(dataStructure, "ImageGeometry"); + imageGeom->setDimensions({5, 1, 1}); + auto* cellAM = AttributeMatrix::Create(dataStructure, "CellData", {1, 1, 4}, imageGeom->getId()); + imageGeom->setCellData(*cellAM); + auto* featureAM = AttributeMatrix::Create(dataStructure, "FeatureData", {4}, imageGeom->getId()); + UnitTest::CreateTestDataArray(dataStructure, "FeatureIds", {1, 1, 4}, {1}, cellAM->getId()); + UnitTest::CreateTestDataArray(dataStructure, "NumNeighbors", {4}, {1}, featureAM->getId()); + + RequireMinNumNeighborsFilter filter; + Arguments args; + args.insertOrAssign(RequireMinNumNeighborsFilter::k_MinNumNeighbors_Key, std::make_any(0)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_ApplyToSinglePhase_Key, std::make_any(false)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_PhaseNumber_Key, std::make_any(0)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"ImageGeometry"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_FeatureIdsPath_Key, std::make_any(DataPath({"ImageGeometry", "CellData", "FeatureIds"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_NumNeighborsPath_Key, std::make_any(DataPath({"ImageGeometry", "FeatureData", "NumNeighbors"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_IgnoredVoxelArrays_Key, std::make_any>(std::vector{})); + + const auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors()[0].code == -55571); +} + +TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - feature ID out of range (-55567)", "[SimplnxCore][RequireMinNumNeighborsFilter][execute]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + + const SizeVec3 imageSize = {4, 1, 1}; + const ShapeType arraySize(std::reverse_iterator(imageSize.end()), std::reverse_iterator(imageSize.begin())); + + auto* imageGeom = ImageGeom::Create(dataStructure, "ImageGeometry"); + imageGeom->setDimensions(imageSize); + auto* cellAM = AttributeMatrix::Create(dataStructure, "CellData", arraySize, imageGeom->getId()); + imageGeom->setCellData(*cellAM); + auto* featureAM = AttributeMatrix::Create(dataStructure, "FeatureData", {4}, imageGeom->getId()); + + auto* featureIds = UnitTest::CreateTestDataArray(dataStructure, "FeatureIds", arraySize, {1}, cellAM->getId()); + auto* numNeighbors = UnitTest::CreateTestDataArray(dataStructure, "NumNeighbors", {4}, {1}, featureAM->getId()); + const std::array inputFeatureIds = {1, 4, 2, 3}; + const std::array inputNumNeighbors = {0, 0, 3, 3}; + for(usize i = 0; i < inputFeatureIds.size(); i++) + { + featureIds->getDataStoreRef()[i] = inputFeatureIds[i]; + numNeighbors->getDataStoreRef()[i] = inputNumNeighbors[i]; + } + + RequireMinNumNeighborsFilter filter; + Arguments args; + args.insertOrAssign(RequireMinNumNeighborsFilter::k_MinNumNeighbors_Key, std::make_any(2)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_ApplyToSinglePhase_Key, std::make_any(false)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_PhaseNumber_Key, std::make_any(0)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"ImageGeometry"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_FeatureIdsPath_Key, std::make_any(DataPath({"ImageGeometry", "CellData", "FeatureIds"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_NumNeighborsPath_Key, std::make_any(DataPath({"ImageGeometry", "FeatureData", "NumNeighbors"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_IgnoredVoxelArrays_Key, std::make_any>(std::vector{})); + + SIMPLNX_RESULT_REQUIRE_VALID(filter.preflight(dataStructure, args).outputActions); + const auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); + REQUIRE(executeResult.result.errors()[0].code == -55567); +} + +TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter: Execute - negative feature ID is reassigned", "[SimplnxCore][RequireMinNumNeighborsFilter][execute]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + + const SizeVec3 imageSize = {4, 1, 1}; + const ShapeType arraySize(std::reverse_iterator(imageSize.end()), std::reverse_iterator(imageSize.begin())); + + auto* imageGeom = ImageGeom::Create(dataStructure, "ImageGeometry"); + imageGeom->setDimensions(imageSize); + auto* cellAM = AttributeMatrix::Create(dataStructure, "CellData", arraySize, imageGeom->getId()); + imageGeom->setCellData(*cellAM); + auto* featureAM = AttributeMatrix::Create(dataStructure, "FeatureData", {3}, imageGeom->getId()); + + auto* featureIds = UnitTest::CreateTestDataArray(dataStructure, "FeatureIds", arraySize, {1}, cellAM->getId()); + auto* numNeighbors = UnitTest::CreateTestDataArray(dataStructure, "NumNeighbors", {3}, {1}, featureAM->getId()); + const std::array inputFeatureIds = {-1, 1, 1, 2}; + const std::array inputNumNeighbors = {0, 3, 3}; + for(usize i = 0; i < inputFeatureIds.size(); i++) + { + featureIds->getDataStoreRef()[i] = inputFeatureIds[i]; + if(i < inputNumNeighbors.size()) + { + numNeighbors->getDataStoreRef()[i] = inputNumNeighbors[i]; + } + } + + RequireMinNumNeighborsFilter filter; + Arguments args; + args.insertOrAssign(RequireMinNumNeighborsFilter::k_MinNumNeighbors_Key, std::make_any(2)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_ApplyToSinglePhase_Key, std::make_any(false)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_PhaseNumber_Key, std::make_any(0)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"ImageGeometry"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_FeatureIdsPath_Key, std::make_any(DataPath({"ImageGeometry", "CellData", "FeatureIds"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_NumNeighborsPath_Key, std::make_any(DataPath({"ImageGeometry", "FeatureData", "NumNeighbors"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_IgnoredVoxelArrays_Key, std::make_any>(std::vector{})); + + SIMPLNX_RESULT_REQUIRE_VALID(filter.preflight(dataStructure, args).outputActions); + const auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({"ImageGeometry", "CellData", "FeatureIds"}))); + const auto& outputFeatureIds = dataStructure.getDataRefAs(DataPath({"ImageGeometry", "CellData", "FeatureIds"})); + const std::array expectedFeatureIds = {1, 1, 1, 2}; + for(usize i = 0; i < expectedFeatureIds.size(); i++) + { + CHECK(outputFeatureIds[i] == expectedFeatureIds[i]); + } +} + +TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - unavailable phase (-5555)", "[SimplnxCore][RequireMinNumNeighborsFilter][execute]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + + const SizeVec3 imageSize = {4, 1, 1}; + const ShapeType arraySize(std::reverse_iterator(imageSize.end()), std::reverse_iterator(imageSize.begin())); + + auto* imageGeom = ImageGeom::Create(dataStructure, "ImageGeometry"); + imageGeom->setDimensions(imageSize); + auto* cellAM = AttributeMatrix::Create(dataStructure, "CellData", arraySize, imageGeom->getId()); + imageGeom->setCellData(*cellAM); + auto* featureAM = AttributeMatrix::Create(dataStructure, "FeatureData", {4}, imageGeom->getId()); + + auto* featureIds = UnitTest::CreateTestDataArray(dataStructure, "FeatureIds", arraySize, {1}, cellAM->getId()); + auto* numNeighbors = UnitTest::CreateTestDataArray(dataStructure, "NumNeighbors", {4}, {1}, featureAM->getId()); + auto* phases = UnitTest::CreateTestDataArray(dataStructure, "Phases", {4}, {1}, featureAM->getId()); + const std::array inputFeatureIds = {2, 1, 1, 3}; + const std::array inputNumNeighbors = {0, 0, 3, 3}; + const std::array inputPhases = {0, 1, 2, 1}; + for(usize i = 0; i < inputFeatureIds.size(); i++) + { + featureIds->getDataStoreRef()[i] = inputFeatureIds[i]; + numNeighbors->getDataStoreRef()[i] = inputNumNeighbors[i]; + phases->getDataStoreRef()[i] = inputPhases[i]; + } + + RequireMinNumNeighborsFilter filter; + Arguments args; + args.insertOrAssign(RequireMinNumNeighborsFilter::k_MinNumNeighbors_Key, std::make_any(2)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_ApplyToSinglePhase_Key, std::make_any(true)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_PhaseNumber_Key, std::make_any(5)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"ImageGeometry"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_FeatureIdsPath_Key, std::make_any(DataPath({"ImageGeometry", "CellData", "FeatureIds"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_FeaturePhasesPath_Key, std::make_any(DataPath({"ImageGeometry", "FeatureData", "Phases"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_NumNeighborsPath_Key, std::make_any(DataPath({"ImageGeometry", "FeatureData", "NumNeighbors"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_IgnoredVoxelArrays_Key, std::make_any>(std::vector{})); + + SIMPLNX_RESULT_REQUIRE_VALID(filter.preflight(dataStructure, args).outputActions); + const auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); + REQUIRE(executeResult.result.errors()[0].code == -5555); +} + +TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - all features rejected (-55569)", "[SimplnxCore][RequireMinNumNeighborsFilter][execute]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + + const SizeVec3 imageSize = {4, 1, 1}; + const ShapeType arraySize(std::reverse_iterator(imageSize.end()), std::reverse_iterator(imageSize.begin())); + + auto* imageGeom = ImageGeom::Create(dataStructure, "ImageGeometry"); + imageGeom->setDimensions(imageSize); + auto* cellAM = AttributeMatrix::Create(dataStructure, "CellData", arraySize, imageGeom->getId()); + imageGeom->setCellData(*cellAM); + auto* featureAM = AttributeMatrix::Create(dataStructure, "FeatureData", {4}, imageGeom->getId()); + + auto* featureIds = UnitTest::CreateTestDataArray(dataStructure, "FeatureIds", arraySize, {1}, cellAM->getId()); + auto* numNeighbors = UnitTest::CreateTestDataArray(dataStructure, "NumNeighbors", {4}, {1}, featureAM->getId()); + const std::array inputFeatureIds = {1, 2, 3, 1}; + const std::array inputNumNeighbors = {0, 0, 0, 0}; + for(usize i = 0; i < inputFeatureIds.size(); i++) + { + featureIds->getDataStoreRef()[i] = inputFeatureIds[i]; + numNeighbors->getDataStoreRef()[i] = inputNumNeighbors[i]; + } + + RequireMinNumNeighborsFilter filter; + Arguments args; + args.insertOrAssign(RequireMinNumNeighborsFilter::k_MinNumNeighbors_Key, std::make_any(1)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_ApplyToSinglePhase_Key, std::make_any(false)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_PhaseNumber_Key, std::make_any(0)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"ImageGeometry"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_FeatureIdsPath_Key, std::make_any(DataPath({"ImageGeometry", "CellData", "FeatureIds"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_NumNeighborsPath_Key, std::make_any(DataPath({"ImageGeometry", "FeatureData", "NumNeighbors"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_IgnoredVoxelArrays_Key, std::make_any>(std::vector{})); + + SIMPLNX_RESULT_REQUIRE_VALID(filter.preflight(dataStructure, args).outputActions); + const auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); + REQUIRE(executeResult.result.errors()[0].code == -55569); +} + TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter: SIMPL Backwards Compatibility", "[SimplnxCore][RequireMinNumNeighborsFilter][BackwardsCompatibility]") { auto app = Application::GetOrCreateInstance(); diff --git a/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md b/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md new file mode 100644 index 0000000000..8a3d5b7a1b --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md @@ -0,0 +1,133 @@ +# V&V Report: RequireMinNumNeighborsFilter + +| | | +|---|---| +| Plugin | SimplnxCore | +| SIMPLNX UUID | `4ab5153f-6014-4e6d-bbd6-194068620389` | +| DREAM3D 6.5.171 equivalent | `MinNeighbors` (SIMPL UUID `dab5de3c-5f81-5bb5-8490-73521e1183ea`) - `Source/Plugins/Processing/ProcessingFilters/MinNeighbors.{h,cpp}` | +| Verified commit | ** | +| Status | READY FOR REVIEW | +| Sign-off | ** | + +## At a glance + +| Aspect | Current state | +|---|---| +| Algorithm Relationship | **Minor changes** - the NX port retains the legacy selection, coarsening, and feature-removal while incorporating bug fixes and minor code improvements. | +| Oracle (confirmed) | **Class 1 analytical + Class 4 invariants** - two 4x1x1 fixtures check cell and compacted feature arrays. | +| Code paths enumerated | 12 of 16 paths are assertion-covered; cancellation and defensive error paths are not covered. | +| Tests today | 9 active test cases: analytical all/single-phase execution, three execute errors, negative-FeatureId reassignment, two preflight errors, Small IN100 regression test, and SIMPL conversion. | +| Exemplar archive | No V&V output archive is needed because the oracle is inline; the retained `6_5_test_data_1_v2.tar.gz` input archive supports only the Small IN100 regression test. | +| Legacy comparison | **Run** - NX and DREAM3D 6.5.171 matched all five arrays for the oracle fixture. | +| Bug flags | None | +| V&V phase | Ready for review | + +## Summary + +`RequireMinNumNeighborsFilter` removes features below a neighbor-count threshold, fills their voxels from dominant valid face neighbors, and compacts feature-level arrays. Verification uses a hand-derived analytical fixture with invariants, focused error tests, a Small IN100 regression test, SIMPL conversion checks, and a DREAM3D 6.5.171 comparison. The analytical all-phase comparison is exactly matched; report status remains DRAFT for the outstanding gates listed below. + +## Algorithm Relationship + +*Classification:* Port | **Minor changes** | Rewrite | New filter + +*Evidence:* The NX algorithm retains the legacy `MinNeighbors` selection, voxel reassignment, and inactive-feature-removal. It replaces direct neighbor indexing with shared utilities and incorporates fixes for ignored arrays, data-array updates, and invalid feature IDs. During this V&V cycle, negative FeatureIds skip initial marking and enter the reassignment pass, while out-of-range IDs return `-55567` before they are used for `activeObjects` indexing. + +*PR(s):* + +- PR #154 ("Added MinNeighbors filter") - introduced the filter as `MinNeighbors` in `ComplexCore`. +- PR #183 ("Added ability to specify initial value of DataStore") - updated data-store initialization APIs. +- PR #226 ("Add optional parameters that skip validation when inactive") - made optional-parameter validation conditional. +- PR #266 ("Refactored geometry hierarchy") - adapted the filter to the geometry-hierarchy refactor. +- PR #272 ("Filter Parameter Organization and Code Cleanup") - reorganized parameters and cleaned filter code. +- PR #275 ("Filter and Unit Test Fixes") - applied general filter and test corrections. +- PR #299 ("Geometry: Update classes to reuse functionality from higher level classes and rename methods") - updated geometry API usage. +- PR #346 ("DataPathSelectionUpdates") - migrated data-path selection parameters. +- PR #349 ("DOCS: Update paths and CMake codes to prepare for documentation updates") - updated documentation and build-path references. +- PR #351 ("Add Filter Comments") - added filter comment support. +- PR #378 ("MultiArraySelectionParameter Updates") - updated multi-array selection parameter handling. +- PR #437 ("BUG: Fix filter default tags") - corrected default filter tags. +- PR #671 ("API: Add C++ Class Name to All Default Tags") - added the C++ class name to default tags. +- PR #684 ("ENH: Improve the Tuple Count validation error reporting") - improved tuple-count validation diagnostics. +- PR #735 ("ENH: Create DataModifiedAction that marks DataObjects as being modified by a filter") - adopted modified-data action tracking. +- PR #779 ("ENH: Implement SIMPL pipeline conversion") - added SIMPL pipeline conversion. +- PR #801 ("ENH: Rename complex to simplnx") - moved the filter from `ComplexCore` to `SimplnxCore`. +- PR #874 ("ENH: Refactor the Parameter Keys to make them consistent and easy to learn") - renamed parameter keys. +- PR #926 ("BUG: Filters that delete NeighborLists from the DataStructure send strong warning messages") - added NeighborList-removal warnings. +- PR #931 ("ENH: All filter's class names end with \"Filter\".") - renamed `MinNeighbors` to `MinNeighborsFilter`. +- PR #934 ("BUG: Pipeline and Filter human facing label cleanup") - corrected the filter's user-facing label. +- PR #956 ("ENH: Rename Filters that start with Find/Generate/Calculate to Compute") - renamed `MinNeighborsFilter` to `RequireMinNumNeighborsFilter`. +- PR #980 ("ENH: Update docs for filters that change FeatureIds to warn user of invalid feature attribute matrix") - added invalid feature-attribute-matrix guidance. +- PR #1017 ("ENH/BUG: Data Array to Store, Speed Optimizations, Code Cleanup (SimplnxCore)") - migrated array access to data stores and cleaned code. +- PR #1082 ("SIMPLConversion header optimization") - optimized SIMPL conversion includes. +- PR #1088 ("Added versioning to filter parameters and json") - added parameter and JSON versioning. +- PR #1238 ("ENH: Added pipeline relative path support") - added relative-path pipeline support. +- PR #1249 ("COMP: Misc. compiler warning cleanups.") - resolved compiler warnings. +- PR #1278 ("BUG: Ensure FeatureId arrays are range checked against the Feature Attribute Matrix.") - added FeatureId range validation. +- PR #1310 ("BUG: Fix RequireMinNumNeighbors Not Using Ignore Paths") - corrected ignored voxel-array handling and introduced the separate algorithm class. +- PR #1320 ("BUG: RequireMinNumNeighbors DataArray Update fixes") - corrected cell-data updates during reassignment. +- PR #1343 ("BUG: Fix out-of-bounds array access in RequireMinNumNeighbors Filter") - added invalid FeatureId error handling. +- PR #1377 ("STY: Ensure all code arguments are consistent across filters") - standardized filter argument style. +- PR #1439 ("ENH/API: Multi-Dimensional Tuple Support for StringArray and NeighborList") - adapted to multidimensional NeighborList APIs. +- PR #1523 ("ENH: Factor out the 6-face neighbor code that is systemic through out the code base") - extracted shared six-face neighbor utilities. +- PR #1535 ("ENH: Remove redundant preflight checks that are already done in the parameter") - removed duplicated preflight checks. +- PR #1590 ("ENH: Standardize 2D Image Handling") - standardized shared image-neighbor behavior. +- PR #1682 ("BUG: Fix SIMPL pipeline conversion bugs and Write Image naming") - corrected optional SIMPL conversion argument handling. + +## Oracle + +*Class:* **1 (Analytical)** primary + **4 (Invariant)** companion. + +*Applied:* A hand-built 4x1x1 fixture has FeatureIds `[2, 1, 1, 3]`. Feature 1 is below the threshold and its two rejected voxels have unique valid left and right face neighbors, so coarsening must copy feature 2 then feature 3. After inactive-feature removal remaps IDs, the exact FeatureIds are `[1, 1, 2, 2]`; a copied cell array is `[20, 20, 30, 30]`, while an ignored array remains `[200, 101, 102, 300]`. The compacted all-phase feature arrays are NumNeighbors `[0, 3, 3]` and Phases `[0, 2, 1]`; in single-phase mode NumNeighbors is `[0, 0, 3]` because nonselected feature 2 remains active. The fixture runs in both modes. + +*Encoded:* `test/RequireMinNumNeighborsTest.cpp::SimplnxCore::RequireMinNumNeighborsFilter: Analytical Oracle` - two fixtures, both passing in the available in-core build. Class 4 asserts nonnegative, in-range final FeatureIds; Class 1 asserts exact compacted NumNeighbors and Phases values. + +*Second-engineer review:* pending second-engineer review. + +## Code path coverage + +12 of 16 paths are assertion-covered. Cancellation and defensive infrastructure/error paths are documented gaps. + +Source: `src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/RequireMinNumNeighbors.cpp` (296 lines), plus preflight validation in `Filters/RequireMinNumNeighborsFilter.cpp`. + +The filter selects features to retain, marks rejected-feature voxels, iteratively copies a dominant face neighbor into each rejected voxel, and removes inactive feature tuples. + +| # | Phase | Path | Test case | +|---|---|---|---| +| 1 | Preflight | Feature-level `NumNeighbors` and, when enabled, `FeaturePhases` tuple counts disagree - return `-252`. | `SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - tuple count mismatch (-252)` | +| 2 | Preflight | FeatureIds tuple count differs from the selected Image Geometry cell count - return `-55571`. | `SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - FeatureIds tuple count mismatch (-55571)` | +| 3 | Preflight | NeighborList-removal preflight warning is produced when feature NeighborLists are present. | `SimplnxCore::RequireMinNumNeighborsFilter` - asserts only one `-5558` warning. | +| 4 | Setup | Cell-data array discovery fails - return `-5556`. | *Not directly tested. Defensive check.* | +| 5 | Selection | Single-phase mode names a phase absent from `FeaturePhases` - return `-5555`. | `SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - unavailable phase (-5555)` | +| 6 | Selection | Every eligible feature is below the neighbor threshold - return `-55569` before mutation. | `SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - all features rejected (-55569)` | +| 7 | Selection | All-phase or selected-phase feature meets threshold; nonselected phases remain active. | `SimplnxCore::RequireMinNumNeighborsFilter: Analytical Oracle` - `ApplyToSinglePhase=0` and `ApplyToSinglePhase=1` | +| 8 | Cancellation | Cancellation before mutation or within coarsening returns without further changes. | *Not directly tested. Requires cancel signal injection.* | +| 9 | Marking | Rejected feature IDs are marked and retained feature IDs remain available for coarsening. | `SimplnxCore::RequireMinNumNeighborsFilter: Analytical Oracle` | +| 10 | Reassignment | Rejected voxel chooses the most frequent valid face-neighbor feature, including boundary-face rejection. | `SimplnxCore::RequireMinNumNeighborsFilter: Analytical Oracle` | +| 11 | Reassignment | Feature ID is outside the feature-array range - return `-55567` before it is used for indexing. | `SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - feature ID out of range (-55567)` | +| 12 | Reassignment | Negative Feature IDs skip initial marking and are reassigned from valid face neighbors. | `SimplnxCore::RequireMinNumNeighborsFilter: Execute - negative feature ID is reassigned` | +| 13 | Copy | A rejected voxel copies every nonignored cell-data tuple from its selected neighbor. | `SimplnxCore::RequireMinNumNeighborsFilter: Analytical Oracle` | +| 14 | Copy | A cell array with an out-of-range source or destination tuple returns `-55568`. | *Not directly tested. AttributeMatrix construction rejects mismatched tuple shapes before execution. Defensive check.* | +| 15 | Finalize | Inactive feature tuples are removed and remaining FeatureIds are remapped; NeighborLists are removed. | `SimplnxCore::RequireMinNumNeighborsFilter` and `SimplnxCore::RequireMinNumNeighborsFilter: Analytical Oracle` | +| 16 | Finalize | Inactive-feature removal fails - return `-55570`. | *Not directly tested. Defensive check.* | + +## Test inventory + +| Test case | Status | Notes | +|---|---|---| +| `SimplnxCore::RequireMinNumNeighborsFilter: Analytical Oracle` | new-for-V&V | Builds the Class 1+4 fixture and checks exact FeatureIds, copied and ignored arrays, ID invariants, and feature-array compaction. | +| `SimplnxCore::RequireMinNumNeighborsFilter` | kept | Loads `6_5_test_data_1_v2.tar.gz`, computes neighbors, runs the filter, and checks feature tuples plus every `NumElements` value. | +| `SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - tuple count mismatch (-252)` | kept | Asserts invalid preflight and `-252` code for mismatched feature-level arrays. | +| `SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - FeatureIds tuple count mismatch (-55571)` | new-for-V&V | Asserts invalid preflight and exact `-55571` code. | +| `SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - feature ID out of range (-55567)` | new-for-V&V | Asserts error for rejected out of range IDs. | +| `SimplnxCore::RequireMinNumNeighborsFilter: Execute - negative feature ID is reassigned` | new-for-V&V | Asserts a negative FeatureId is accepted and reassigned from its valid face neighbor. | +| `SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - unavailable phase (-5555)` | new-for-V&V | Asserts exact unavailable-phase error. | +| `SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - all features rejected (-55569)` | new-for-V&V | Asserts exact all-rejected error. | +| `SimplnxCore::RequireMinNumNeighborsFilter: SIMPL Backwards Compatibility` | kept | SIMPL json backwards compatibility check. | + +## Exemplar archive + +No new exemplar archive was created for this V&V cycle: the Class 1 oracle is encoded entirely as inline expected values in the test source. + +## Deviations from DREAM3D 6.5.171 + +No deviations observed. The all-phase analytical fixture compared FeatureIds, copied and ignored cell arrays, NumNeighbors, and Phases; DREAM3D 6.5.171 and NX matched all five arrays. diff --git a/src/Plugins/SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md b/src/Plugins/SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md new file mode 100644 index 0000000000..7be058b84f --- /dev/null +++ b/src/Plugins/SimplnxCore/vv/deviations/RequireMinNumNeighborsFilter.md @@ -0,0 +1,3 @@ +# Deviations from DREAM3D 6.5.171: RequireMinNumNeighborsFilter + +No behavioral deviation was observed in the all-phase analytical comparison. DREAM3D 6.5.171 `MinNeighbors` and NX `RequireMinNumNeighborsFilter` matched FeatureIds, copied and ignored cell arrays, NumNeighbors, and Phases.