From 740c6e9b5cf98a6b3d7bc85d52425314056e02ba Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 6 Oct 2026 22:57:50 -0400 Subject: [PATCH 01/23] BUG: Fix ComputeVertexToTriangleDistances normals component shape * CS-1: the Triangle Normals parameter declared no component shape, but the algorithm reads 3 component(s) per tuple, so a DataArray with another component shape was read with the wrong stride (wrong output or a read past the end of the array). The parameter now declares a component shape of 3, and parameter validation rejects other shapes with -208 * Document the requirement and add a preflight test Co-Authored-By: Claude Opus 5.5 --- .../ComputeVertexToTriangleDistancesFilter.md | 2 + ...ComputeVertexToTriangleDistancesFilter.cpp | 4 +- .../ComputeVertexToTriangleDistancesTest.cpp | 45 +++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/src/Plugins/SimplnxCore/docs/ComputeVertexToTriangleDistancesFilter.md b/src/Plugins/SimplnxCore/docs/ComputeVertexToTriangleDistancesFilter.md index 57788f5734..7d950f31d4 100644 --- a/src/Plugins/SimplnxCore/docs/ComputeVertexToTriangleDistancesFilter.md +++ b/src/Plugins/SimplnxCore/docs/ComputeVertexToTriangleDistancesFilter.md @@ -20,6 +20,8 @@ The sign of each distance depends on the **Triangle Normals**. For the sign to b % Auto generated parameter table will be inserted here +**Triangle Normals** must have 3 components. + ## License & Copyright Please see the description file distributed with this plugin. diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeVertexToTriangleDistancesFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeVertexToTriangleDistancesFilter.cpp index 27d2a7b9c4..d9e697caf5 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeVertexToTriangleDistancesFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeVertexToTriangleDistancesFilter.cpp @@ -64,8 +64,8 @@ Parameters ComputeVertexToTriangleDistancesFilter::parameters() const GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Vertex})); params.insert(std::make_unique(k_SelectedTriangleGeometryPath_Key, "Target Triangle Geometry", "The triangle geometry to compare against", DataPath{}, GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Triangle})); - params.insert( - std::make_unique(k_TriangleNormalsArrayPath_Key, "Triangle Normals", "The triangle geometry's normals array", DataPath{}, std::set{DataType::float64})); + params.insert(std::make_unique(k_TriangleNormalsArrayPath_Key, "Triangle Normals", "The triangle geometry's normals array", DataPath{}, + std::set{DataType::float64}, ArraySelectionParameter::AllowedComponentShapes{{3}})); params.insertSeparator(Parameters::Separator{"Output Arrays"}); params.insert(std::make_unique(k_DistancesArrayName_Key, "Distances Array", "The array to store distance between vertex and triangle", "")); diff --git a/src/Plugins/SimplnxCore/test/ComputeVertexToTriangleDistancesTest.cpp b/src/Plugins/SimplnxCore/test/ComputeVertexToTriangleDistancesTest.cpp index 8981c0b064..35608405e3 100644 --- a/src/Plugins/SimplnxCore/test/ComputeVertexToTriangleDistancesTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeVertexToTriangleDistancesTest.cpp @@ -1,5 +1,6 @@ #include "SimplnxCore/Filters/ComputeVertexToTriangleDistancesFilter.hpp" #include "SimplnxCore/SimplnxCore_test_dirs.hpp" +#include "simplnx/DataStructure/Geometry/VertexGeom.hpp" #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" @@ -8,6 +9,7 @@ #include "simplnx/Pipeline/PipelineFilter.hpp" #include "simplnx/UnitTest/UnitTestCommon.hpp" +#include #include namespace fs = std::filesystem; @@ -116,3 +118,46 @@ TEST_CASE("SimplnxCore::ComputeVertexToTriangleDistancesFilter: SIMPL Backwards } } } + +TEST_CASE("SimplnxCore::ComputeVertexToTriangleDistancesFilter: Triangle Normals requires 3 components", "[SimplnxCore][ComputeVertexToTriangleDistancesFilter][ComponentShape]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure; + auto* triangleGeom = TriangleGeom::Create(dataStructure, "Triangles"); + REQUIRE(triangleGeom != nullptr); + auto* vertices = UnitTest::CreateTestDataArray(dataStructure, "Vertices", {3}, {3}, triangleGeom->getId()); + triangleGeom->setVertices(*vertices); + triangleGeom->setVertexCoordinate(0, {0.0F, 0.0F, 0.0F}); + triangleGeom->setVertexCoordinate(1, {1.0F, 0.0F, 0.0F}); + triangleGeom->setVertexCoordinate(2, {0.0F, 1.0F, 0.0F}); + auto* faces = UnitTest::CreateTestDataArray(dataStructure, "Faces", {1}, {3}, triangleGeom->getId()); + (*faces)[0] = 0; + (*faces)[1] = 1; + (*faces)[2] = 2; + triangleGeom->setFaceList(*faces); + auto* faceData = AttributeMatrix::Create(dataStructure, "Face Data", {1}, triangleGeom->getId()); + REQUIRE(faceData != nullptr); + triangleGeom->setFaceAttributeMatrix(*faceData); + UnitTest::CreateTestDataArray(dataStructure, "Normals", {1}, {1}, faceData->getId()); + auto* vertexGeom = VertexGeom::Create(dataStructure, "Query Points"); + REQUIRE(vertexGeom != nullptr); + auto* queryVertices = UnitTest::CreateTestDataArray(dataStructure, "Vertices", {1}, {3}, vertexGeom->getId()); + vertexGeom->setVertices(*queryVertices); + vertexGeom->setVertexCoordinate(0, {0.25F, 0.25F, 1.0F}); + auto* vertexData = AttributeMatrix::Create(dataStructure, "Vertex Data", {1}, vertexGeom->getId()); + REQUIRE(vertexData != nullptr); + vertexGeom->setVertexAttributeMatrix(*vertexData); + + ComputeVertexToTriangleDistancesFilter filter; + auto args = filter.getDefaultArguments(); + args.insertOrAssign(ComputeVertexToTriangleDistancesFilter::k_SelectedVertexGeometryPath_Key, std::make_any(DataPath({"Query Points"}))); + args.insertOrAssign(ComputeVertexToTriangleDistancesFilter::k_SelectedTriangleGeometryPath_Key, std::make_any(DataPath({"Triangles"}))); + args.insertOrAssign(ComputeVertexToTriangleDistancesFilter::k_TriangleNormalsArrayPath_Key, std::make_any(DataPath({"Triangles", "Face Data", "Normals"}))); + args.insertOrAssign(ComputeVertexToTriangleDistancesFilter::k_DistancesArrayName_Key, std::make_any("Distances")); + args.insertOrAssign(ComputeVertexToTriangleDistancesFilter::k_ClosestTriangleIdArrayName_Key, std::make_any("Closest Triangle Ids")); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(std::any_of(preflightResult.outputActions.errors().begin(), preflightResult.outputActions.errors().end(), [](const Error& error) { return error.code == -208; })); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} From f7036b405c90a7fa0aa2d5c2909a803613abf4da Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 6 Oct 2026 22:57:50 -0400 Subject: [PATCH 02/23] BUG: Fix ComputeShapesTriangleGeom centroids component shape * CS-2: the Face Feature Centroids parameter declared no component shape, but the algorithm reads 3 component(s) per tuple, so a DataArray with another component shape was read with the wrong stride (wrong output or a read past the end of the array). The parameter now declares a component shape of 3, and parameter validation rejects other shapes with -208 * Document the requirement and add a preflight test Co-Authored-By: Claude Opus 5.5 --- .../docs/ComputeShapesTriangleGeomFilter.md | 2 + .../ComputeShapesTriangleGeomFilter.cpp | 3 +- .../test/ComputeShapesTriangleGeomTest.cpp | 41 +++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/Plugins/OrientationAnalysis/docs/ComputeShapesTriangleGeomFilter.md b/src/Plugins/OrientationAnalysis/docs/ComputeShapesTriangleGeomFilter.md index 35344f98e7..9c37e94c91 100644 --- a/src/Plugins/OrientationAnalysis/docs/ComputeShapesTriangleGeomFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/ComputeShapesTriangleGeomFilter.md @@ -47,6 +47,8 @@ but any shape with clear sharp corners may be affected. % Auto generated parameter table will be inserted here +**Face Feature Centroids** must have 3 components. + ## References [1] Representation and Reconstruction of Three-dimensional Microstructures in Ni-based Superalloys, AFOSR diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeShapesTriangleGeomFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeShapesTriangleGeomFilter.cpp index 93244e81d5..58d125c978 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeShapesTriangleGeomFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeShapesTriangleGeomFilter.cpp @@ -60,7 +60,8 @@ Parameters ComputeShapesTriangleGeomFilter::parameters() const "The DataPath to the AttributeMatrix that holds feature data for the faces", DataPath({"TriangleDataContainer", "Face Feature Data"}))); params.insert(std::make_unique(k_CentroidsArrayPath_Key, "Face Feature Centroids", "Input DataPath to the **Feature Centroids** for the face data", - DataPath({"Face Feature Data", "Centroids"}), ArraySelectionParameter::AllowedTypes{DataType::float32})); + DataPath({"Face Feature Data", "Centroids"}), ArraySelectionParameter::AllowedTypes{DataType::float32}, + ArraySelectionParameter::AllowedComponentShapes{{3}})); params.insertSeparator(Parameters::Separator{"Output Face Feature Data"}); params.insert(std::make_unique(k_Omega3sArrayName_Key, "Omega3s", "The name of the DataArray that holds the calculated Omega3 values", "Omega3s")); diff --git a/src/Plugins/OrientationAnalysis/test/ComputeShapesTriangleGeomTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeShapesTriangleGeomTest.cpp index 7e57f402f5..d755cdfeba 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeShapesTriangleGeomTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeShapesTriangleGeomTest.cpp @@ -1,11 +1,13 @@ #include "OrientationAnalysis/Filters/ComputeShapesTriangleGeomFilter.hpp" #include "OrientationAnalysis/OrientationAnalysis_test_dirs.hpp" +#include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" #include "simplnx/Parameters/ArrayCreationParameter.hpp" #include "simplnx/Parameters/DataObjectNameParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/UnitTest/UnitTestCommon.hpp" +#include #include #include @@ -94,3 +96,42 @@ TEST_CASE("OrientationAnalysis::ComputeShapesTriangleGeom", "[OrientationAnalysi UnitTest::CheckArraysInheritTupleDims(exemplarDataStructure); } + +TEST_CASE("OrientationAnalysis::ComputeShapesTriangleGeom: Face Feature Centroids requires 3 components", "[OrientationAnalysis][ComputeShapesTriangleGeom][ComponentShape]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure; + auto* triangleGeom = TriangleGeom::Create(dataStructure, "Triangles"); + REQUIRE(triangleGeom != nullptr); + auto* vertices = UnitTest::CreateTestDataArray(dataStructure, "Vertices", {3}, {3}, triangleGeom->getId()); + triangleGeom->setVertices(*vertices); + triangleGeom->setVertexCoordinate(0, {0.0F, 0.0F, 0.0F}); + triangleGeom->setVertexCoordinate(1, {1.0F, 0.0F, 0.0F}); + triangleGeom->setVertexCoordinate(2, {0.0F, 1.0F, 0.0F}); + auto* faces = UnitTest::CreateTestDataArray(dataStructure, "Faces", {1}, {3}, triangleGeom->getId()); + (*faces)[0] = 0; + (*faces)[1] = 1; + (*faces)[2] = 2; + triangleGeom->setFaceList(*faces); + auto* faceData = AttributeMatrix::Create(dataStructure, "Face Data", {1}, triangleGeom->getId()); + REQUIRE(faceData != nullptr); + triangleGeom->setFaceAttributeMatrix(*faceData); + auto* labels = UnitTest::CreateTestDataArray(dataStructure, "Face Labels", {1}, {2}, faceData->getId()); + (*labels)[0] = 1; + (*labels)[1] = 0; + auto* featureData = AttributeMatrix::Create(dataStructure, "Feature Data", {2}, triangleGeom->getId()); + REQUIRE(featureData != nullptr); + UnitTest::CreateTestDataArray(dataStructure, "Centroids", {2}, {1}, featureData->getId()); + + ComputeShapesTriangleGeomFilter filter; + auto args = filter.getDefaultArguments(); + args.insertOrAssign(ComputeShapesTriangleGeomFilter::k_TriGeometryDataPath_Key, std::make_any(DataPath({"Triangles"}))); + args.insertOrAssign(ComputeShapesTriangleGeomFilter::k_FaceLabelsArrayPath_Key, std::make_any(DataPath({"Triangles", "Face Data", "Face Labels"}))); + args.insertOrAssign(ComputeShapesTriangleGeomFilter::k_FeatureAttributeMatrixPath_Key, std::make_any(DataPath({"Triangles", "Feature Data"}))); + args.insertOrAssign(ComputeShapesTriangleGeomFilter::k_CentroidsArrayPath_Key, std::make_any(DataPath({"Triangles", "Feature Data", "Centroids"}))); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(std::any_of(preflightResult.outputActions.errors().begin(), preflightResult.outputActions.errors().end(), [](const Error& error) { return error.code == -208; })); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} From e477b3b497bb319bb82cbcd30c1cfe9141964ea3 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 6 Oct 2026 22:57:51 -0400 Subject: [PATCH 03/23] BUG: Fix ComputeShapes Cell Feature Ids component shape * CS-4: the Cell Feature Ids parameter declared no component shape, but the algorithm reads 1 component(s) per tuple, so a DataArray with another component shape was read with the wrong stride (wrong output or a read past the end of the array). The parameter now declares a component shape of 1, and parameter validation rejects other shapes with -208 * Document the requirement and add a preflight test Co-Authored-By: Claude Opus 5.5 --- .../docs/ComputeShapesFilter.md | 2 + .../Filters/ComputeShapesFilter.cpp | 2 +- .../test/ComputeShapesFilterTest.cpp | 41 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/Plugins/OrientationAnalysis/docs/ComputeShapesFilter.md b/src/Plugins/OrientationAnalysis/docs/ComputeShapesFilter.md index 3e5baebf86..fa3f844266 100644 --- a/src/Plugins/OrientationAnalysis/docs/ComputeShapesFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/ComputeShapesFilter.md @@ -36,6 +36,8 @@ For shape analysis on triangle geometry meshes rather than voxelized image geome % Auto generated parameter table will be inserted here +**Cell Feature Ids** must have 1 component. + ## References [1] Representation and Reconstruction of Three-dimensional Microstructures in Ni-based Superalloys, AFOSR FA9550-07-1-0179 Final Report, 20 Dec 2010. diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeShapesFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeShapesFilter.cpp index f18a77a642..c52bbc5fd4 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeShapesFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeShapesFilter.cpp @@ -57,7 +57,7 @@ Parameters ComputeShapesFilter::parameters() const GeometrySelectionParameter::AllowedTypes{IGeometry::Type::Image})); params.insertSeparator(Parameters::Separator{"Input Cell Data"}); params.insert(std::make_unique(k_CellFeatureIdsArrayPath_Key, "Cell Feature Ids", "Specifies to which feature each cell belongs.", DataPath({"Cell Data", "FeatureIds"}), - ArraySelectionParameter::AllowedTypes{DataType::int32})); + ArraySelectionParameter::AllowedTypes{DataType::int32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); params.insertSeparator(Parameters::Separator{"Input Feature Data"}); params.insert(std::make_unique(k_CentroidsArrayPath_Key, "Feature Centroids", "X, Y, Z coordinates of Feature center of mass", DataPath({"Centroids"}), diff --git a/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp index a53979d1a2..cad567b0b0 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp @@ -1,5 +1,6 @@ #include "OrientationAnalysis/Filters/ComputeShapesFilter.hpp" #include "OrientationAnalysis/OrientationAnalysis_test_dirs.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/AttributeMatrix.hpp" @@ -11,6 +12,7 @@ #include "simplnx/Pipeline/PipelineFilter.hpp" #include "simplnx/UnitTest/UnitTestCommon.hpp" +#include #include #include @@ -186,3 +188,42 @@ TEST_CASE("OrientationAnalysis::ComputeShapesFilter: Geometry cell tuple count", REQUIRE(errorRef.message.find("'Grid' has 12 cells") != std::string::npos); } } + +TEST_CASE("OrientationAnalysis::ComputeShapesFilter: Cell Feature Ids requires 1 component", "[OrientationAnalysis][ComputeShapesFilter][ComponentShape]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure; + auto* imageGeom = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeom != nullptr); + imageGeom->setDimensions({2, 1, 1}); + imageGeom->setSpacing({1.0F, 1.0F, 1.0F}); + imageGeom->setOrigin({0.0F, 0.0F, 0.0F}); + auto* cellData = AttributeMatrix::Create(dataStructure, "Cell Data", {1, 1, 2}, imageGeom->getId()); + REQUIRE(cellData != nullptr); + imageGeom->setCellData(*cellData); + auto* featureIds = UnitTest::CreateTestDataArray(dataStructure, "Feature Ids", {1, 1, 2}, {2}, cellData->getId()); + (*featureIds)[0] = 1; + (*featureIds)[1] = 0; + (*featureIds)[2] = 2; + (*featureIds)[3] = 0; + auto* featureData = AttributeMatrix::Create(dataStructure, "Feature Data", {3}, imageGeom->getId()); + REQUIRE(featureData != nullptr); + auto* centroids = UnitTest::CreateTestDataArray(dataStructure, "Centroids", {3}, {3}, featureData->getId()); + (*centroids)[3] = 0.5F; + (*centroids)[4] = 0.5F; + (*centroids)[5] = 0.5F; + (*centroids)[6] = 1.5F; + (*centroids)[7] = 0.5F; + (*centroids)[8] = 0.5F; + + ComputeShapesFilter filter; + auto args = filter.getDefaultArguments(); + args.insertOrAssign(ComputeShapesFilter::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(ComputeShapesFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(DataPath({"Image", "Cell Data", "Feature Ids"}))); + args.insertOrAssign(ComputeShapesFilter::k_CentroidsArrayPath_Key, std::make_any(DataPath({"Image", "Feature Data", "Centroids"}))); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(std::any_of(preflightResult.outputActions.errors().begin(), preflightResult.outputActions.errors().end(), [](const Error& error) { return error.code == -208; })); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} From 98066c4f3db2a2f3e127bdd061b6b4f5b6c63323 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 6 Oct 2026 22:57:51 -0400 Subject: [PATCH 04/23] BUG: Fix ComputeFeaturePhasesBinary Cell Feature Ids component shape * CS-6: the Cell Feature Ids parameter declared no component shape, but the algorithm reads 1 component(s) per tuple, so a DataArray with another component shape was read with the wrong stride (wrong output or a read past the end of the array). The parameter now declares a component shape of 1, and parameter validation rejects other shapes with -208 * Document the requirement and add a preflight test Co-Authored-By: Claude Opus 5.5 --- .../docs/ComputeFeaturePhasesBinaryFilter.md | 2 ++ .../ComputeFeaturePhasesBinaryFilter.cpp | 2 +- .../test/ComputeFeaturePhasesBinaryTest.cpp | 28 +++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/Plugins/SimplnxCore/docs/ComputeFeaturePhasesBinaryFilter.md b/src/Plugins/SimplnxCore/docs/ComputeFeaturePhasesBinaryFilter.md index b47c738721..e675a2b789 100644 --- a/src/Plugins/SimplnxCore/docs/ComputeFeaturePhasesBinaryFilter.md +++ b/src/Plugins/SimplnxCore/docs/ComputeFeaturePhasesBinaryFilter.md @@ -16,6 +16,8 @@ This **Filter** is generally useful any time data has already been reduced to a % Auto generated parameter table will be inserted here +**Cell Feature Ids** must have 1 component. + ## License & Copyright Please see the description file distributed with this **Plugin** diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeaturePhasesBinaryFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeaturePhasesBinaryFilter.cpp index 78729dafbb..91a3d089d7 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeaturePhasesBinaryFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeaturePhasesBinaryFilter.cpp @@ -55,7 +55,7 @@ Parameters ComputeFeaturePhasesBinaryFilter::parameters() const // Create the parameter descriptors that are needed for this filter params.insertSeparator(Parameters::Separator{"Input Cell Data"}); params.insert(std::make_unique(k_FeatureIdsArrayPath_Key, "Cell Feature Ids", "Data Array that specifies to which Feature each Element belongs", DataPath{}, - ArraySelectionParameter::AllowedTypes{DataType::int32})); + ArraySelectionParameter::AllowedTypes{DataType::int32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); params.insertSeparator(Parameters::Separator{"Optional Data Mask"}); params.insert(std::make_unique(k_MaskArrayPath_Key, "Mask Array", "Data Array that specifies if the Cell is to be counted in the algorithm", DataPath{}, diff --git a/src/Plugins/SimplnxCore/test/ComputeFeaturePhasesBinaryTest.cpp b/src/Plugins/SimplnxCore/test/ComputeFeaturePhasesBinaryTest.cpp index 1d781e57c2..22a2124ade 100644 --- a/src/Plugins/SimplnxCore/test/ComputeFeaturePhasesBinaryTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeFeaturePhasesBinaryTest.cpp @@ -1,4 +1,5 @@ #include "SimplnxCore/SimplnxCore_test_dirs.hpp" +#include #include #include #include @@ -100,3 +101,30 @@ TEST_CASE("SimplnxCore::ComputeFeaturePhasesBinaryFilter: SIMPL Backwards Compat } } } + +TEST_CASE("SimplnxCore::ComputeFeaturePhasesBinaryFilter: Cell Feature Ids requires 1 component", "[SimplnxCore][ComputeFeaturePhasesBinaryFilter][ComponentShape]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure; + auto* cellData = AttributeMatrix::Create(dataStructure, "Cell Data", {2}); + REQUIRE(cellData != nullptr); + auto* featureIds = UnitTest::CreateTestDataArray(dataStructure, "Feature Ids", {2}, {2}, cellData->getId()); + (*featureIds)[0] = 1; + (*featureIds)[1] = 0; + (*featureIds)[2] = 2; + (*featureIds)[3] = 0; + auto* mask = UnitTest::CreateTestDataArray(dataStructure, "Mask", {2}, {1}, cellData->getId()); + mask->fill(true); + REQUIRE(AttributeMatrix::Create(dataStructure, "Feature Data", {3}) != nullptr); + + ComputeFeaturePhasesBinaryFilter filter; + auto args = filter.getDefaultArguments(); + args.insertOrAssign(ComputeFeaturePhasesBinaryFilter::k_FeatureIdsArrayPath_Key, std::make_any(DataPath({"Cell Data", "Feature Ids"}))); + args.insertOrAssign(ComputeFeaturePhasesBinaryFilter::k_MaskArrayPath_Key, std::make_any(DataPath({"Cell Data", "Mask"}))); + args.insertOrAssign(ComputeFeaturePhasesBinaryFilter::k_CellDataAMPath_Key, std::make_any(DataPath({"Feature Data"}))); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(std::any_of(preflightResult.outputActions.errors().begin(), preflightResult.outputActions.errors().end(), [](const Error& error) { return error.code == -208; })); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} From 6869ef40278fb9331b2aedff8e8a21f6fbab7a31 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 6 Oct 2026 22:57:51 -0400 Subject: [PATCH 05/23] BUG: Fix ComputeKMeans Cell Mask Array component shape * CS-7: the Cell Mask Array parameter declared no component shape, but the algorithm reads 1 component(s) per tuple, so a DataArray with another component shape was read with the wrong stride (wrong output or a read past the end of the array). The parameter now declares a component shape of 1, and parameter validation rejects other shapes with -208 * Document the requirement and add a preflight test Co-Authored-By: Claude Opus 5.5 --- .../SimplnxCore/docs/ComputeKMeansFilter.md | 2 ++ .../Filters/ComputeKMeansFilter.cpp | 2 +- .../SimplnxCore/test/ComputeKMeansTest.cpp | 30 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Plugins/SimplnxCore/docs/ComputeKMeansFilter.md b/src/Plugins/SimplnxCore/docs/ComputeKMeansFilter.md index 912c5f4411..c42fcb7f05 100644 --- a/src/Plugins/SimplnxCore/docs/ComputeKMeansFilter.md +++ b/src/Plugins/SimplnxCore/docs/ComputeKMeansFilter.md @@ -12,6 +12,8 @@ This **Filter** applies the k means algorithm to an **Attribute Array**. K mean Each cluster is summarized by its **centroid**, which is the arithmetic mean (the average position) of all the points assigned to that cluster. A k means partitioning is a *Voronoi tessellation*: every point is assigned to the cluster whose centroid is closest to it, so the data space is carved into regions where each region contains all the points nearer to one centroid than to any other. An optimal solution to the k means problem is one in which each point is associated with the cluster that has the closest mean. This partitioning minimizes the **within-cluster variance**, that is, the sum over all clusters of the squared distances from each point to its cluster centroid; minimizing it makes the points within each cluster as tightly grouped as possible. The user may select from several distance metrics: *Euclidean*, *Squared Euclidean*, *Manhattan*, *Cosine*, *Pearson*, and *Squared Pearson*. +**Cell Mask Array** must have 1 component. + ## Algorithm Initial centroids are drawn uniformly, with replacement, from all selected tuples. A seed gives different initial centroids than in earlier DREAM3D-NX versions. diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMeansFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMeansFilter.cpp index b120507443..d01aac2a6a 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMeansFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMeansFilter.cpp @@ -76,7 +76,7 @@ Parameters ComputeKMeansFilter::parameters() const params.insertLinkableParameter(std::make_unique(k_UseMask_Key, "Use Mask Array", "Specifies whether or not to use a mask array", false)); params.insert(std::make_unique(k_MaskArrayPath_Key, "Cell Mask Array", "DataPath to the boolean or uint8 mask array. Values that are true will mark that cell/point as usable.", DataPath{}, - ArraySelectionParameter::AllowedTypes{DataType::boolean, DataType::uint8})); + ArraySelectionParameter::AllowedTypes{DataType::boolean, DataType::uint8}, ArraySelectionParameter::AllowedComponentShapes{{1}})); params.insertSeparator(Parameters::Separator{"Input Data Objects"}); params.insert(std::make_unique(k_SelectedArrayPath_Key, "Attribute Array to Cluster", "The array to cluster from", DataPath{}, nx::core::GetAllNumericTypes())); diff --git a/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp b/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp index 24943f78aa..13e095ee38 100644 --- a/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp @@ -1,4 +1,5 @@ #include "SimplnxCore/SimplnxCore_test_dirs.hpp" +#include #include #include "simplnx/Core/Application.hpp" @@ -605,3 +606,32 @@ TEST_CASE("SimplnxCore::ComputeKMeansFilter: SIMPL Backwards Compatibility", "[S } } } + +TEST_CASE("SimplnxCore::ComputeKMeans: Cell Mask Array requires 1 component", "[SimplnxCore][ComputeKMeans][ComponentShape]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure; + auto* cellData = AttributeMatrix::Create(dataStructure, "Cell Data", {2}); + REQUIRE(cellData != nullptr); + auto* input = UnitTest::CreateTestDataArray(dataStructure, "Input", {2}, {1}, cellData->getId()); + (*input)[0] = 0.0F; + (*input)[1] = 1.0F; + auto* mask = UnitTest::CreateTestDataArray(dataStructure, "Mask", {2}, {2}, cellData->getId()); + (*mask)[0] = 1; + (*mask)[1] = 0; + (*mask)[2] = 1; + (*mask)[3] = 1; + + ComputeKMeansFilter filter; + auto args = filter.getDefaultArguments(); + args.insertOrAssign(ComputeKMeansFilter::k_InitClusters_Key, std::make_any(1)); + args.insertOrAssign(ComputeKMeansFilter::k_UseMask_Key, std::make_any(true)); + args.insertOrAssign(ComputeKMeansFilter::k_MaskArrayPath_Key, std::make_any(DataPath({"Cell Data", "Mask"}))); + args.insertOrAssign(ComputeKMeansFilter::k_SelectedArrayPath_Key, std::make_any(DataPath({"Cell Data", "Input"}))); + args.insertOrAssign(ComputeKMeansFilter::k_FeatureAMPath_Key, std::make_any(DataPath({"Cluster Data"}))); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(std::any_of(preflightResult.outputActions.errors().begin(), preflightResult.outputActions.errors().end(), [](const Error& error) { return error.code == -208; })); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} From b1d8a7604d88bc4c3a9658fa29baa91a4324f3ca Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 6 Oct 2026 22:57:51 -0400 Subject: [PATCH 06/23] BUG: Fix ComputeKMedoids Mask Array component shape * CS-8: the Mask Array parameter declared no component shape, but the algorithm reads 1 component(s) per tuple, so a DataArray with another component shape was read with the wrong stride (wrong output or a read past the end of the array). The parameter now declares a component shape of 1, and parameter validation rejects other shapes with -208 * Document the requirement and add a preflight test Co-Authored-By: Claude Opus 5.5 --- .../SimplnxCore/docs/ComputeKMedoidsFilter.md | 2 ++ .../Filters/ComputeKMedoidsFilter.cpp | 2 +- .../SimplnxCore/test/ComputeKMedoidsTest.cpp | 30 +++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/src/Plugins/SimplnxCore/docs/ComputeKMedoidsFilter.md b/src/Plugins/SimplnxCore/docs/ComputeKMedoidsFilter.md index 3b19e2f7bd..8ad32d93b4 100644 --- a/src/Plugins/SimplnxCore/docs/ComputeKMedoidsFilter.md +++ b/src/Plugins/SimplnxCore/docs/ComputeKMedoidsFilter.md @@ -64,6 +64,8 @@ The filter creates a *cluster Ids* array (one dimensionless integer cluster labe - **Clustered Attribute Array** -- any **Attribute Array** (cell-level, feature-level, or generic) whose tuples are to be partitioned into clusters. This is typically an output of an earlier computation or import step. +**Mask Array** must have 1 component. + ## Algorithm This filter has two algorithm implementations that are automatically selected at runtime based on how the input data is stored. The user does not need to choose between them. diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMedoidsFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMedoidsFilter.cpp index 73cccb2d74..aa543dd0b2 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMedoidsFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMedoidsFilter.cpp @@ -69,7 +69,7 @@ Parameters ComputeKMedoidsFilter::parameters() const params.insertSeparator(Parameters::Separator{"Input Parameter(s)"}); params.insertLinkableParameter(std::make_unique(k_UseMask_Key, "Use Mask Array", "Specifies whether or not to use a mask array", false)); params.insert(std::make_unique(k_MaskArrayPath_Key, "Mask Array", "DataPath to the boolean or uint8 mask array. Values that are true will mark that cell/point as usable.", - DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::boolean, DataType::uint8})); + DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::boolean, DataType::uint8}, ArraySelectionParameter::AllowedComponentShapes{{1}})); params.insert(std::make_unique(k_InitClusters_Key, "Number of Clusters", "This will be the tuple size for Cluster Attribute Matrix and the values within", 0)); params.insert(std::make_unique( k_DistanceMetric_Key, "Distance Metric", "Distance Metric type to be used for calculations", to_underlying(ClusterUtilities::DistanceMetric::Euclidean), diff --git a/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp b/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp index d253093120..ac3c0e5be6 100644 --- a/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp @@ -1,4 +1,5 @@ #include "SimplnxCore/SimplnxCore_test_dirs.hpp" +#include #include #include #include @@ -474,3 +475,32 @@ TEST_CASE("SimplnxCore::ComputeKMedoidsFilter: SIMPL Backwards Compatibility", " } } } + +TEST_CASE("SimplnxCore::ComputeKMedoidsFilter: Mask Array requires 1 component", "[SimplnxCore][ComputeKMedoidsFilter][ComponentShape]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure; + auto* cellData = AttributeMatrix::Create(dataStructure, "Cell Data", {2}); + REQUIRE(cellData != nullptr); + auto* input = UnitTest::CreateTestDataArray(dataStructure, "Input", {2}, {1}, cellData->getId()); + (*input)[0] = 0.0F; + (*input)[1] = 1.0F; + auto* mask = UnitTest::CreateTestDataArray(dataStructure, "Mask", {2}, {2}, cellData->getId()); + (*mask)[0] = 1; + (*mask)[1] = 0; + (*mask)[2] = 1; + (*mask)[3] = 1; + + ComputeKMedoidsFilter filter; + auto args = filter.getDefaultArguments(); + args.insertOrAssign(ComputeKMedoidsFilter::k_InitClusters_Key, std::make_any(1)); + args.insertOrAssign(ComputeKMedoidsFilter::k_UseMask_Key, std::make_any(true)); + args.insertOrAssign(ComputeKMedoidsFilter::k_MaskArrayPath_Key, std::make_any(DataPath({"Cell Data", "Mask"}))); + args.insertOrAssign(ComputeKMedoidsFilter::k_SelectedArrayPath_Key, std::make_any(DataPath({"Cell Data", "Input"}))); + args.insertOrAssign(ComputeKMedoidsFilter::k_FeatureAMPath_Key, std::make_any(DataPath({"Cluster Data"}))); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(std::any_of(preflightResult.outputActions.errors().begin(), preflightResult.outputActions.errors().end(), [](const Error& error) { return error.code == -208; })); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} From c77e59617238be6dd07226a5882765f6611cf1eb Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 6 Oct 2026 23:02:07 -0400 Subject: [PATCH 07/23] BUG: Fix ComputeNumFeatures Feature Phases type and range checks * CS-3: the Feature Phases parameter declared no component shape, but execute reads one value per feature. It now declares a component shape of 1 (parameter validation rejects others with -208) * CS-14: the parameter accepted every DataType, but execute reads it as an Int32Array, so any other DataType was dereferenced as null. The parameter now accepts only int32 (-207 otherwise) * CS-15: a Feature Phases value below 0 or not less than the Ensemble Attribute Matrix tuple count was used directly as an index for a write. Execute now checks every value first and returns -9740 * Document the requirements and add tests Co-Authored-By: Claude Opus 5.5 --- .../docs/ComputeNumFeaturesFilter.md | 2 + .../Filters/ComputeNumFeaturesFilter.cpp | 16 ++- .../test/ComputeNumFeaturesTest.cpp | 104 ++++++++++++++++++ 3 files changed, 121 insertions(+), 1 deletion(-) diff --git a/src/Plugins/SimplnxCore/docs/ComputeNumFeaturesFilter.md b/src/Plugins/SimplnxCore/docs/ComputeNumFeaturesFilter.md index 6cf8a38bfa..34f7966ba4 100644 --- a/src/Plugins/SimplnxCore/docs/ComputeNumFeaturesFilter.md +++ b/src/Plugins/SimplnxCore/docs/ComputeNumFeaturesFilter.md @@ -16,6 +16,8 @@ The output is a single-component count array indexed by **Ensemble** (phase) Id: % Auto generated parameter table will be inserted here +**Feature Phases** must be a 1-component int32 DataArray; each value from feature index 1 onward must be a valid index into the **Ensemble Attribute Matrix**. + ## Example Pipelines + INL Export diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeNumFeaturesFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeNumFeaturesFilter.cpp index d571c8eb5c..0b143121fa 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeNumFeaturesFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeNumFeaturesFilter.cpp @@ -52,7 +52,8 @@ Parameters ComputeNumFeaturesFilter::parameters() const params.insertSeparator(Parameters::Separator{"Input Feature Data"}); params.insert(std::make_unique(k_FeaturePhasesArrayPath_Key, "Feature Phases", "Array specifying which Ensemble each Feature belongs", - DataPath({"DataContainer", "Feature Data", "Phases"}), nx::core::GetAllDataTypes())); + DataPath({"DataContainer", "Feature Data", "Phases"}), ArraySelectionParameter::AllowedTypes{DataType::int32}, + ArraySelectionParameter::AllowedComponentShapes{{1}})); params.insertSeparator(Parameters::Separator{"Input Ensemble Data"}); params.insert(std::make_unique(k_EnsembleAttributeMatrixPath_Key, "Ensemble Attribute Matrix", @@ -108,6 +109,19 @@ Result<> ComputeNumFeaturesFilter::executeImpl(DataStructure& dataStructure, con const auto& featurePhasesStore = dataStructure.getDataAs(pFeaturePhasesArrayPathValue)->getDataStoreRef(); auto& numFeaturesStore = dataStructure.getDataAs(pNumFeaturesArrayPathValue)->getDataStoreRef(); + const usize ensembleTupleCount = numFeaturesStore.getNumberOfTuples(); + for(usize index = 1; index < featurePhasesStore.getNumberOfTuples(); index++) + { + const int32 phase = featurePhasesStore[index]; + if(phase < 0 || static_cast(phase) >= ensembleTupleCount) + { + return MakeErrorResult(-9740, + fmt::format("**Feature Phases** DataArray '{}' has value {} at tuple index {}, which is not a valid Ensemble index. The **Ensemble Attribute Matrix** has Ensemble tuple " + "count {}. Each value must be a valid Ensemble index.", + pFeaturePhasesArrayPathValue.toString(), phase, index, ensembleTupleCount)); + } + } + for(usize index = 1; index < featurePhasesStore.getNumberOfTuples(); index++) { numFeaturesStore[featurePhasesStore[index]].inc(); diff --git a/src/Plugins/SimplnxCore/test/ComputeNumFeaturesTest.cpp b/src/Plugins/SimplnxCore/test/ComputeNumFeaturesTest.cpp index a17279893a..8e40611e41 100644 --- a/src/Plugins/SimplnxCore/test/ComputeNumFeaturesTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeNumFeaturesTest.cpp @@ -26,8 +26,112 @@ const DataPath k_FeatureCountsPath({Constants::k_DataContainer, Constants::k_Cel const DataPath k_FeatureCountsPathNX({Constants::k_DataContainer, Constants::k_CellEnsembleData, k_FeatureCountsNX}); const fs::path k_BaseDataFilePath = fs::path(fmt::format("{}/6_6_volume_fraction_feature_count.dream3d", unit_test::k_TestFilesDir)); + +template +Arguments CreatePhaseFixture(DataStructure& dataStructure, const std::vector& values, const ShapeType& componentShape = {1}) +{ + const usize numComponents = componentShape.front(); + auto* featureData = AttributeMatrix::Create(dataStructure, "Feature Data", {values.size() / numComponents}); + REQUIRE(featureData != nullptr); + auto* phases = UnitTest::CreateTestDataArray(dataStructure, "Phases", {values.size() / numComponents}, componentShape, featureData->getId()); + REQUIRE(phases != nullptr); + for(usize index = 0; index < values.size(); index++) + { + (*phases)[index] = values[index]; + } + REQUIRE(AttributeMatrix::Create(dataStructure, "Ensemble Data", {3}) != nullptr); + + auto args = ComputeNumFeaturesFilter().getDefaultArguments(); + args.insertOrAssign(ComputeNumFeaturesFilter::k_FeaturePhasesArrayPath_Key, std::make_any(DataPath({"Feature Data", "Phases"}))); + args.insertOrAssign(ComputeNumFeaturesFilter::k_EnsembleAttributeMatrixPath_Key, std::make_any(DataPath({"Ensemble Data"}))); + args.insertOrAssign(ComputeNumFeaturesFilter::k_NumFeaturesArrayName_Key, std::make_any("Counts")); + return args; +} + +void RequireInvalidPhase(const std::vector& values, const std::string& expectedValue, const std::string& expectedTuple) +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure; + const auto args = CreatePhaseFixture(dataStructure, values); + ComputeNumFeaturesFilter filter; + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); + REQUIRE(executeResult.result.errors().size() == 1); + const auto& error = executeResult.result.errors().front(); + REQUIRE(error.code == -9740); + REQUIRE_THAT(error.message, Catch::Matchers::Contains(expectedValue)); + REQUIRE_THAT(error.message, Catch::Matchers::Contains(expectedTuple)); + REQUIRE_THAT(error.message, Catch::Matchers::Contains("Ensemble tuple count 3")); + + // Validation must finish before counting any features. + const DataPath countsPath({"Ensemble Data", "Counts"}); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(countsPath)); + const auto& counts = dataStructure.getDataRefAs(countsPath); + REQUIRE(counts[0] == 0); + REQUIRE(counts[1] == 0); + REQUIRE(counts[2] == 0); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} } // namespace +TEST_CASE("SimplnxCore::ComputeNumFeaturesFilter: Feature Phases requires 1 component", "[SimplnxCore][ComputeNumFeaturesFilter][ComponentShape]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure; + const auto args = CreatePhaseFixture(dataStructure, {0, 0, 1, 1}, {2}); + ComputeNumFeaturesFilter filter; + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().front().code == -208); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ComputeNumFeaturesFilter: Feature Phases requires int32", "[SimplnxCore][ComputeNumFeaturesFilter][ComponentShape]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure; + const auto args = CreatePhaseFixture(dataStructure, {0.0F, 1.0F}); + ComputeNumFeaturesFilter filter; + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().front().code == -207); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::ComputeNumFeaturesFilter: Rejects phase above Ensemble range", "[SimplnxCore][ComputeNumFeaturesFilter][ComponentShape]") +{ + RequireInvalidPhase({0, 1, 5}, "value 5", "tuple index 2"); +} + +TEST_CASE("SimplnxCore::ComputeNumFeaturesFilter: Rejects negative phase", "[SimplnxCore][ComputeNumFeaturesFilter][ComponentShape]") +{ + RequireInvalidPhase({0, -1}, "value -1", "tuple index 1"); +} + +TEST_CASE("SimplnxCore::ComputeNumFeaturesFilter: Counts valid phases and skips feature zero", "[SimplnxCore][ComputeNumFeaturesFilter][ComponentShape]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure; + const auto args = CreatePhaseFixture(dataStructure, {0, 1, 2, 1, 0, 2, 1}); + ComputeNumFeaturesFilter filter; + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + const DataPath countsPath({"Ensemble Data", "Counts"}); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(countsPath)); + const auto& counts = dataStructure.getDataRefAs(countsPath); + REQUIRE(counts.getNumberOfTuples() == 3); + REQUIRE(counts[0] == 1); + REQUIRE(counts[1] == 3); + REQUIRE(counts[2] == 2); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + TEST_CASE("SimplnxCore::ComputeNumFeaturesFilter: Valid filter execution", "[SimplnxCore][ComputeNumFeaturesFilter]") { UnitTest::LoadPlugins(); From c05632ac7d43768dd06c225b7511ce4f65eefc5f Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 6 Oct 2026 23:06:52 -0400 Subject: [PATCH 08/23] BUG: Fix ComputeGBCDPoleFigure GBCD component shape check * CS-11: the Input GBCD DataArray had no component-shape check. The algorithm reads 5 bin dimensions from the component shape and then 2 hemisphere values per bin, so a DataArray with another shape read past the end of the shape vector or of the data. Preflight now requires 6 component dimensions, the last equal to 2 and the others positive, and returns -34645 otherwise. The bin dimensions depend on the GBCD resolution, so a fixed parameter shape cannot express this * Document the requirement and add tests Co-Authored-By: Claude Opus 5.5 --- .../docs/ComputeGBCDPoleFigureFilter.md | 2 ++ .../Filters/ComputeGBCDPoleFigureFilter.cpp | 11 ++++++++ .../test/ComputeGBCDPoleFigureTest.cpp | 26 +++++++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/src/Plugins/OrientationAnalysis/docs/ComputeGBCDPoleFigureFilter.md b/src/Plugins/OrientationAnalysis/docs/ComputeGBCDPoleFigureFilter.md index 42e67b5054..dc6d0607ae 100644 --- a/src/Plugins/OrientationAnalysis/docs/ComputeGBCDPoleFigureFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/ComputeGBCDPoleFigureFilter.md @@ -14,6 +14,8 @@ The GBCD is a 5-dimensional histogram that captures the statistical distribution ![Using ParaView's Threshold filter + Cells to Points + Delaunay2D Filters](Images/Small_IN100_GBCD_Delaunay2D.png) +The GBCD DataArray must have the 6-dimension component shape that Compute GBCD creates (5 positive bin dimensions followed by a final dimension of 2 (hemispheres)). + ## Algorithm The image spans the stereographic projection square [-1,1] x [-1,1]. The coordinates are projection values, not lengths, so the geometry length unit is "Unspecified". diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeGBCDPoleFigureFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeGBCDPoleFigureFilter.cpp index 9789de67c0..a08cabfcfa 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeGBCDPoleFigureFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeGBCDPoleFigureFilter.cpp @@ -17,6 +17,9 @@ #include "simplnx/Utilities/SIMPLConversion.hpp" +#include + +#include #include using namespace nx::core; @@ -109,6 +112,14 @@ IFilter::PreflightResult ComputeGBCDPoleFigureFilter::preflightImpl(const DataSt const auto& gbcd = dataStructure.getDataRefAs(pGBCDArrayPathValue); + const auto componentShape = gbcd.getComponentShape(); + if(componentShape.size() != 6 || componentShape[5] != 2 || std::any_of(componentShape.begin(), componentShape.end(), [](usize dimension) { return dimension == 0; })) + { + return {MakeErrorResult(-34645, fmt::format("The DataArray '{}' selected for **Input GBCD** has component shape [{}]. The GBCD DataArray must have 6 component dimensions: 5 " + "positive bin dimensions followed by a final dimension of 2 (hemispheres).", + pGBCDArrayPathValue.toString(), fmt::join(componentShape, ", ")))}; + } + auto numEnsembles = gbcd.getNumberOfTuples(); if(pPhaseOfInterestValue >= numEnsembles) { diff --git a/src/Plugins/OrientationAnalysis/test/ComputeGBCDPoleFigureTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeGBCDPoleFigureTest.cpp index 0db7cb38a7..fd3a031d6c 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeGBCDPoleFigureTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeGBCDPoleFigureTest.cpp @@ -270,3 +270,29 @@ TEST_CASE("OrientationAnalysis::ComputeGBCDPoleFigureFilter: Phase and Laue Inde UnitTest::CheckArraysInheritTupleDims(dataStructure); } + +TEST_CASE("OrientationAnalysis::ComputeGBCDPoleFigureFilter: Invalid GBCD Component Shape", "[OrientationAnalysis][ComputeGBCDPoleFigureFilter][ComponentShape]") +{ + const auto componentShape = GENERATE(ShapeType{1}, ShapeType{2, 2, 2, 2, 2, 1}, ShapeType{0, 2, 2, 2, 2, 2}, ShapeType{2, 0, 2, 2, 2, 2}, ShapeType{2, 2, 0, 2, 2, 2}, ShapeType{2, 2, 2, 0, 2, 2}, + ShapeType{2, 2, 2, 2, 0, 2}); + CAPTURE(componentShape); + + DataStructure dataStructure; + const DataPath gbcdPath({"Malformed GBCD"}); + const DataPath crystalStructuresPath({"Crystal Structures"}); + auto* gbcd = UnitTest::CreateTestDataArray(dataStructure, gbcdPath.getTargetName(), {2}, componentShape); + REQUIRE(gbcd != nullptr); + auto* crystalStructures = UnitTest::CreateTestDataArray(dataStructure, crystalStructuresPath.getTargetName(), {2}, {1}); + REQUIRE(crystalStructures != nullptr); + + ComputeGBCDPoleFigureFilter filter; + Arguments args = filter.getDefaultArguments(); + args.insertOrAssign(ComputeGBCDPoleFigureFilter::k_PhaseOfInterest_Key, std::make_any(1)); + args.insertOrAssign(ComputeGBCDPoleFigureFilter::k_GBCDArrayPath_Key, std::make_any(gbcdPath)); + args.insertOrAssign(ComputeGBCDPoleFigureFilter::k_CrystalStructuresArrayPath_Key, std::make_any(crystalStructuresPath)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + REQUIRE(preflightResult.outputActions.errors()[0].code == -34645); +} From 43ac0597e6a0f9c4de6fc1b6cafbab575d902ca9 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 6 Oct 2026 23:06:52 -0400 Subject: [PATCH 09/23] BUG: Fix WriteGBCDGMTFile GBCD component shape check * CS-12: the GBCD DataArray had no component-shape check. The algorithm reads 5 bin dimensions from the component shape and then 2 hemisphere values per bin, so a DataArray with another shape read past the end of the shape vector or of the data. Preflight now requires 6 component dimensions, the last equal to 2 and the others positive, and returns -96713 otherwise. The bin dimensions depend on the GBCD resolution, so a fixed parameter shape cannot express this * Document the requirement and add tests Co-Authored-By: Claude Opus 5.5 --- .../docs/WriteGBCDGMTFileFilter.md | 2 ++ .../Filters/WriteGBCDGMTFileFilter.cpp | 11 ++++++++ .../test/WriteGBCDGMTFileTest.cpp | 27 +++++++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/src/Plugins/OrientationAnalysis/docs/WriteGBCDGMTFileFilter.md b/src/Plugins/OrientationAnalysis/docs/WriteGBCDGMTFileFilter.md index c4f33b475d..4a0f7221b0 100644 --- a/src/Plugins/OrientationAnalysis/docs/WriteGBCDGMTFileFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/WriteGBCDGMTFileFilter.md @@ -37,6 +37,8 @@ It samples the GBCD for the selected phase and misorientation and writes the res % Auto generated parameter table will be inserted here +The GBCD DataArray must have the 6-dimension component shape that Compute GBCD creates (5 positive bin dimensions followed by a final dimension of 2 (hemispheres)). + ## Example Pipelines + (08) Small IN100 GBCD diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/WriteGBCDGMTFileFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/WriteGBCDGMTFileFilter.cpp index b3c649038d..2203b64410 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/WriteGBCDGMTFileFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/WriteGBCDGMTFileFilter.cpp @@ -12,6 +12,9 @@ #include "simplnx/Utilities/SIMPLConversion.hpp" +#include + +#include #include namespace fs = std::filesystem; @@ -108,6 +111,14 @@ IFilter::PreflightResult WriteGBCDGMTFileFilter::preflightImpl(const DataStructu const auto& gbcd = dataStructure.getDataRefAs(pGBCDArrayPathValue); + const auto componentShape = gbcd.getComponentShape(); + if(componentShape.size() != 6 || componentShape[5] != 2 || std::any_of(componentShape.begin(), componentShape.end(), [](usize dimension) { return dimension == 0; })) + { + return {MakeErrorResult(-96713, fmt::format("The DataArray '{}' selected for **GBCD** has component shape [{}]. The GBCD DataArray must have 6 component dimensions: 5 positive bin " + "dimensions followed by a final dimension of 2 (hemispheres).", + pGBCDArrayPathValue.toString(), fmt::join(componentShape, ", ")))}; + } + auto numEnsembles = gbcd.getNumberOfTuples(); if(pPhaseOfInterestValue >= numEnsembles) { diff --git a/src/Plugins/OrientationAnalysis/test/WriteGBCDGMTFileTest.cpp b/src/Plugins/OrientationAnalysis/test/WriteGBCDGMTFileTest.cpp index 9b8a20b84e..b1e1f3182e 100644 --- a/src/Plugins/OrientationAnalysis/test/WriteGBCDGMTFileTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/WriteGBCDGMTFileTest.cpp @@ -479,3 +479,30 @@ TEST_CASE("OrientationAnalysis::WriteGBCDGMTFileFilter: SIMPL Backwards Compatib } } } + +TEST_CASE("OrientationAnalysis::WriteGBCDGMTFileFilter: Invalid GBCD Component Shape", "[OrientationAnalysis][WriteGBCDGMTFile][ComponentShape]") +{ + const auto componentShape = GENERATE(ShapeType{1}, ShapeType{2, 2, 2, 2, 2, 1}, ShapeType{0, 2, 2, 2, 2, 2}, ShapeType{2, 0, 2, 2, 2, 2}, ShapeType{2, 2, 0, 2, 2, 2}, ShapeType{2, 2, 2, 0, 2, 2}, + ShapeType{2, 2, 2, 2, 0, 2}); + CAPTURE(componentShape); + + DataStructure dataStructure; + const DataPath gbcdPath({"Malformed GBCD"}); + const DataPath crystalStructuresPath({"Crystal Structures"}); + auto* gbcd = UnitTest::CreateTestDataArray(dataStructure, gbcdPath.getTargetName(), {2}, componentShape); + REQUIRE(gbcd != nullptr); + auto* crystalStructures = UnitTest::CreateTestDataArray(dataStructure, crystalStructuresPath.getTargetName(), {2}, {1}); + REQUIRE(crystalStructures != nullptr); + + WriteGBCDGMTFileFilter filter; + Arguments args = filter.getDefaultArguments(); + args.insertOrAssign(WriteGBCDGMTFileFilter::k_PhaseOfInterest_Key, std::make_any(1)); + args.insertOrAssign(WriteGBCDGMTFileFilter::k_GBCDArrayPath_Key, std::make_any(gbcdPath)); + args.insertOrAssign(WriteGBCDGMTFileFilter::k_CrystalStructuresArrayPath_Key, std::make_any(crystalStructuresPath)); + args.insertOrAssign(WriteGBCDGMTFileFilter::k_OutputFile_Key, std::make_any(fs::path(unit_test::k_BinaryTestOutputDir.view()) / "invalid_gbcd_shape.dat")); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + REQUIRE(preflightResult.outputActions.errors()[0].code == -96713); +} From ec18b0285658987715f3619419ecf78644177159 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 6 Oct 2026 23:11:12 -0400 Subject: [PATCH 10/23] BUG: Fix DBSCAN mask component shape and tuple count checks * CS-13: the Cell Mask Array parameter declared no component shape, and preflight did not check that the Mask Array has the same number of tuples as the Attribute Array to Cluster. The Scanline algorithm rejected both at execute (-54062), but the Direct algorithm read the mask by flat element, so the two algorithms disagreed - The parameter now declares a component shape of 1 (-208) - Preflight returns -7587 when Use Mask is on and the tuple counts differ - The Scanline execute checks -54062 and -54060 are removed, because parameter validation and preflight now make them unreachable * Add V&V deviation DBSCAN-D6 and the two new tests to the V&V report. The deviations document now names DREAM3D 6.5.171, the reference release for V&V documents Co-Authored-By: Claude Opus 5.5 --- src/Plugins/SimplnxCore/docs/DBSCANFilter.md | 2 + .../Filters/Algorithms/DBSCANScanline.cpp | 12 ----- .../src/SimplnxCore/Filters/DBSCANFilter.cpp | 15 +++++- src/Plugins/SimplnxCore/test/DBSCANTest.cpp | 51 +++++++++++++++++++ src/Plugins/SimplnxCore/vv/DBSCANFilter.md | 17 ++++--- .../SimplnxCore/vv/deviations/DBSCANFilter.md | 25 +++++++-- 6 files changed, 100 insertions(+), 22 deletions(-) diff --git a/src/Plugins/SimplnxCore/docs/DBSCANFilter.md b/src/Plugins/SimplnxCore/docs/DBSCANFilter.md index 5ea77bc10a..b3f3c7baa9 100644 --- a/src/Plugins/SimplnxCore/docs/DBSCANFilter.md +++ b/src/Plugins/SimplnxCore/docs/DBSCANFilter.md @@ -188,6 +188,8 @@ The *Distance Metric* parameter provides the following choices: - **Pearson [4]**: Uses the Pearson correlation distance, measuring linear correlation dissimilarity. - **Squared Pearson [5]**: Uses the squared Pearson correlation distance. +**Cell Mask Array** must have 1 component. + ## Note on Randomness The inclusion of randomness in this algorithm is solely to attempt to reduce bias from starting cluster. Three parse order options are available: *Low Density First* (deterministic, no seed needed), *Random* (non-deterministic, uses a time-based seed), and *Seeded Random* (deterministic, uses a user-supplied seed value for reproducibility). Low Density First produced identical results faster in our test cases, but the random initialization is truest to the well known DBSCAN algorithm. diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/DBSCANScanline.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/DBSCANScanline.cpp index aadf9e5bc3..94205c9b20 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/DBSCANScanline.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/DBSCANScanline.cpp @@ -321,19 +321,7 @@ class ExternalGDCF { return MakeErrorResult(-54061, "DBSCAN out-of-core execution requires registered external-sort and temporary-record-store providers."); } - if(m_Dimensions != 2 && m_Dimensions != 3) - { - return MakeErrorResult(-54060, "Input components invalid. Only 2 or 3 accepted."); - } const usize tupleCount = m_InputStore.getNumberOfTuples(); - if(m_InputStore.getNumberOfComponents() != m_Dimensions) - { - return MakeErrorResult(-54060, "DBSCAN input component count does not match the selected dimensionality."); - } - if(m_MaskStore != nullptr && (m_MaskStore->getNumberOfTuples() != tupleCount || m_MaskStore->getNumberOfComponents() != 1)) - { - return MakeErrorResult(-54062, "DBSCAN mask tuple count does not match the clustering array or the mask is not scalar."); - } if(m_InputValues.MinPoints < 0) { return MakeErrorResult(-54063, "DBSCAN Minimum Points cannot be negative."); diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/DBSCANFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/DBSCANFilter.cpp index aafec862c1..71fd7d0682 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/DBSCANFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/DBSCANFilter.cpp @@ -17,6 +17,7 @@ #include "simplnx/Parameters/NumberParameter.hpp" #include "simplnx/Parameters/StringParameter.hpp" #include "simplnx/Utilities/ClusteringUtilities.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" @@ -86,7 +87,7 @@ Parameters DBSCANFilter::parameters() const params.insertLinkableParameter(std::make_unique(k_UseMask_Key, "Use Mask Array", "Specifies whether or not to use a mask array", false)); params.insert(std::make_unique(k_MaskArrayPath_Key, "Cell Mask Array", "DataPath to the boolean or uint8 mask array. Values that are true will mark that cell/point as usable.", DataPath{}, - ArraySelectionParameter::AllowedTypes{DataType::boolean, DataType::uint8})); + ArraySelectionParameter::AllowedTypes{DataType::boolean, DataType::uint8}, ArraySelectionParameter::AllowedComponentShapes{{1}})); params.insertSeparator(Parameters::Separator{"Input Data Objects"}); params.insert(std::make_unique(k_SelectedArrayPath_Key, "Attribute Array to Cluster", "The data array to cluster", DataPath{}, nx::core::GetAllNumericTypes(), @@ -150,6 +151,18 @@ IFilter::PreflightResult DBSCANFilter::preflightImpl(const DataStructure& dataSt const auto& clusterArray = dataStructure.getDataRefAs(pSelectedArrayPathValue); + if(filterArgs.value(k_UseMask_Key)) + { + const auto maskPath = filterArgs.value(k_MaskArrayPath_Key); + if(!CheckArraysHaveSameTupleCount(dataStructure, {pSelectedArrayPathValue, maskPath})) + { + const auto& maskArray = dataStructure.getDataRefAs(maskPath); + return MakePreflightErrorResult( + -7587, fmt::format("**Attribute Array to Cluster** DataArray '{}' has {} tuples, but **Cell Mask Array** DataArray '{}' has {} tuples. Select DataArrays with the same number of tuples.", + pSelectedArrayPathValue.toString(), clusterArray.getNumberOfTuples(), maskPath.toString(), maskArray.getNumberOfTuples())); + } + } + { auto createAction = std::make_unique(DataType::int32, clusterArray.getTupleShape(), std::vector{1}, pSelectedArrayPathValue.replaceName(pFeatureIdsArrayNameValue), "", "0"); diff --git a/src/Plugins/SimplnxCore/test/DBSCANTest.cpp b/src/Plugins/SimplnxCore/test/DBSCANTest.cpp index 69d2953dca..acf36e7a06 100644 --- a/src/Plugins/SimplnxCore/test/DBSCANTest.cpp +++ b/src/Plugins/SimplnxCore/test/DBSCANTest.cpp @@ -678,6 +678,57 @@ TEST_CASE("SimplnxCore::DBSCANFilter: No-mask preflight does not create a cell-s CHECK(preflightResult.outputActions.value().deferredActions.empty()); } +TEST_CASE("SimplnxCore::DBSCAN: Reject multi-component mask in preflight", "[SimplnxCore][DBSCAN][ComponentShape]") +{ + DataStructure dataStructure; + auto* points = Float32Array::CreateWithStore>(dataStructure, "Points", ShapeType{4}, ShapeType{2}); + REQUIRE(points != nullptr); + auto* mask = UInt8Array::CreateWithStore>(dataStructure, "Mask", ShapeType{4}, ShapeType{2}); + REQUIRE(mask != nullptr); + + DBSCANFilter filter; + Arguments args = filter.getDefaultArguments(); + args.insertOrAssign(DBSCANFilter::k_SelectedArrayPath_Key, std::make_any(DataPath({"Points"}))); + args.insertOrAssign(DBSCANFilter::k_MaskArrayPath_Key, std::make_any(DataPath({"Mask"}))); + args.insertOrAssign(DBSCANFilter::k_FeatureAMPath_Key, std::make_any(DataPath({"Features"}))); + args.insertOrAssign(DBSCANFilter::k_UseMask_Key, std::make_any(false)); + auto unmaskedResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(unmaskedResult.outputActions); + + args.insertOrAssign(DBSCANFilter::k_UseMask_Key, std::make_any(true)); + auto preflightResult = filter.preflight(dataStructure, args); + REQUIRE(preflightResult.outputActions.invalid()); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + REQUIRE(preflightResult.outputActions.errors()[0].code == -208); +} + +TEST_CASE("SimplnxCore::DBSCAN: Reject short mask in preflight", "[SimplnxCore][DBSCAN][ComponentShape]") +{ + DataStructure dataStructure; + auto* points = Float32Array::CreateWithStore>(dataStructure, "Points", ShapeType{4}, ShapeType{2}); + REQUIRE(points != nullptr); + auto* mask = UInt8Array::CreateWithStore>(dataStructure, "Mask", ShapeType{3}, ShapeType{1}); + REQUIRE(mask != nullptr); + + DBSCANFilter filter; + Arguments args = filter.getDefaultArguments(); + args.insertOrAssign(DBSCANFilter::k_SelectedArrayPath_Key, std::make_any(DataPath({"Points"}))); + args.insertOrAssign(DBSCANFilter::k_MaskArrayPath_Key, std::make_any(DataPath({"Mask"}))); + args.insertOrAssign(DBSCANFilter::k_FeatureAMPath_Key, std::make_any(DataPath({"Features"}))); + args.insertOrAssign(DBSCANFilter::k_UseMask_Key, std::make_any(false)); + auto unmaskedResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(unmaskedResult.outputActions); + + args.insertOrAssign(DBSCANFilter::k_UseMask_Key, std::make_any(true)); + auto preflightResult = filter.preflight(dataStructure, args); + REQUIRE(preflightResult.outputActions.invalid()); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + REQUIRE(preflightResult.outputActions.errors()[0].code == -7587); + const auto& message = preflightResult.outputActions.errors()[0].message; + REQUIRE_THAT(message, Catch::Matchers::Contains("DataArray 'Points' has 4 tuples")); + REQUIRE_THAT(message, Catch::Matchers::Contains("DataArray 'Mask' has 3 tuples")); +} + TEST_CASE("SimplnxCore::DBSCANFilter: SIMPL Backwards Compatibility", "[SimplnxCore][DBSCANFilter][BackwardsCompatibility]") { auto app = Application::GetOrCreateInstance(); diff --git a/src/Plugins/SimplnxCore/vv/DBSCANFilter.md b/src/Plugins/SimplnxCore/vv/DBSCANFilter.md index 89dc8ed1e8..bd08424828 100644 --- a/src/Plugins/SimplnxCore/vv/DBSCANFilter.md +++ b/src/Plugins/SimplnxCore/vv/DBSCANFilter.md @@ -16,8 +16,8 @@ |------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Algorithm Relationship | **Rewrite** — SIMPLNX implements GDCF (Grid-based DBSCAN, Boonchoo et al. 2019, DOI 10.1016/j.patcog.2019.01.034) in place of the traditional point-by-point DBSCAN in legacy DREAM3D. UUID changed from `c2d4f1e8` to `763dad44` (legacy UUID retained via SIMPL mapper). | | Oracle (confirmed) | **Class 2 (Reference — scikit-learn 1.7.1 DBSCAN) primary + Class 4 (Invariant) companion.** Input data independently generated from deterministic sklearn scripts in `dbscan_vv/dbscan_data_proj/`. Phase 6 reconciliation complete: 4/6 datasets exact match; 2 deviations (ansio, varied) fully explained by DBSCAN-D1 (GDCF vs. traditional DBSCAN). See Phase 5 + Phase 6. | -| Code paths enumerated | 18 of 21 enumerated paths exercised. The untested invalid-component, unvisited-border, and empty-forest paths remain identified below. | -| Tests today | 12 registered tests in this file plus 1 hidden HDF5 batch-tail test. The OOC build also registers seven external-storage tests. Existing analytical and regression assertions are preserved. | +| Code paths enumerated | 20 of 23 enumerated paths exercised. The untested invalid-component, unvisited-border, and empty-forest paths remain identified below. | +| Tests today | 14 registered tests in this file plus 1 hidden HDF5 batch-tail test. The OOC build also registers seven external-storage tests. Existing analytical and regression assertions are preserved. | | Exemplar archive | **`dbscan_test.tar.gz` — promoted to regression fixtures (Phase 6/10).** Originally circular oracle; independently verified via Class 2 sklearn oracle (Phase 6). LDF arrays now pin verified-correct SIMPLNX output. See provenance sidecar. | | Legacy comparison | Historical comparison used a local legacy proof build and the sklearn oracle. It supports the documented GDCF-versus-traditional DBSCAN differences. This OOC recertification does not establish a new stock DREAM3D 6.5.171 binary comparison. | | Bug flags | ✅ Circular oracle resolved (Phase 6) — `dbscan_test.tar.gz` LDF arrays promoted to regression fixtures; Class 2 sklearn oracle confirms correctness. 2 expected GDCF deviations (ansio, varied) documented as DBSCAN-D1. **1 SIMPLNX bug found and fixed during V&V:** `ParseOrder::Random` fed the user-supplied seed to the shuffle instead of the documented time-based seed, making "Random" a silent duplicate of "Seeded Random" (see Phase 7). **1 robustness defect found and fixed:** an all-false mask left the grid bounds NaN and those NaNs were cast to `usize` while computing grid dimensions (undefined behavior). | @@ -84,7 +84,7 @@ The prior Random-seed and all-false-mask fixes are described in Phase 7. They ha ## Code path coverage -18 of 21 enumerated paths exercised. +20 of 23 enumerated paths exercised. Source: `Algorithms/DBSCAN.cpp` (34 lines), `DBSCANDirect.cpp` (1,274 lines), and `DBSCANScanline.cpp` (3,655 lines). Scanline selects ExternalGDCF when a routed array is on disk; forced Scanline on resident arrays uses its resident fallback. @@ -114,6 +114,8 @@ Logical phases: **(a) Grid construction** — build HyperGridBitMap and bin poin | 18 | External I/O | A 65,536-point input batch and a two-point partial tail | `genuine HDF5 65536-batch tail oracle` | | 19 | External clustering | Adjacent core grids merge across the input-batch boundary | `genuine HDF5 65536-batch tail oracle` — four active points form one cluster and every other label is zero | | 20 | Preflight | No-mask mode avoids a cell-sized temporary mask | `No-mask preflight does not create a cell-sized temporary mask` | +| 21 | Preflight | Enabled mask has component shape other than `{1}` → error `-208` | `Reject multi-component mask in preflight` | +| 22 | Preflight | Enabled mask and input array have unequal tuple counts → error `-7587` | `Reject short mask in preflight` | ## Test inventory @@ -131,6 +133,8 @@ Logical phases: **(a) Grid construction** — build HyperGridBitMap and bin poin | `SimplnxCore::DBSCAN: Analytical Fixture F1 - No Clusters Warning` | new-for-V&V | Class 1 oracle. 4 corner points, ε=0.1, minPts=5. Covers code path #5 (warning -85640). Self-contained inline data. | | `SimplnxCore::DBSCAN: Analytical Fixture F2 - Mask Exclusion` | new-for-V&V | Class 1 oracle. 3 points, P2 masked. Covers code path #4 (mask=true). Self-contained inline data. | | `SimplnxCore::DBSCAN: Analytical Fixture F3 - All Points Masked` | new-for-V&V | Class 1 oracle. Same 3 points as F2 with every point masked off. Covers code path #4b and pins the all-false-mask contract (warning `-85640`, all IDs 0, AM 1 tuple) that the NaN-bounds guard makes architecture-independent. Self-contained inline data. | +| `SimplnxCore::DBSCAN: Reject multi-component mask in preflight` | new-for-CS-13 | Four coordinate tuples and four mask tuples with component shape `{2}`; preflight rejects with `-208` when the mask is enabled and accepts when disabled. See DBSCAN-D6. | +| `SimplnxCore::DBSCAN: Reject short mask in preflight` | new-for-CS-13 | Four coordinate tuples and three scalar mask tuples; preflight rejects with `-7587` when the mask is enabled and accepts when disabled. See DBSCAN-D6. | | Class 4 invariant assertions | new-for-V&V | `CheckClusterInvariants()` hooked into `LDFTestCase2D` and `RandomTestCase2D` — covers all 18 2D test runs — and into F1/F2/F3. 3D test has inline AM tuple check but not the full helper. | | `SimplnxCore::DBSCANFilter: No-mask preflight does not create a cell-sized temporary mask` | kept | Checks action allocation in no-mask mode. | | `SimplnxCore::DBSCAN: genuine HDF5 65536-batch tail oracle` | new-for-V&V | 65,538 points, of which five are active. Four points form two adjacent core grids spanning the batch boundary; the fifth is distant noise. All labels and the feature count are checked. | @@ -154,6 +158,7 @@ OOC recertification, 2026-09-18: serial CTest passed 12/12 in `NX-Com-Qt69-Vtk96 - `DBSCAN-D3` — `use_precaching` parameter removed; old pipelines silently ignore it — see `vv/deviations/DBSCANFilter.md` - `DBSCAN-D4` — Parameter key renamed `init_type_index` → `parse_order_index`; `upgradeParametersImpl` not implemented — early SIMPLNX pipelines silently default to `LowDensityFirst` — see `vv/deviations/DBSCANFilter.md` - `DBSCAN-D5` — `SeededRandom` shuffle replaced with `std::shuffle`; same seed now produces a different (correct, unbiased) permutation compared to pre-PR builds — see `vv/deviations/DBSCANFilter.md` +- `DBSCAN-D6` — Multi-component masks and unequal mask/input tuple counts now fail preflight before Direct or Scanline execution — see `vv/deviations/DBSCANFilter.md` --- @@ -415,7 +420,7 @@ Code paths newly covered: path #4 (mask=true), path #5 (no core grids warning). **Status: Complete (2026-08-05).** -The `dbscan_test.tar.gz` LDF exemplar arrays have been promoted from "circular oracle" to "regression fixtures" — the Class 2 sklearn oracle (Phase 6) independently confirmed SIMPLNX correctness for 4/6 datasets, and the legacy 6.5.172 comparison (Phase 9) corroborated the DBSCAN-D1 classification for the remaining 2 datasets. No archive regeneration is required; no new exemplar archive was created. +The `dbscan_test.tar.gz` LDF exemplar arrays have been promoted from "circular oracle" to "regression fixtures" — the Class 2 sklearn oracle (Phase 6) independently confirmed SIMPLNX correctness for 4/6 datasets, and the historical local legacy proof build comparison (Phase 9) corroborated the DBSCAN-D1 classification for the remaining 2 datasets. No archive regeneration is required; no new exemplar archive was created. Provenance sidecar updated at `src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md` with both Phase 6 and Phase 9 entries. @@ -448,11 +453,11 @@ All V&V working artifacts are stored in the `dbscan_vv/` working folder outside | `oracle_results.json` | Sklearn oracle results — cluster counts + sizes per dataset (Phase 5) | | `compare_exemplar_vs_oracle.py` | Phase 6 three-way comparison: exemplar vs. sklearn (Phase 6) | | `phase6_comparison_results.json` | Phase 6 comparison results — SIMPLNX vs. sklearn (Phase 6) | -| `phase9_ab_test.py` | A/B test script: creates 6.5 HDF5 input, runs legacy 6.5.172, three-way comparison (Phase 9) | +| `phase9_ab_test.py` | A/B test script: creates 6.5 HDF5 input, runs the historical local legacy proof build, three-way comparison (Phase 9) | | `6_5_input.dream3d` | HDF5 input file in DREAM3D 6.5 format used by legacy PipelineRunner (Phase 9) | | `dbscan_6_5_pipeline.json` | DREAM3D 6.5 pipeline JSON: DataContainerReader + 6×DBSCAN + DataContainerWriter (Phase 9) | | `6_5_output.dream3d` | Output from the local legacy proof build PipelineRunner containing legacy cluster IDs (Phase 9) | -| `phase9_comparison_results.json` | Phase 9 three-way comparison results (legacy 6.5.172 vs. sklearn vs. SIMPLNX) (Phase 9) | +| `phase9_comparison_results.json` | Phase 9 three-way comparison results (historical local legacy proof build vs. sklearn vs. SIMPLNX) (Phase 9) | No SBIR submission packaging required at this stage. Artifacts are on-disk in the development environment; all scripts are self-contained and reproducible. diff --git a/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md b/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md index d5de6b5eb2..fc17024be9 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md @@ -1,6 +1,6 @@ -# Deviations from DREAM3D 6.5.172: DBSCANFilter +# Deviations from DREAM3D 6.5.171: DBSCANFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.172 equivalent (D1–D4), plus within-SIMPLNX changes introduced by this PR that break reproducibility for existing users (D5). +This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (D1–D4), plus within-SIMPLNX changes introduced by this PR that break reproducibility for existing users (D5–D6). Entries are referenced by stable ID (`DBSCAN-D`) from the V&V report and from public migration guidance. The Filter UUID fields are the permanent cross-reference anchors. @@ -19,7 +19,7 @@ Entries are referenced by stable ID (`DBSCAN-D`) from the V&V report and from **Symptom:** Sparse datasets where individual data points each have ≥ minPoints neighbors within ε (and would form clusters under traditional DBSCAN) may produce **more noise (cluster 0) points in SIMPLNX** if those neighborhoods span multiple grid cells with fewer than minPoints points per cell. -**Confirmed (Phase 9, 2026-08-05):** DREAM3D 6.5.172 run on the 6 sklearn toy datasets confirms: legacy finds 6 clusters (ansio) and 11 clusters (varied) — matching sklearn — while SIMPLNX finds only 3 for both. The large-cluster sizes agree for varied ([87,166,169] — exact three-way match); for ansio the large clusters are close but not identical (sklearn oracle: [152,155,157]; SIMPLNX: [153,156,161] — small boundary discrepancy also attributable to GDCF grid-cell vs. point-level core definition). Only the micro-clusters (≤6 points each) are absent in SIMPLNX. Evidence: `dbscan_vv/phase9_comparison_results.json`. +**Confirmed (Phase 9, 2026-08-05):** The historical local legacy proof build run on the 6 sklearn toy datasets confirms: legacy finds 6 clusters (ansio) and 11 clusters (varied) — matching sklearn — while SIMPLNX finds only 3 for both. The large-cluster sizes agree for varied ([87,166,169] — exact three-way match); for ansio the large clusters are close but not identical (sklearn oracle: [152,155,157]; SIMPLNX: [153,156,161] — small boundary discrepancy also attributable to GDCF grid-cell vs. point-level core definition). Only the micro-clusters (≤6 points each) are absent in SIMPLNX. Evidence: `dbscan_vv/phase9_comparison_results.json`. **Root cause:** Algorithmic choice. SIMPLNX implements Grid-based DBSCAN (GDCF, Boonchoo et al. 2019). The core-object definition differs fundamentally: - **Legacy (traditional DBSCAN)**: A data point `p` is a *core point* if the ε-ball centered on `p` contains ≥ minPoints data points (inclusive of `p` itself). Cluster membership is point-centric. @@ -117,3 +117,22 @@ The `FromSIMPLJson` converter (`DBSCANFilter.cpp`, starting at line 242) is not **Affected users:** Users who relied on a specific `SeededRandom` seed to reproduce a particular cluster-ID assignment from a pre-PR build. The same seed now produces a correct unbiased shuffle instead of the previously biased one, so the resulting cluster-ID numbers will differ. Users running `LowDensityFirst` (the default) are entirely unaffected. Users running `Random` (time-based seed, non-deterministic by design) are unaffected in practice. **Recommendation:** If reproducibility of specific cluster-ID values across builds is required, re-run with the same seed on the current build and use the new output as the reference. Cluster membership (grouping) is preserved for densely-clustered datasets; only the numeric label assigned to each cluster may change. + +--- + +## DBSCAN-D6 + +| Field | Value | +|---|---| +| **Deviation ID** | `DBSCAN-D6` | +| **SIMPLNX UUID** | `763dad44-fad7-4606-808f-617867257b98` | +| **Legacy SIMPL UUID** | `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e` | +| **Status** | active | + +**Symptom:** With **Use Mask Array** enabled, SIMPLNX previously accepted a multi-component or short **Cell Mask Array** in preflight. The Direct path read the mask by flat element, while the external Scanline path rejected it during execution with `-54062`. Both paths now reject these inputs before execution: component shape other than `{1}` produces `-208`, and unequal tuple counts produce `-7587`. + +**Root cause:** The mask selection parameter did not restrict component shape, and preflight did not compare the tuple counts of **Cell Mask Array** and **Attribute Array to Cluster**. Validation now applies before algorithm dispatch. This is a within-SIMPLNX correction; no new comparison with DREAM3D 6.5.171 is claimed. + +**Affected users:** Users whose pipelines select a multi-component mask or a mask with a different number of tuples from the clustering DataArray. Previously accepted Direct executions may have used unintended mask elements or accessed beyond a short mask. + +**Recommendation:** Select a boolean or uint8 mask DataArray with component shape `{1}` and one tuple per tuple in **Attribute Array to Cluster**. The preflight tuple-count error identifies both DataArrays and their tuple counts. Regression coverage: `SimplnxCore::DBSCANFilter: Reject multi-component mask in preflight` and `SimplnxCore::DBSCANFilter: Reject short mask in preflight` in `DBSCANTest.cpp`. From 6e72b799a87b1f9b129093ae2285cf1b63f0d7c7 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 7 Oct 2026 19:00:28 -0400 Subject: [PATCH 11/23] BUG: Fix ComputeKMeans mask tuple count check When masking is enabled, preflight now rejects a Cell Mask Array whose number of tuples differs from the Attribute Array to Cluster (-7588). Previously the mismatch reached execute, where the Direct path could throw std::out_of_range and the Scanline path failed with a DataStore range error. Co-Authored-By: Claude Opus 5.5 --- .../SimplnxCore/docs/ComputeKMeansFilter.md | 2 + .../Filters/ComputeKMeansFilter.cpp | 13 ++++++ .../SimplnxCore/test/ComputeKMeansTest.cpp | 42 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/src/Plugins/SimplnxCore/docs/ComputeKMeansFilter.md b/src/Plugins/SimplnxCore/docs/ComputeKMeansFilter.md index c42fcb7f05..c366278de1 100644 --- a/src/Plugins/SimplnxCore/docs/ComputeKMeansFilter.md +++ b/src/Plugins/SimplnxCore/docs/ComputeKMeansFilter.md @@ -14,6 +14,8 @@ Each cluster is summarized by its **centroid**, which is the arithmetic mean (th **Cell Mask Array** must have 1 component. +When masking is enabled, **Cell Mask Array** must have the same number of tuples as **Attribute Array to Cluster**. + ## Algorithm Initial centroids are drawn uniformly, with replacement, from all selected tuples. A seed gives different initial centroids than in earlier DREAM3D-NX versions. diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMeansFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMeansFilter.cpp index d01aac2a6a..f688b998f5 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMeansFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMeansFilter.cpp @@ -16,6 +16,7 @@ #include "simplnx/Parameters/NumberParameter.hpp" #include "simplnx/Parameters/StringParameter.hpp" #include "simplnx/Utilities/ClusteringUtilities.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" @@ -126,6 +127,18 @@ IFilter::PreflightResult ComputeKMeansFilter::preflightImpl(const DataStructure& return MakePreflightErrorResult(-7585, "Array to Cluster MUST be a valid DataPath."); } + if(filterArgs.value(k_UseMask_Key)) + { + const auto maskPath = filterArgs.value(k_MaskArrayPath_Key); + if(!CheckArraysHaveSameTupleCount(dataStructure, {pSelectedArrayPathValue, maskPath})) + { + const auto& maskArray = dataStructure.getDataRefAs(maskPath); + return MakePreflightErrorResult( + -7588, fmt::format("**Attribute Array to Cluster** DataArray '{}' has {} tuples, but **Cell Mask Array** DataArray '{}' has {} tuples. Select DataArrays with the same number of tuples.", + pSelectedArrayPathValue.toString(), clusterArray->getNumberOfTuples(), maskPath.toString(), maskArray.getNumberOfTuples())); + } + } + { auto createAction = std::make_unique(DataType::int32, clusterArray->getTupleShape(), std::vector{1}, pSelectedArrayPathValue.replaceName(pFeatureIdsArrayNameValue), "", "0"); diff --git a/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp b/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp index 13e095ee38..1ac58562fe 100644 --- a/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp @@ -635,3 +635,45 @@ TEST_CASE("SimplnxCore::ComputeKMeans: Cell Mask Array requires 1 component", "[ REQUIRE(std::any_of(preflightResult.outputActions.errors().begin(), preflightResult.outputActions.errors().end(), [](const Error& error) { return error.code == -208; })); UnitTest::CheckArraysInheritTupleDims(dataStructure); } + +TEST_CASE("SimplnxCore::ComputeKMeans: Cell Mask Array requires matching tuple counts", "[SimplnxCore][ComputeKMeans][ComponentShape]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure; + auto* cellData = AttributeMatrix::Create(dataStructure, "Cell Data", {6}); + auto* maskData = AttributeMatrix::Create(dataStructure, "Mask Data", {4}); + REQUIRE(cellData != nullptr); + REQUIRE(maskData != nullptr); + REQUIRE(UnitTest::CreateTestDataArray(dataStructure, "Input", {6}, {1}, cellData->getId()) != nullptr); + REQUIRE(BoolArray::CreateWithStore(dataStructure, "Mask", ShapeType{4}, ShapeType{1}, maskData->getId()) != nullptr); + const DataPath inputPath({"Cell Data", "Input"}); + const DataPath maskPath({"Mask Data", "Mask"}); + + ComputeKMeansFilter filter; + auto args = filter.getDefaultArguments(); + args.insertOrAssign(ComputeKMeansFilter::k_InitClusters_Key, std::make_any(1)); + args.insertOrAssign(ComputeKMeansFilter::k_MaskArrayPath_Key, std::make_any(maskPath)); + args.insertOrAssign(ComputeKMeansFilter::k_SelectedArrayPath_Key, std::make_any(inputPath)); + args.insertOrAssign(ComputeKMeansFilter::k_FeatureAMPath_Key, std::make_any(DataPath({"Cluster Data"}))); + + SECTION("Enabled mask rejects fewer tuples") + { + args.insertOrAssign(ComputeKMeansFilter::k_UseMask_Key, std::make_any(true)); + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + const auto& errors = preflightResult.outputActions.errors(); + const auto error = std::find_if(errors.begin(), errors.end(), [](const Error& value) { return value.code == -7588; }); + REQUIRE(error != errors.end()); + CHECK(error->message.find(inputPath.toString()) != std::string::npos); + CHECK(error->message.find(maskPath.toString()) != std::string::npos); + CHECK(error->message.find("6 tuples") != std::string::npos); + CHECK(error->message.find("4 tuples") != std::string::npos); + } + SECTION("Disabled mask ignores fewer tuples") + { + args.insertOrAssign(ComputeKMeansFilter::k_UseMask_Key, std::make_any(false)); + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + } + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} From 7587e055808ffb797014b3ee160bdf0012e0c335 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 7 Oct 2026 19:00:28 -0400 Subject: [PATCH 12/23] BUG: Fix ComputeKMedoids mask tuple count check When masking is enabled, preflight now rejects a Mask Array whose number of tuples differs from the Attribute Array to Cluster (-7589). Previously the mismatch reached execute, where both the Direct and Scanline eligibility pre-scans failed with a DataStore range error. Co-Authored-By: Claude Opus 5.5 --- .../SimplnxCore/docs/ComputeKMedoidsFilter.md | 2 + .../Filters/ComputeKMedoidsFilter.cpp | 13 ++++++ .../SimplnxCore/test/ComputeKMedoidsTest.cpp | 42 +++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/src/Plugins/SimplnxCore/docs/ComputeKMedoidsFilter.md b/src/Plugins/SimplnxCore/docs/ComputeKMedoidsFilter.md index 8ad32d93b4..49c3b89abb 100644 --- a/src/Plugins/SimplnxCore/docs/ComputeKMedoidsFilter.md +++ b/src/Plugins/SimplnxCore/docs/ComputeKMedoidsFilter.md @@ -66,6 +66,8 @@ The filter creates a *cluster Ids* array (one dimensionless integer cluster labe **Mask Array** must have 1 component. +When masking is enabled, **Mask Array** must have the same number of tuples as **Attribute Array to Cluster**. + ## Algorithm This filter has two algorithm implementations that are automatically selected at runtime based on how the input data is stored. The user does not need to choose between them. diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMedoidsFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMedoidsFilter.cpp index aa543dd0b2..49713ec73d 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMedoidsFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMedoidsFilter.cpp @@ -16,6 +16,7 @@ #include "simplnx/Parameters/NumberParameter.hpp" #include "simplnx/Parameters/StringParameter.hpp" #include "simplnx/Utilities/ClusteringUtilities.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" @@ -131,6 +132,18 @@ IFilter::PreflightResult ComputeKMedoidsFilter::preflightImpl(const DataStructur return MakePreflightErrorResult(-7586, "The medoids output dimensions overflow the supported address range."); } + if(filterArgs.value(k_UseMask_Key)) + { + const auto maskPath = filterArgs.value(k_MaskArrayPath_Key); + if(!CheckArraysHaveSameTupleCount(dataStructure, {pSelectedArrayPathValue, maskPath})) + { + const auto& maskArray = dataStructure.getDataRefAs(maskPath); + return MakePreflightErrorResult( + -7589, fmt::format("**Attribute Array to Cluster** DataArray '{}' has {} tuples, but **Mask Array** DataArray '{}' has {} tuples. Select DataArrays with the same number of tuples.", + pSelectedArrayPathValue.toString(), clusterArray->getNumberOfTuples(), maskPath.toString(), maskArray.getNumberOfTuples())); + } + } + { auto createAction = std::make_unique(DataType::int32, clusterArray->getTupleShape(), std::vector{1}, pSelectedArrayPathValue.replaceName(pFeatureIdsArrayNameValue), "", "0"); diff --git a/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp b/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp index ac3c0e5be6..dcbb677084 100644 --- a/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp @@ -504,3 +504,45 @@ TEST_CASE("SimplnxCore::ComputeKMedoidsFilter: Mask Array requires 1 component", REQUIRE(std::any_of(preflightResult.outputActions.errors().begin(), preflightResult.outputActions.errors().end(), [](const Error& error) { return error.code == -208; })); UnitTest::CheckArraysInheritTupleDims(dataStructure); } + +TEST_CASE("SimplnxCore::ComputeKMedoidsFilter: Mask Array requires matching tuple counts", "[SimplnxCore][ComputeKMedoidsFilter][ComponentShape]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure; + auto* cellData = AttributeMatrix::Create(dataStructure, "Cell Data", {6}); + auto* maskData = AttributeMatrix::Create(dataStructure, "Mask Data", {4}); + REQUIRE(cellData != nullptr); + REQUIRE(maskData != nullptr); + REQUIRE(UnitTest::CreateTestDataArray(dataStructure, "Input", {6}, {1}, cellData->getId()) != nullptr); + REQUIRE(BoolArray::CreateWithStore(dataStructure, "Mask", ShapeType{4}, ShapeType{1}, maskData->getId()) != nullptr); + const DataPath inputPath({"Cell Data", "Input"}); + const DataPath maskPath({"Mask Data", "Mask"}); + + ComputeKMedoidsFilter filter; + auto args = filter.getDefaultArguments(); + args.insertOrAssign(ComputeKMedoidsFilter::k_InitClusters_Key, std::make_any(1)); + args.insertOrAssign(ComputeKMedoidsFilter::k_MaskArrayPath_Key, std::make_any(maskPath)); + args.insertOrAssign(ComputeKMedoidsFilter::k_SelectedArrayPath_Key, std::make_any(inputPath)); + args.insertOrAssign(ComputeKMedoidsFilter::k_FeatureAMPath_Key, std::make_any(DataPath({"Cluster Data"}))); + + SECTION("Enabled mask rejects fewer tuples") + { + args.insertOrAssign(ComputeKMedoidsFilter::k_UseMask_Key, std::make_any(true)); + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + const auto& errors = preflightResult.outputActions.errors(); + const auto error = std::find_if(errors.begin(), errors.end(), [](const Error& value) { return value.code == -7589; }); + REQUIRE(error != errors.end()); + CHECK(error->message.find(inputPath.toString()) != std::string::npos); + CHECK(error->message.find(maskPath.toString()) != std::string::npos); + CHECK(error->message.find("6 tuples") != std::string::npos); + CHECK(error->message.find("4 tuples") != std::string::npos); + } + SECTION("Disabled mask ignores fewer tuples") + { + args.insertOrAssign(ComputeKMedoidsFilter::k_UseMask_Key, std::make_any(false)); + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + } + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} From 55aca951417210eccb2dde59c4f8a256f29103ac Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 19 Oct 2026 14:21:38 -0400 Subject: [PATCH 13/23] BUG: DBSCAN preflight no longer throws for a StringArray mask The mask parameter accepts any IArray, and a StringArray passes it. When its tuple count differs from Attribute Array to Cluster, the tuple-count error message cast the mask to IDataArray, which threw std::bad_cast out of preflight. The message now reads the tuple count through IArray, so preflight returns error -7587. Co-Authored-By: Claude Opus 5.5 --- .../src/SimplnxCore/Filters/DBSCANFilter.cpp | 2 +- src/Plugins/SimplnxCore/test/DBSCANTest.cpp | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/DBSCANFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/DBSCANFilter.cpp index 71fd7d0682..9f4f015de9 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/DBSCANFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/DBSCANFilter.cpp @@ -156,7 +156,7 @@ IFilter::PreflightResult DBSCANFilter::preflightImpl(const DataStructure& dataSt const auto maskPath = filterArgs.value(k_MaskArrayPath_Key); if(!CheckArraysHaveSameTupleCount(dataStructure, {pSelectedArrayPathValue, maskPath})) { - const auto& maskArray = dataStructure.getDataRefAs(maskPath); + const auto& maskArray = dataStructure.getDataRefAs(maskPath); return MakePreflightErrorResult( -7587, fmt::format("**Attribute Array to Cluster** DataArray '{}' has {} tuples, but **Cell Mask Array** DataArray '{}' has {} tuples. Select DataArrays with the same number of tuples.", pSelectedArrayPathValue.toString(), clusterArray.getNumberOfTuples(), maskPath.toString(), maskArray.getNumberOfTuples())); diff --git a/src/Plugins/SimplnxCore/test/DBSCANTest.cpp b/src/Plugins/SimplnxCore/test/DBSCANTest.cpp index acf36e7a06..53a6046d7d 100644 --- a/src/Plugins/SimplnxCore/test/DBSCANTest.cpp +++ b/src/Plugins/SimplnxCore/test/DBSCANTest.cpp @@ -6,6 +6,7 @@ #include "simplnx/DataStructure/DataArray.hpp" #include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/DataStructure/StringArray.hpp" #include "simplnx/Parameters/ChoicesParameter.hpp" #include "simplnx/Pipeline/Pipeline.hpp" #include "simplnx/Pipeline/PipelineFilter.hpp" @@ -729,6 +730,29 @@ TEST_CASE("SimplnxCore::DBSCAN: Reject short mask in preflight", "[SimplnxCore][ REQUIRE_THAT(message, Catch::Matchers::Contains("DataArray 'Mask' has 3 tuples")); } +TEST_CASE("SimplnxCore::DBSCAN: Reject short StringArray mask in preflight", "[SimplnxCore][DBSCAN][ComponentShape]") +{ + DataStructure dataStructure; + auto* points = Float32Array::CreateWithStore>(dataStructure, "Points", ShapeType{4}, ShapeType{2}); + REQUIRE(points != nullptr); + auto* mask = StringArray::CreateWithValues(dataStructure, "Mask", ShapeType{3}, {"1", "1", "1"}); + REQUIRE(mask != nullptr); + + DBSCANFilter filter; + Arguments args = filter.getDefaultArguments(); + args.insertOrAssign(DBSCANFilter::k_SelectedArrayPath_Key, std::make_any(DataPath({"Points"}))); + args.insertOrAssign(DBSCANFilter::k_MaskArrayPath_Key, std::make_any(DataPath({"Mask"}))); + args.insertOrAssign(DBSCANFilter::k_FeatureAMPath_Key, std::make_any(DataPath({"Features"}))); + args.insertOrAssign(DBSCANFilter::k_UseMask_Key, std::make_any(true)); + auto preflightResult = filter.preflight(dataStructure, args); + REQUIRE(preflightResult.outputActions.invalid()); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + REQUIRE(preflightResult.outputActions.errors()[0].code == -7587); + const auto& message = preflightResult.outputActions.errors()[0].message; + REQUIRE_THAT(message, Catch::Matchers::Contains("DataArray 'Points' has 4 tuples")); + REQUIRE_THAT(message, Catch::Matchers::Contains("DataArray 'Mask' has 3 tuples")); +} + TEST_CASE("SimplnxCore::DBSCANFilter: SIMPL Backwards Compatibility", "[SimplnxCore][DBSCANFilter][BackwardsCompatibility]") { auto app = Application::GetOrCreateInstance(); From c24975c4f32358cc3e9dff8c3827b3ff9c0d50e6 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 19 Oct 2026 14:21:38 -0400 Subject: [PATCH 14/23] BUG: ComputeKMeans preflight no longer throws for a StringArray mask The mask parameter accepts any IArray, and a StringArray passes it. When its tuple count differs from Attribute Array to Cluster, the tuple-count error message cast the mask to IDataArray, which threw std::bad_cast out of preflight. The message now reads the tuple count through IArray, so preflight returns error -7588. Co-Authored-By: Claude Opus 5.5 --- .../Filters/ComputeKMeansFilter.cpp | 2 +- .../SimplnxCore/test/ComputeKMeansTest.cpp | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMeansFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMeansFilter.cpp index f688b998f5..9ddf6465b9 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMeansFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMeansFilter.cpp @@ -132,7 +132,7 @@ IFilter::PreflightResult ComputeKMeansFilter::preflightImpl(const DataStructure& const auto maskPath = filterArgs.value(k_MaskArrayPath_Key); if(!CheckArraysHaveSameTupleCount(dataStructure, {pSelectedArrayPathValue, maskPath})) { - const auto& maskArray = dataStructure.getDataRefAs(maskPath); + const auto& maskArray = dataStructure.getDataRefAs(maskPath); return MakePreflightErrorResult( -7588, fmt::format("**Attribute Array to Cluster** DataArray '{}' has {} tuples, but **Cell Mask Array** DataArray '{}' has {} tuples. Select DataArrays with the same number of tuples.", pSelectedArrayPathValue.toString(), clusterArray->getNumberOfTuples(), maskPath.toString(), maskArray.getNumberOfTuples())); diff --git a/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp b/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp index 1ac58562fe..5cb93c4a8e 100644 --- a/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp @@ -6,6 +6,7 @@ #include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataArray.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/DataStructure/StringArray.hpp" #include "simplnx/Parameters/ChoicesParameter.hpp" #include "simplnx/Pipeline/Pipeline.hpp" #include "simplnx/Pipeline/PipelineFilter.hpp" @@ -677,3 +678,36 @@ TEST_CASE("SimplnxCore::ComputeKMeans: Cell Mask Array requires matching tuple c } UnitTest::CheckArraysInheritTupleDims(dataStructure); } + +TEST_CASE("SimplnxCore::ComputeKMeans: Reject short StringArray mask in preflight", "[SimplnxCore][ComputeKMeans][ComponentShape]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure; + auto* cellData = AttributeMatrix::Create(dataStructure, "Cell Data", {6}); + auto* maskData = AttributeMatrix::Create(dataStructure, "Mask Data", {4}); + REQUIRE(cellData != nullptr); + REQUIRE(maskData != nullptr); + REQUIRE(UnitTest::CreateTestDataArray(dataStructure, "Input", {6}, {1}, cellData->getId()) != nullptr); + REQUIRE(StringArray::CreateWithValues(dataStructure, "Mask", ShapeType{4}, {"1", "1", "1", "1"}, maskData->getId()) != nullptr); + const DataPath inputPath({"Cell Data", "Input"}); + const DataPath maskPath({"Mask Data", "Mask"}); + + ComputeKMeansFilter filter; + auto args = filter.getDefaultArguments(); + args.insertOrAssign(ComputeKMeansFilter::k_InitClusters_Key, std::make_any(1)); + args.insertOrAssign(ComputeKMeansFilter::k_MaskArrayPath_Key, std::make_any(maskPath)); + args.insertOrAssign(ComputeKMeansFilter::k_SelectedArrayPath_Key, std::make_any(inputPath)); + args.insertOrAssign(ComputeKMeansFilter::k_FeatureAMPath_Key, std::make_any(DataPath({"Cluster Data"}))); + + args.insertOrAssign(ComputeKMeansFilter::k_UseMask_Key, std::make_any(true)); + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + const auto& errors = preflightResult.outputActions.errors(); + const auto error = std::find_if(errors.begin(), errors.end(), [](const Error& value) { return value.code == -7588; }); + REQUIRE(error != errors.end()); + CHECK(error->message.find(inputPath.toString()) != std::string::npos); + CHECK(error->message.find(maskPath.toString()) != std::string::npos); + CHECK(error->message.find("6 tuples") != std::string::npos); + CHECK(error->message.find("4 tuples") != std::string::npos); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} From 6c1dfda5df77aeb6321034a55d4d132ae0674c37 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 19 Oct 2026 14:21:38 -0400 Subject: [PATCH 15/23] BUG: ComputeKMedoids preflight no longer throws for a StringArray mask The mask parameter accepts any IArray, and a StringArray passes it. When its tuple count differs from Attribute Array to Cluster, the tuple-count error message cast the mask to IDataArray, which threw std::bad_cast out of preflight. The message now reads the tuple count through IArray, so preflight returns error -7589. Co-Authored-By: Claude Opus 5.5 --- .../Filters/ComputeKMedoidsFilter.cpp | 2 +- .../SimplnxCore/test/ComputeKMedoidsTest.cpp | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMedoidsFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMedoidsFilter.cpp index 49713ec73d..a471396c1e 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMedoidsFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMedoidsFilter.cpp @@ -137,7 +137,7 @@ IFilter::PreflightResult ComputeKMedoidsFilter::preflightImpl(const DataStructur const auto maskPath = filterArgs.value(k_MaskArrayPath_Key); if(!CheckArraysHaveSameTupleCount(dataStructure, {pSelectedArrayPathValue, maskPath})) { - const auto& maskArray = dataStructure.getDataRefAs(maskPath); + const auto& maskArray = dataStructure.getDataRefAs(maskPath); return MakePreflightErrorResult( -7589, fmt::format("**Attribute Array to Cluster** DataArray '{}' has {} tuples, but **Mask Array** DataArray '{}' has {} tuples. Select DataArrays with the same number of tuples.", pSelectedArrayPathValue.toString(), clusterArray->getNumberOfTuples(), maskPath.toString(), maskArray.getNumberOfTuples())); diff --git a/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp b/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp index dcbb677084..baf8c5b12b 100644 --- a/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp @@ -9,6 +9,7 @@ #include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataArray.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/DataStructure/StringArray.hpp" #include "simplnx/Parameters/ChoicesParameter.hpp" #include "simplnx/Pipeline/Pipeline.hpp" #include "simplnx/Pipeline/PipelineFilter.hpp" @@ -546,3 +547,36 @@ TEST_CASE("SimplnxCore::ComputeKMedoidsFilter: Mask Array requires matching tupl } UnitTest::CheckArraysInheritTupleDims(dataStructure); } + +TEST_CASE("SimplnxCore::ComputeKMedoidsFilter: Reject short StringArray mask in preflight", "[SimplnxCore][ComputeKMedoidsFilter][ComponentShape]") +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure; + auto* cellData = AttributeMatrix::Create(dataStructure, "Cell Data", {6}); + auto* maskData = AttributeMatrix::Create(dataStructure, "Mask Data", {4}); + REQUIRE(cellData != nullptr); + REQUIRE(maskData != nullptr); + REQUIRE(UnitTest::CreateTestDataArray(dataStructure, "Input", {6}, {1}, cellData->getId()) != nullptr); + REQUIRE(StringArray::CreateWithValues(dataStructure, "Mask", ShapeType{4}, {"1", "1", "1", "1"}, maskData->getId()) != nullptr); + const DataPath inputPath({"Cell Data", "Input"}); + const DataPath maskPath({"Mask Data", "Mask"}); + + ComputeKMedoidsFilter filter; + auto args = filter.getDefaultArguments(); + args.insertOrAssign(ComputeKMedoidsFilter::k_InitClusters_Key, std::make_any(1)); + args.insertOrAssign(ComputeKMedoidsFilter::k_MaskArrayPath_Key, std::make_any(maskPath)); + args.insertOrAssign(ComputeKMedoidsFilter::k_SelectedArrayPath_Key, std::make_any(inputPath)); + args.insertOrAssign(ComputeKMedoidsFilter::k_FeatureAMPath_Key, std::make_any(DataPath({"Cluster Data"}))); + + args.insertOrAssign(ComputeKMedoidsFilter::k_UseMask_Key, std::make_any(true)); + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + const auto& errors = preflightResult.outputActions.errors(); + const auto error = std::find_if(errors.begin(), errors.end(), [](const Error& value) { return value.code == -7589; }); + REQUIRE(error != errors.end()); + CHECK(error->message.find(inputPath.toString()) != std::string::npos); + CHECK(error->message.find(maskPath.toString()) != std::string::npos); + CHECK(error->message.find("6 tuples") != std::string::npos); + CHECK(error->message.find("4 tuples") != std::string::npos); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} From 2b080f1383527bf7fa678fc8b331f53575915cff Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Mon, 19 Oct 2026 14:21:38 -0400 Subject: [PATCH 16/23] DOC: Correct DBSCAN mask requirement, test names and legacy version The filter doc now states the mask tuple-count rule and that both rules apply only when Use Mask Array is enabled. The deviation doc uses the registered test names. The provenance note refers to DREAM3D 6.5.171. Co-Authored-By: Claude Opus 5.5 --- src/Plugins/SimplnxCore/docs/DBSCANFilter.md | 2 +- src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md | 2 +- src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Plugins/SimplnxCore/docs/DBSCANFilter.md b/src/Plugins/SimplnxCore/docs/DBSCANFilter.md index b3f3c7baa9..20f6e0ccda 100644 --- a/src/Plugins/SimplnxCore/docs/DBSCANFilter.md +++ b/src/Plugins/SimplnxCore/docs/DBSCANFilter.md @@ -188,7 +188,7 @@ The *Distance Metric* parameter provides the following choices: - **Pearson [4]**: Uses the Pearson correlation distance, measuring linear correlation dissimilarity. - **Squared Pearson [5]**: Uses the squared Pearson correlation distance. -**Cell Mask Array** must have 1 component. +When **Use Mask Array** is enabled, **Cell Mask Array** must have 1 component and the same number of tuples as **Attribute Array to Cluster**. ## Note on Randomness diff --git a/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md b/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md index fc17024be9..fe0c0a01cd 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md @@ -135,4 +135,4 @@ The `FromSIMPLJson` converter (`DBSCANFilter.cpp`, starting at line 242) is not **Affected users:** Users whose pipelines select a multi-component mask or a mask with a different number of tuples from the clustering DataArray. Previously accepted Direct executions may have used unintended mask elements or accessed beyond a short mask. -**Recommendation:** Select a boolean or uint8 mask DataArray with component shape `{1}` and one tuple per tuple in **Attribute Array to Cluster**. The preflight tuple-count error identifies both DataArrays and their tuple counts. Regression coverage: `SimplnxCore::DBSCANFilter: Reject multi-component mask in preflight` and `SimplnxCore::DBSCANFilter: Reject short mask in preflight` in `DBSCANTest.cpp`. +**Recommendation:** Select a boolean or uint8 mask DataArray with component shape `{1}` and one tuple per tuple in **Attribute Array to Cluster**. The preflight tuple-count error identifies both DataArrays and their tuple counts. Regression coverage: `SimplnxCore::DBSCAN: Reject multi-component mask in preflight` and `SimplnxCore::DBSCAN: Reject short mask in preflight` in `DBSCANTest.cpp`. diff --git a/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md b/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md index 48ff8b8ce4..338ace6d0a 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md +++ b/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md @@ -98,12 +98,12 @@ The circular-oracle situation identified in Phase 2 was resolved in Phase 6 reco This archive has **not** been regenerated. Regeneration is not needed because the independent oracle confirmed the existing arrays are correct. -## Legacy comparison corroboration (Phase 9, 2026-08-05) +## Legacy DREAM3D 6.5.171 DBSCAN comparison corroboration (Phase 9, 2026-08-05) Script: `dbscan_vv/phase9_ab_test.py` Results: `dbscan_vv/phase9_comparison_results.json` -DREAM3D 6.5.172 (traditional DBSCAN, via `DREAM3DReview` plugin UUID `c2d4f1e8`) was run against the same 6 sklearn datasets. Three-way comparison (legacy 6.5.172 vs. sklearn 1.7.1 vs. SIMPLNX): +Legacy DREAM3D 6.5.171 DBSCAN (via `DREAM3DReview` plugin UUID `c2d4f1e8`) was run against the same 6 sklearn datasets. Three-way comparison (legacy DREAM3D 6.5.171 DBSCAN vs. sklearn 1.7.1 vs. SIMPLNX): | Dataset | Legacy n_clusters | sklearn n_clusters | SIMPLNX n_clusters | Match | |---|---|---|---|---| From 9e642bb820df45430ecb5a8d86b88316c3a745c1 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 20 Oct 2026 18:17:51 -0400 Subject: [PATCH 17/23] DOC: Move DBSCAN V&V record changes to the V&V cleanup branch * Restore the DBSCAN V&V report, deviations, and provenance files to their develop versions. * This PR now changes no V&V record. The DBSCAN V&V update moves to topic/vv_report_cleanup, which merges after this PR. Signed-off-by: Michael Jackson --- .../docs/ComputeGBCDPoleFigureFilter.md | 2 +- src/Plugins/SimplnxCore/vv/DBSCANFilter.md | 17 +++++-------- .../SimplnxCore/vv/deviations/DBSCANFilter.md | 25 +++---------------- .../SimplnxCore/vv/provenance/dbscan_test.md | 4 +-- 4 files changed, 12 insertions(+), 36 deletions(-) diff --git a/src/Plugins/OrientationAnalysis/docs/ComputeGBCDPoleFigureFilter.md b/src/Plugins/OrientationAnalysis/docs/ComputeGBCDPoleFigureFilter.md index dc6d0607ae..a3c8293b2e 100644 --- a/src/Plugins/OrientationAnalysis/docs/ComputeGBCDPoleFigureFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/ComputeGBCDPoleFigureFilter.md @@ -14,7 +14,7 @@ The GBCD is a 5-dimensional histogram that captures the statistical distribution ![Using ParaView's Threshold filter + Cells to Points + Delaunay2D Filters](Images/Small_IN100_GBCD_Delaunay2D.png) -The GBCD DataArray must have the 6-dimension component shape that Compute GBCD creates (5 positive bin dimensions followed by a final dimension of 2 (hemispheres)). +The GBCD DataArray must have the 6-dimension component shape that Compute GBCD creates (5 positive bin dimensions followed by a final dimension of 2 hemispheres). ## Algorithm diff --git a/src/Plugins/SimplnxCore/vv/DBSCANFilter.md b/src/Plugins/SimplnxCore/vv/DBSCANFilter.md index bd08424828..89dc8ed1e8 100644 --- a/src/Plugins/SimplnxCore/vv/DBSCANFilter.md +++ b/src/Plugins/SimplnxCore/vv/DBSCANFilter.md @@ -16,8 +16,8 @@ |------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | Algorithm Relationship | **Rewrite** — SIMPLNX implements GDCF (Grid-based DBSCAN, Boonchoo et al. 2019, DOI 10.1016/j.patcog.2019.01.034) in place of the traditional point-by-point DBSCAN in legacy DREAM3D. UUID changed from `c2d4f1e8` to `763dad44` (legacy UUID retained via SIMPL mapper). | | Oracle (confirmed) | **Class 2 (Reference — scikit-learn 1.7.1 DBSCAN) primary + Class 4 (Invariant) companion.** Input data independently generated from deterministic sklearn scripts in `dbscan_vv/dbscan_data_proj/`. Phase 6 reconciliation complete: 4/6 datasets exact match; 2 deviations (ansio, varied) fully explained by DBSCAN-D1 (GDCF vs. traditional DBSCAN). See Phase 5 + Phase 6. | -| Code paths enumerated | 20 of 23 enumerated paths exercised. The untested invalid-component, unvisited-border, and empty-forest paths remain identified below. | -| Tests today | 14 registered tests in this file plus 1 hidden HDF5 batch-tail test. The OOC build also registers seven external-storage tests. Existing analytical and regression assertions are preserved. | +| Code paths enumerated | 18 of 21 enumerated paths exercised. The untested invalid-component, unvisited-border, and empty-forest paths remain identified below. | +| Tests today | 12 registered tests in this file plus 1 hidden HDF5 batch-tail test. The OOC build also registers seven external-storage tests. Existing analytical and regression assertions are preserved. | | Exemplar archive | **`dbscan_test.tar.gz` — promoted to regression fixtures (Phase 6/10).** Originally circular oracle; independently verified via Class 2 sklearn oracle (Phase 6). LDF arrays now pin verified-correct SIMPLNX output. See provenance sidecar. | | Legacy comparison | Historical comparison used a local legacy proof build and the sklearn oracle. It supports the documented GDCF-versus-traditional DBSCAN differences. This OOC recertification does not establish a new stock DREAM3D 6.5.171 binary comparison. | | Bug flags | ✅ Circular oracle resolved (Phase 6) — `dbscan_test.tar.gz` LDF arrays promoted to regression fixtures; Class 2 sklearn oracle confirms correctness. 2 expected GDCF deviations (ansio, varied) documented as DBSCAN-D1. **1 SIMPLNX bug found and fixed during V&V:** `ParseOrder::Random` fed the user-supplied seed to the shuffle instead of the documented time-based seed, making "Random" a silent duplicate of "Seeded Random" (see Phase 7). **1 robustness defect found and fixed:** an all-false mask left the grid bounds NaN and those NaNs were cast to `usize` while computing grid dimensions (undefined behavior). | @@ -84,7 +84,7 @@ The prior Random-seed and all-false-mask fixes are described in Phase 7. They ha ## Code path coverage -20 of 23 enumerated paths exercised. +18 of 21 enumerated paths exercised. Source: `Algorithms/DBSCAN.cpp` (34 lines), `DBSCANDirect.cpp` (1,274 lines), and `DBSCANScanline.cpp` (3,655 lines). Scanline selects ExternalGDCF when a routed array is on disk; forced Scanline on resident arrays uses its resident fallback. @@ -114,8 +114,6 @@ Logical phases: **(a) Grid construction** — build HyperGridBitMap and bin poin | 18 | External I/O | A 65,536-point input batch and a two-point partial tail | `genuine HDF5 65536-batch tail oracle` | | 19 | External clustering | Adjacent core grids merge across the input-batch boundary | `genuine HDF5 65536-batch tail oracle` — four active points form one cluster and every other label is zero | | 20 | Preflight | No-mask mode avoids a cell-sized temporary mask | `No-mask preflight does not create a cell-sized temporary mask` | -| 21 | Preflight | Enabled mask has component shape other than `{1}` → error `-208` | `Reject multi-component mask in preflight` | -| 22 | Preflight | Enabled mask and input array have unequal tuple counts → error `-7587` | `Reject short mask in preflight` | ## Test inventory @@ -133,8 +131,6 @@ Logical phases: **(a) Grid construction** — build HyperGridBitMap and bin poin | `SimplnxCore::DBSCAN: Analytical Fixture F1 - No Clusters Warning` | new-for-V&V | Class 1 oracle. 4 corner points, ε=0.1, minPts=5. Covers code path #5 (warning -85640). Self-contained inline data. | | `SimplnxCore::DBSCAN: Analytical Fixture F2 - Mask Exclusion` | new-for-V&V | Class 1 oracle. 3 points, P2 masked. Covers code path #4 (mask=true). Self-contained inline data. | | `SimplnxCore::DBSCAN: Analytical Fixture F3 - All Points Masked` | new-for-V&V | Class 1 oracle. Same 3 points as F2 with every point masked off. Covers code path #4b and pins the all-false-mask contract (warning `-85640`, all IDs 0, AM 1 tuple) that the NaN-bounds guard makes architecture-independent. Self-contained inline data. | -| `SimplnxCore::DBSCAN: Reject multi-component mask in preflight` | new-for-CS-13 | Four coordinate tuples and four mask tuples with component shape `{2}`; preflight rejects with `-208` when the mask is enabled and accepts when disabled. See DBSCAN-D6. | -| `SimplnxCore::DBSCAN: Reject short mask in preflight` | new-for-CS-13 | Four coordinate tuples and three scalar mask tuples; preflight rejects with `-7587` when the mask is enabled and accepts when disabled. See DBSCAN-D6. | | Class 4 invariant assertions | new-for-V&V | `CheckClusterInvariants()` hooked into `LDFTestCase2D` and `RandomTestCase2D` — covers all 18 2D test runs — and into F1/F2/F3. 3D test has inline AM tuple check but not the full helper. | | `SimplnxCore::DBSCANFilter: No-mask preflight does not create a cell-sized temporary mask` | kept | Checks action allocation in no-mask mode. | | `SimplnxCore::DBSCAN: genuine HDF5 65536-batch tail oracle` | new-for-V&V | 65,538 points, of which five are active. Four points form two adjacent core grids spanning the batch boundary; the fifth is distant noise. All labels and the feature count are checked. | @@ -158,7 +154,6 @@ OOC recertification, 2026-09-18: serial CTest passed 12/12 in `NX-Com-Qt69-Vtk96 - `DBSCAN-D3` — `use_precaching` parameter removed; old pipelines silently ignore it — see `vv/deviations/DBSCANFilter.md` - `DBSCAN-D4` — Parameter key renamed `init_type_index` → `parse_order_index`; `upgradeParametersImpl` not implemented — early SIMPLNX pipelines silently default to `LowDensityFirst` — see `vv/deviations/DBSCANFilter.md` - `DBSCAN-D5` — `SeededRandom` shuffle replaced with `std::shuffle`; same seed now produces a different (correct, unbiased) permutation compared to pre-PR builds — see `vv/deviations/DBSCANFilter.md` -- `DBSCAN-D6` — Multi-component masks and unequal mask/input tuple counts now fail preflight before Direct or Scanline execution — see `vv/deviations/DBSCANFilter.md` --- @@ -420,7 +415,7 @@ Code paths newly covered: path #4 (mask=true), path #5 (no core grids warning). **Status: Complete (2026-08-05).** -The `dbscan_test.tar.gz` LDF exemplar arrays have been promoted from "circular oracle" to "regression fixtures" — the Class 2 sklearn oracle (Phase 6) independently confirmed SIMPLNX correctness for 4/6 datasets, and the historical local legacy proof build comparison (Phase 9) corroborated the DBSCAN-D1 classification for the remaining 2 datasets. No archive regeneration is required; no new exemplar archive was created. +The `dbscan_test.tar.gz` LDF exemplar arrays have been promoted from "circular oracle" to "regression fixtures" — the Class 2 sklearn oracle (Phase 6) independently confirmed SIMPLNX correctness for 4/6 datasets, and the legacy 6.5.172 comparison (Phase 9) corroborated the DBSCAN-D1 classification for the remaining 2 datasets. No archive regeneration is required; no new exemplar archive was created. Provenance sidecar updated at `src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md` with both Phase 6 and Phase 9 entries. @@ -453,11 +448,11 @@ All V&V working artifacts are stored in the `dbscan_vv/` working folder outside | `oracle_results.json` | Sklearn oracle results — cluster counts + sizes per dataset (Phase 5) | | `compare_exemplar_vs_oracle.py` | Phase 6 three-way comparison: exemplar vs. sklearn (Phase 6) | | `phase6_comparison_results.json` | Phase 6 comparison results — SIMPLNX vs. sklearn (Phase 6) | -| `phase9_ab_test.py` | A/B test script: creates 6.5 HDF5 input, runs the historical local legacy proof build, three-way comparison (Phase 9) | +| `phase9_ab_test.py` | A/B test script: creates 6.5 HDF5 input, runs legacy 6.5.172, three-way comparison (Phase 9) | | `6_5_input.dream3d` | HDF5 input file in DREAM3D 6.5 format used by legacy PipelineRunner (Phase 9) | | `dbscan_6_5_pipeline.json` | DREAM3D 6.5 pipeline JSON: DataContainerReader + 6×DBSCAN + DataContainerWriter (Phase 9) | | `6_5_output.dream3d` | Output from the local legacy proof build PipelineRunner containing legacy cluster IDs (Phase 9) | -| `phase9_comparison_results.json` | Phase 9 three-way comparison results (historical local legacy proof build vs. sklearn vs. SIMPLNX) (Phase 9) | +| `phase9_comparison_results.json` | Phase 9 three-way comparison results (legacy 6.5.172 vs. sklearn vs. SIMPLNX) (Phase 9) | No SBIR submission packaging required at this stage. Artifacts are on-disk in the development environment; all scripts are self-contained and reproducible. diff --git a/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md b/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md index fe0c0a01cd..d5de6b5eb2 100644 --- a/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md +++ b/src/Plugins/SimplnxCore/vv/deviations/DBSCANFilter.md @@ -1,6 +1,6 @@ -# Deviations from DREAM3D 6.5.171: DBSCANFilter +# Deviations from DREAM3D 6.5.172: DBSCANFilter -This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (D1–D4), plus within-SIMPLNX changes introduced by this PR that break reproducibility for existing users (D5–D6). +This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.172 equivalent (D1–D4), plus within-SIMPLNX changes introduced by this PR that break reproducibility for existing users (D5). Entries are referenced by stable ID (`DBSCAN-D`) from the V&V report and from public migration guidance. The Filter UUID fields are the permanent cross-reference anchors. @@ -19,7 +19,7 @@ Entries are referenced by stable ID (`DBSCAN-D`) from the V&V report and from **Symptom:** Sparse datasets where individual data points each have ≥ minPoints neighbors within ε (and would form clusters under traditional DBSCAN) may produce **more noise (cluster 0) points in SIMPLNX** if those neighborhoods span multiple grid cells with fewer than minPoints points per cell. -**Confirmed (Phase 9, 2026-08-05):** The historical local legacy proof build run on the 6 sklearn toy datasets confirms: legacy finds 6 clusters (ansio) and 11 clusters (varied) — matching sklearn — while SIMPLNX finds only 3 for both. The large-cluster sizes agree for varied ([87,166,169] — exact three-way match); for ansio the large clusters are close but not identical (sklearn oracle: [152,155,157]; SIMPLNX: [153,156,161] — small boundary discrepancy also attributable to GDCF grid-cell vs. point-level core definition). Only the micro-clusters (≤6 points each) are absent in SIMPLNX. Evidence: `dbscan_vv/phase9_comparison_results.json`. +**Confirmed (Phase 9, 2026-08-05):** DREAM3D 6.5.172 run on the 6 sklearn toy datasets confirms: legacy finds 6 clusters (ansio) and 11 clusters (varied) — matching sklearn — while SIMPLNX finds only 3 for both. The large-cluster sizes agree for varied ([87,166,169] — exact three-way match); for ansio the large clusters are close but not identical (sklearn oracle: [152,155,157]; SIMPLNX: [153,156,161] — small boundary discrepancy also attributable to GDCF grid-cell vs. point-level core definition). Only the micro-clusters (≤6 points each) are absent in SIMPLNX. Evidence: `dbscan_vv/phase9_comparison_results.json`. **Root cause:** Algorithmic choice. SIMPLNX implements Grid-based DBSCAN (GDCF, Boonchoo et al. 2019). The core-object definition differs fundamentally: - **Legacy (traditional DBSCAN)**: A data point `p` is a *core point* if the ε-ball centered on `p` contains ≥ minPoints data points (inclusive of `p` itself). Cluster membership is point-centric. @@ -117,22 +117,3 @@ The `FromSIMPLJson` converter (`DBSCANFilter.cpp`, starting at line 242) is not **Affected users:** Users who relied on a specific `SeededRandom` seed to reproduce a particular cluster-ID assignment from a pre-PR build. The same seed now produces a correct unbiased shuffle instead of the previously biased one, so the resulting cluster-ID numbers will differ. Users running `LowDensityFirst` (the default) are entirely unaffected. Users running `Random` (time-based seed, non-deterministic by design) are unaffected in practice. **Recommendation:** If reproducibility of specific cluster-ID values across builds is required, re-run with the same seed on the current build and use the new output as the reference. Cluster membership (grouping) is preserved for densely-clustered datasets; only the numeric label assigned to each cluster may change. - ---- - -## DBSCAN-D6 - -| Field | Value | -|---|---| -| **Deviation ID** | `DBSCAN-D6` | -| **SIMPLNX UUID** | `763dad44-fad7-4606-808f-617867257b98` | -| **Legacy SIMPL UUID** | `c2d4f1e8-2b04-5d82-b90f-2191e8f4262e` | -| **Status** | active | - -**Symptom:** With **Use Mask Array** enabled, SIMPLNX previously accepted a multi-component or short **Cell Mask Array** in preflight. The Direct path read the mask by flat element, while the external Scanline path rejected it during execution with `-54062`. Both paths now reject these inputs before execution: component shape other than `{1}` produces `-208`, and unequal tuple counts produce `-7587`. - -**Root cause:** The mask selection parameter did not restrict component shape, and preflight did not compare the tuple counts of **Cell Mask Array** and **Attribute Array to Cluster**. Validation now applies before algorithm dispatch. This is a within-SIMPLNX correction; no new comparison with DREAM3D 6.5.171 is claimed. - -**Affected users:** Users whose pipelines select a multi-component mask or a mask with a different number of tuples from the clustering DataArray. Previously accepted Direct executions may have used unintended mask elements or accessed beyond a short mask. - -**Recommendation:** Select a boolean or uint8 mask DataArray with component shape `{1}` and one tuple per tuple in **Attribute Array to Cluster**. The preflight tuple-count error identifies both DataArrays and their tuple counts. Regression coverage: `SimplnxCore::DBSCAN: Reject multi-component mask in preflight` and `SimplnxCore::DBSCAN: Reject short mask in preflight` in `DBSCANTest.cpp`. diff --git a/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md b/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md index 338ace6d0a..48ff8b8ce4 100644 --- a/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md +++ b/src/Plugins/SimplnxCore/vv/provenance/dbscan_test.md @@ -98,12 +98,12 @@ The circular-oracle situation identified in Phase 2 was resolved in Phase 6 reco This archive has **not** been regenerated. Regeneration is not needed because the independent oracle confirmed the existing arrays are correct. -## Legacy DREAM3D 6.5.171 DBSCAN comparison corroboration (Phase 9, 2026-08-05) +## Legacy comparison corroboration (Phase 9, 2026-08-05) Script: `dbscan_vv/phase9_ab_test.py` Results: `dbscan_vv/phase9_comparison_results.json` -Legacy DREAM3D 6.5.171 DBSCAN (via `DREAM3DReview` plugin UUID `c2d4f1e8`) was run against the same 6 sklearn datasets. Three-way comparison (legacy DREAM3D 6.5.171 DBSCAN vs. sklearn 1.7.1 vs. SIMPLNX): +DREAM3D 6.5.172 (traditional DBSCAN, via `DREAM3DReview` plugin UUID `c2d4f1e8`) was run against the same 6 sklearn datasets. Three-way comparison (legacy 6.5.172 vs. sklearn 1.7.1 vs. SIMPLNX): | Dataset | Legacy n_clusters | sklearn n_clusters | SIMPLNX n_clusters | Match | |---|---|---|---|---| From bb2f40c510307e0d2ee925785f2295662266b702 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Fri, 23 Oct 2026 13:07:08 -0400 Subject: [PATCH 18/23] DOC: Remove the nested parentheses from the Write GBCD GMT File shape sentence --- src/Plugins/OrientationAnalysis/docs/WriteGBCDGMTFileFilter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugins/OrientationAnalysis/docs/WriteGBCDGMTFileFilter.md b/src/Plugins/OrientationAnalysis/docs/WriteGBCDGMTFileFilter.md index 4a0f7221b0..55b3150860 100644 --- a/src/Plugins/OrientationAnalysis/docs/WriteGBCDGMTFileFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/WriteGBCDGMTFileFilter.md @@ -37,7 +37,7 @@ It samples the GBCD for the selected phase and misorientation and writes the res % Auto generated parameter table will be inserted here -The GBCD DataArray must have the 6-dimension component shape that Compute GBCD creates (5 positive bin dimensions followed by a final dimension of 2 (hemispheres)). +The GBCD DataArray must have the 6-dimension component shape that Compute GBCD creates (5 positive bin dimensions followed by a final dimension of 2 hemispheres). ## Example Pipelines From c4d8afa7ef6f9ef33495f9d86c14522d546f977c Mon Sep 17 00:00:00 2001 From: Joey Kleingers Date: Mon, 26 Oct 2026 11:30:33 -0400 Subject: [PATCH 19/23] BUG: Keep the DBSCAN Scanline input checks for direct calls * DBSCANScanline can be called without DBSCANFilter. Its out-of-core path again checks the coordinate component count (-54060) and the mask component count and tuple count (-54062) before it reads them. Without these checks, a 4-component input writes past a 3-element array and a 2-component mask is read as one value per tuple. * The check that compares the component count with the selected dimensionality is not restored, because it cannot fail. * The error messages now give the actual counts. * Add a test that calls DBSCANScanline directly on its out-of-core path. A scoped replacement of the HDF5 I/O manager supplies the external-sort and record-store providers, and restores the original manager when the test ends, also when an assertion fails. Signed-off-by: Joey Kleingers --- .../Filters/Algorithms/DBSCANScanline.cpp | 17 +- src/Plugins/SimplnxCore/test/DBSCANTest.cpp | 239 ++++++++++++++++++ 2 files changed, 255 insertions(+), 1 deletion(-) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/DBSCANScanline.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/DBSCANScanline.cpp index 94205c9b20..720ea5664b 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/DBSCANScanline.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/DBSCANScanline.cpp @@ -311,7 +311,12 @@ class ExternalGDCF /** * @brief Validates inputs, discovers grid origin, externally sorts enabled points, and builds grid/core/axis records. - * @pre Filter preflight has validated positive Epsilon and MinPoints values. + * + * The coordinate component count and the mask shape are checked here even + * though filter preflight also enforces them, because DBSCANScanline can be + * called without its filter. Grid coordinates hold at most three axes, and + * the mask is read as one value per coordinate tuple. + * @pre Epsilon and MinPoints are positive. * @return Success, a no-cluster warning, or the first provider, validation, or record-I/O failure. */ Result<> initialize() @@ -321,7 +326,17 @@ class ExternalGDCF { return MakeErrorResult(-54061, "DBSCAN out-of-core execution requires registered external-sort and temporary-record-store providers."); } + if(m_Dimensions != 2 && m_Dimensions != 3) + { + return MakeErrorResult(-54060, fmt::format("DBSCAN requires 2 or 3 coordinate components, but the clustering array has a component count of {}.", m_Dimensions)); + } const usize tupleCount = m_InputStore.getNumberOfTuples(); + if(m_MaskStore != nullptr && (m_MaskStore->getNumberOfTuples() != tupleCount || m_MaskStore->getNumberOfComponents() != 1)) + { + return MakeErrorResult(-54062, + fmt::format("The DBSCAN mask must have 1 component and the same number of tuples as the clustering array ({} tuples), but it has {} tuples and a component count of {}.", + tupleCount, m_MaskStore->getNumberOfTuples(), m_MaskStore->getNumberOfComponents())); + } if(m_InputValues.MinPoints < 0) { return MakeErrorResult(-54063, "DBSCAN Minimum Points cannot be negative."); diff --git a/src/Plugins/SimplnxCore/test/DBSCANTest.cpp b/src/Plugins/SimplnxCore/test/DBSCANTest.cpp index 53a6046d7d..e6e9af2f32 100644 --- a/src/Plugins/SimplnxCore/test/DBSCANTest.cpp +++ b/src/Plugins/SimplnxCore/test/DBSCANTest.cpp @@ -6,6 +6,11 @@ #include "simplnx/DataStructure/DataArray.hpp" #include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/DataStructure/IO/Generic/DataIOCollection.hpp" +#include "simplnx/DataStructure/IO/Generic/IDataIOManager.hpp" +#include "simplnx/DataStructure/IO/Generic/IExternalSort.hpp" +#include "simplnx/DataStructure/IO/Generic/ITemporaryRecordStore.hpp" +#include "simplnx/DataStructure/IO/HDF5/DataIOManager.hpp" #include "simplnx/DataStructure/StringArray.hpp" #include "simplnx/Parameters/ChoicesParameter.hpp" #include "simplnx/Pipeline/Pipeline.hpp" @@ -15,10 +20,17 @@ #include "simplnx/Utilities/DataStoreUtilities.hpp" #include "SimplnxCore/Filters/Algorithms/DBSCAN.hpp" +#include "SimplnxCore/Filters/Algorithms/DBSCANScanline.hpp" #include "SimplnxCore/Filters/DBSCANFilter.hpp" #include +#include #include +#include +#include +#include +#include +#include namespace fs = std::filesystem; using namespace nx::core; @@ -247,6 +259,169 @@ void RandomTestCase2D(const DataPath& targetPath, float32 epsilonVal, int32 minP UnitTest::CheckArraysInheritTupleDims(dataStructure); } + +/** + * @brief Keeps its values in memory but reports out-of-core residency. + * + * DBSCANScanline selects its external implementation from the reported store + * type, so this store reaches that implementation in a build without an + * out-of-core storage plugin. + * @tparam T Element type. + */ +template +class ReportedOutOfCoreDataStore : public DataStore +{ +public: + using DataStore::DataStore; + + IDataStore::StoreType getStoreType() const override + { + return IDataStore::StoreType::OutOfCore; + } +}; + +/** + * @brief The built-in HDF5 manager, plus external-sort and temporary-record-store providers that fail when used. + * + * DBSCAN's external implementation requires both capabilities before it validates + * input shapes. Its shape checks run before it creates a sorter or record store, + * so k_ProviderReachedCode shows that execution passed every shape check. + * + * The manager keeps the "HDF5" format name and factories so it can stand in for + * the built-in manager (see ScopedIOManagerReplacement). DataIOCollection asks + * its managers in name order, and "HDF5" sorts before the out-of-core manager + * "HDF5-OOC", so these providers answer in both in-core and out-of-core builds. + */ +class DBSCANShapeGuardHDF5Manager : public HDF5::DataIOManager +{ +public: + static constexpr int32 k_ProviderReachedCode = -54199; + + bool supportsExternalSort() const override + { + return true; + } + + Result> createExternalSort(const ExternalSortConfig&) const override + { + return MakeErrorResult>(k_ProviderReachedCode, "DBSCAN shape-guard test: execution reached external-sort creation."); + } + + bool supportsTemporaryRecordStore() const override + { + return true; + } + + Result> createTemporaryRecordStore(const TemporaryRecordStoreConfig&) const override + { + return MakeErrorResult>(k_ProviderReachedCode, "DBSCAN shape-guard test: execution reached temporary record-store creation."); + } +}; + +/** + * @brief Replaces one registered I/O manager for the lifetime of this object. + * + * DataIOCollection cannot remove a manager, so the replacement takes the format + * name of an existing manager and the destructor registers the saved original + * again. The destructor also runs when a failed assertion unwinds the test, so + * afterwards the collection holds exactly the managers it held before. + */ +class ScopedIOManagerReplacement +{ +public: + /** + * @brief Registers replacement in place of the manager with the same format name. + * @param collection Collection to change; it must outlive this object. + * @param replacement Manager to register. Nothing is registered when no manager has its format name. + */ + ScopedIOManagerReplacement(DataIOCollection& collection, std::shared_ptr replacement) + : m_Collection(collection) + , m_Original(collection.getManager(replacement->formatName())) + { + // Registering under a new name would add a manager that nothing can remove. + if(m_Original != nullptr) + { + m_Replaced = m_Collection.addIOManager(std::move(replacement)).valid(); + } + } + + ~ScopedIOManagerReplacement() noexcept + { + if(m_Replaced) + { + // A destructor cannot report failure; re-registering an accepted name succeeds. + (void)m_Collection.addIOManager(m_Original); + } + } + + ScopedIOManagerReplacement(const ScopedIOManagerReplacement&) = delete; + ScopedIOManagerReplacement(ScopedIOManagerReplacement&&) noexcept = delete; + ScopedIOManagerReplacement& operator=(const ScopedIOManagerReplacement&) = delete; + ScopedIOManagerReplacement& operator=(ScopedIOManagerReplacement&&) noexcept = delete; + + /** + * @brief Reports whether the replacement is registered. + * @return True when the original manager was found and replaced. + */ + bool replaced() const noexcept + { + return m_Replaced; + } + +private: + DataIOCollection& m_Collection; + std::shared_ptr m_Original; + bool m_Replaced = false; +}; + +/** + * @brief Calls DBSCANScanline directly, without the filter or its preflight, on out-of-core coordinates. + * @param pointComponents Coordinate component count. + * @param maskShape Mask tuple and component counts, or std::nullopt to run without a mask. + * @return The algorithm result. + */ +Result<> RunDirectOutOfCoreScanline(usize pointComponents, std::optional> maskShape) +{ + constexpr usize k_PointTuples = 4; + const DataPath pointsPath({"Points"}); + const DataPath maskPath({"Mask"}); + const DataPath idsPath({"Ids"}); + + DataStructure dataStructure; + auto pointStore = std::make_shared>(ShapeType{k_PointTuples}, ShapeType{pointComponents}, 0.0F); + for(usize index = 0; index < pointStore->getSize(); ++index) + { + pointStore->setValue(index, static_cast(index) * 0.25F); + } + auto* points = Float32Array::Create(dataStructure, pointsPath.getTargetName(), pointStore); + REQUIRE(points != nullptr); + // The external implementation runs only for out-of-core arrays. + REQUIRE(IsOutOfCore(*points)); + auto* ids = Int32Array::CreateWithStore>(dataStructure, idsPath.getTargetName(), ShapeType{k_PointTuples}, ShapeType{1}); + REQUIRE(ids != nullptr); + if(maskShape.has_value()) + { + auto* mask = UInt8Array::CreateWithStore>(dataStructure, maskPath.getTargetName(), ShapeType{maskShape->first}, ShapeType{maskShape->second}); + REQUIRE(mask != nullptr); + mask->fill(1); + } + + DBSCANInputValues inputValues; + inputValues.ClusteringArrayPath = pointsPath; + inputValues.MaskArrayPath = maskPath; + inputValues.UseMask = maskShape.has_value(); + inputValues.FeatureIdsArrayPath = idsPath; + inputValues.Epsilon = 1.0F; + inputValues.MinPoints = 2; + inputValues.DistanceMetric = ClusterUtilities::DistanceMetric::Euclidean; + inputValues.FeatureAM = DataPath({"Features"}); + inputValues.ParseOrder = to_underlying(DBSCAN::ParseOrder::LowDensityFirst); + inputValues.Seed = 5489; + + const std::atomic_bool shouldCancel = false; + DBSCANScanline algorithm(dataStructure, IFilter::MessageHandler{}, shouldCancel, &inputValues); + return algorithm(); +} } // namespace TEST_CASE("SimplnxCore::DBSCAN: 2D Test: Aniso", "[SimplnxCore][DBSCAN]") @@ -753,6 +928,70 @@ TEST_CASE("SimplnxCore::DBSCAN: Reject short StringArray mask in preflight", "[S REQUIRE_THAT(message, Catch::Matchers::Contains("DataArray 'Mask' has 3 tuples")); } +TEST_CASE("SimplnxCore::DBSCANScanline: Direct out-of-core call rejects invalid input shapes", "[SimplnxCore][DBSCAN][ComponentShape]") +{ + DataIOCollection& collection = DataStoreUtilities::GetIOCollection(); + const std::shared_ptr builtInHDF5Manager = collection.getManager("HDF5"); + REQUIRE(builtInHDF5Manager != nullptr); + const bool hadExternalSort = collection.hasExternalSortCapability(); + const bool hadTemporaryRecordStore = collection.hasTemporaryRecordStoreCapability(); + + { + const ScopedIOManagerReplacement providers(collection, std::make_shared()); + REQUIRE(providers.replaced()); + + SECTION("Valid shapes pass the shape checks") + { + const Result<> result = RunDirectOutOfCoreScanline(2, std::make_pair(usize{4}, usize{1})); + REQUIRE(result.invalid()); + REQUIRE(result.errors().size() == 1); + REQUIRE(result.errors()[0].code == DBSCANShapeGuardHDF5Manager::k_ProviderReachedCode); + } + SECTION("Four coordinate components") + { + const Result<> result = RunDirectOutOfCoreScanline(4, std::nullopt); + REQUIRE(result.invalid()); + REQUIRE(result.errors().size() == 1); + REQUIRE(result.errors()[0].code == -54060); + REQUIRE_THAT(result.errors()[0].message, Catch::Matchers::Contains("has a component count of 4")); + } + SECTION("Two-component mask") + { + const Result<> result = RunDirectOutOfCoreScanline(2, std::make_pair(usize{4}, usize{2})); + REQUIRE(result.invalid()); + REQUIRE(result.errors().size() == 1); + REQUIRE(result.errors()[0].code == -54062); + REQUIRE_THAT(result.errors()[0].message, Catch::Matchers::Contains("it has 4 tuples and a component count of 2")); + } + SECTION("Short mask") + { + const Result<> result = RunDirectOutOfCoreScanline(2, std::make_pair(usize{3}, usize{1})); + REQUIRE(result.invalid()); + REQUIRE(result.errors().size() == 1); + REQUIRE(result.errors()[0].code == -54062); + REQUIRE_THAT(result.errors()[0].message, Catch::Matchers::Contains("it has 3 tuples and a component count of 1")); + } + } + + SECTION("An exception inside the replacement scope restores the built-in manager") + { + // A failed REQUIRE also leaves the scope by throwing. + const auto failInsideScope = [&collection]() { + const ScopedIOManagerReplacement providers(collection, std::make_shared()); + if(providers.replaced()) + { + throw std::runtime_error("Simulated assertion failure inside the replacement scope"); + } + }; + REQUIRE_THROWS_AS(failInsideScope(), std::runtime_error); + } + + // Later tests in the same process must see the collection as it was before this test. + REQUIRE(collection.getManager("HDF5") == builtInHDF5Manager); + REQUIRE(collection.hasExternalSortCapability() == hadExternalSort); + REQUIRE(collection.hasTemporaryRecordStoreCapability() == hadTemporaryRecordStore); +} + TEST_CASE("SimplnxCore::DBSCANFilter: SIMPL Backwards Compatibility", "[SimplnxCore][DBSCANFilter][BackwardsCompatibility]") { auto app = Application::GetOrCreateInstance(); From 05a7bd1295a9eeae09f21d83f6abae319034246b Mon Sep 17 00:00:00 2001 From: Joey Kleingers Date: Mon, 26 Oct 2026 11:30:33 -0400 Subject: [PATCH 20/23] STY: Remove the duplicate ImageGeom include from the ComputeShapes test Signed-off-by: Joey Kleingers --- src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp index cad567b0b0..85d0cf8db8 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp @@ -1,6 +1,5 @@ #include "OrientationAnalysis/Filters/ComputeShapesFilter.hpp" #include "OrientationAnalysis/OrientationAnalysis_test_dirs.hpp" -#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/AttributeMatrix.hpp" From 7ecc51610164bd0cda6b0bd58c8cdcfbc6a1671b Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 27 Oct 2026 15:58:53 -0400 Subject: [PATCH 21/23] REV: Load plugins in the new component shape tests and state the mask rule condition Add UnitTest::LoadPlugins() as the first statement of the six new component shape TEST_CASEs in the DBSCAN, Compute GBCD Pole Figure and Write GBCD GMT File tests. State in the K Means and K Medoids docs that the mask component and tuple rules apply only when Use Mask Array is enabled. --- .../OrientationAnalysis/test/ComputeGBCDPoleFigureTest.cpp | 1 + src/Plugins/OrientationAnalysis/test/WriteGBCDGMTFileTest.cpp | 1 + src/Plugins/SimplnxCore/docs/ComputeKMeansFilter.md | 4 +--- src/Plugins/SimplnxCore/docs/ComputeKMedoidsFilter.md | 4 +--- src/Plugins/SimplnxCore/test/DBSCANTest.cpp | 4 ++++ 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Plugins/OrientationAnalysis/test/ComputeGBCDPoleFigureTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeGBCDPoleFigureTest.cpp index fd3a031d6c..655ff04ae4 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeGBCDPoleFigureTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeGBCDPoleFigureTest.cpp @@ -273,6 +273,7 @@ TEST_CASE("OrientationAnalysis::ComputeGBCDPoleFigureFilter: Phase and Laue Inde TEST_CASE("OrientationAnalysis::ComputeGBCDPoleFigureFilter: Invalid GBCD Component Shape", "[OrientationAnalysis][ComputeGBCDPoleFigureFilter][ComponentShape]") { + UnitTest::LoadPlugins(); const auto componentShape = GENERATE(ShapeType{1}, ShapeType{2, 2, 2, 2, 2, 1}, ShapeType{0, 2, 2, 2, 2, 2}, ShapeType{2, 0, 2, 2, 2, 2}, ShapeType{2, 2, 0, 2, 2, 2}, ShapeType{2, 2, 2, 0, 2, 2}, ShapeType{2, 2, 2, 2, 0, 2}); CAPTURE(componentShape); diff --git a/src/Plugins/OrientationAnalysis/test/WriteGBCDGMTFileTest.cpp b/src/Plugins/OrientationAnalysis/test/WriteGBCDGMTFileTest.cpp index b1e1f3182e..a190ab7db0 100644 --- a/src/Plugins/OrientationAnalysis/test/WriteGBCDGMTFileTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/WriteGBCDGMTFileTest.cpp @@ -482,6 +482,7 @@ TEST_CASE("OrientationAnalysis::WriteGBCDGMTFileFilter: SIMPL Backwards Compatib TEST_CASE("OrientationAnalysis::WriteGBCDGMTFileFilter: Invalid GBCD Component Shape", "[OrientationAnalysis][WriteGBCDGMTFile][ComponentShape]") { + UnitTest::LoadPlugins(); const auto componentShape = GENERATE(ShapeType{1}, ShapeType{2, 2, 2, 2, 2, 1}, ShapeType{0, 2, 2, 2, 2, 2}, ShapeType{2, 0, 2, 2, 2, 2}, ShapeType{2, 2, 0, 2, 2, 2}, ShapeType{2, 2, 2, 0, 2, 2}, ShapeType{2, 2, 2, 2, 0, 2}); CAPTURE(componentShape); diff --git a/src/Plugins/SimplnxCore/docs/ComputeKMeansFilter.md b/src/Plugins/SimplnxCore/docs/ComputeKMeansFilter.md index c366278de1..956a217c4f 100644 --- a/src/Plugins/SimplnxCore/docs/ComputeKMeansFilter.md +++ b/src/Plugins/SimplnxCore/docs/ComputeKMeansFilter.md @@ -12,9 +12,7 @@ This **Filter** applies the k means algorithm to an **Attribute Array**. K mean Each cluster is summarized by its **centroid**, which is the arithmetic mean (the average position) of all the points assigned to that cluster. A k means partitioning is a *Voronoi tessellation*: every point is assigned to the cluster whose centroid is closest to it, so the data space is carved into regions where each region contains all the points nearer to one centroid than to any other. An optimal solution to the k means problem is one in which each point is associated with the cluster that has the closest mean. This partitioning minimizes the **within-cluster variance**, that is, the sum over all clusters of the squared distances from each point to its cluster centroid; minimizing it makes the points within each cluster as tightly grouped as possible. The user may select from several distance metrics: *Euclidean*, *Squared Euclidean*, *Manhattan*, *Cosine*, *Pearson*, and *Squared Pearson*. -**Cell Mask Array** must have 1 component. - -When masking is enabled, **Cell Mask Array** must have the same number of tuples as **Attribute Array to Cluster**. +When **Use Mask Array** is enabled, **Cell Mask Array** must have 1 component and the same number of tuples as **Attribute Array to Cluster**. ## Algorithm diff --git a/src/Plugins/SimplnxCore/docs/ComputeKMedoidsFilter.md b/src/Plugins/SimplnxCore/docs/ComputeKMedoidsFilter.md index 49c3b89abb..427e3fa198 100644 --- a/src/Plugins/SimplnxCore/docs/ComputeKMedoidsFilter.md +++ b/src/Plugins/SimplnxCore/docs/ComputeKMedoidsFilter.md @@ -64,9 +64,7 @@ The filter creates a *cluster Ids* array (one dimensionless integer cluster labe - **Clustered Attribute Array** -- any **Attribute Array** (cell-level, feature-level, or generic) whose tuples are to be partitioned into clusters. This is typically an output of an earlier computation or import step. -**Mask Array** must have 1 component. - -When masking is enabled, **Mask Array** must have the same number of tuples as **Attribute Array to Cluster**. +When **Use Mask Array** is enabled, **Mask Array** must have 1 component and the same number of tuples as **Attribute Array to Cluster**. ## Algorithm diff --git a/src/Plugins/SimplnxCore/test/DBSCANTest.cpp b/src/Plugins/SimplnxCore/test/DBSCANTest.cpp index e6e9af2f32..7852da78a6 100644 --- a/src/Plugins/SimplnxCore/test/DBSCANTest.cpp +++ b/src/Plugins/SimplnxCore/test/DBSCANTest.cpp @@ -856,6 +856,7 @@ TEST_CASE("SimplnxCore::DBSCANFilter: No-mask preflight does not create a cell-s TEST_CASE("SimplnxCore::DBSCAN: Reject multi-component mask in preflight", "[SimplnxCore][DBSCAN][ComponentShape]") { + UnitTest::LoadPlugins(); DataStructure dataStructure; auto* points = Float32Array::CreateWithStore>(dataStructure, "Points", ShapeType{4}, ShapeType{2}); REQUIRE(points != nullptr); @@ -880,6 +881,7 @@ TEST_CASE("SimplnxCore::DBSCAN: Reject multi-component mask in preflight", "[Sim TEST_CASE("SimplnxCore::DBSCAN: Reject short mask in preflight", "[SimplnxCore][DBSCAN][ComponentShape]") { + UnitTest::LoadPlugins(); DataStructure dataStructure; auto* points = Float32Array::CreateWithStore>(dataStructure, "Points", ShapeType{4}, ShapeType{2}); REQUIRE(points != nullptr); @@ -907,6 +909,7 @@ TEST_CASE("SimplnxCore::DBSCAN: Reject short mask in preflight", "[SimplnxCore][ TEST_CASE("SimplnxCore::DBSCAN: Reject short StringArray mask in preflight", "[SimplnxCore][DBSCAN][ComponentShape]") { + UnitTest::LoadPlugins(); DataStructure dataStructure; auto* points = Float32Array::CreateWithStore>(dataStructure, "Points", ShapeType{4}, ShapeType{2}); REQUIRE(points != nullptr); @@ -930,6 +933,7 @@ TEST_CASE("SimplnxCore::DBSCAN: Reject short StringArray mask in preflight", "[S TEST_CASE("SimplnxCore::DBSCANScanline: Direct out-of-core call rejects invalid input shapes", "[SimplnxCore][DBSCAN][ComponentShape]") { + UnitTest::LoadPlugins(); DataIOCollection& collection = DataStoreUtilities::GetIOCollection(); const std::shared_ptr builtInHDF5Manager = collection.getManager("HDF5"); REQUIRE(builtInHDF5Manager != nullptr); From af201c93cf964b666b986d6f0ebd3c63dae4b709 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Tue, 27 Oct 2026 16:10:46 -0400 Subject: [PATCH 22/23] REV: Load plugins in the older DBSCAN test cases Add UnitTest::LoadPlugins() as the first statement of the ten DBSCAN TEST_CASEs that predate this branch, so every TEST_CASE in the file starts with it. --- src/Plugins/SimplnxCore/test/DBSCANTest.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Plugins/SimplnxCore/test/DBSCANTest.cpp b/src/Plugins/SimplnxCore/test/DBSCANTest.cpp index 7852da78a6..7e894e1ee2 100644 --- a/src/Plugins/SimplnxCore/test/DBSCANTest.cpp +++ b/src/Plugins/SimplnxCore/test/DBSCANTest.cpp @@ -426,6 +426,7 @@ Result<> RunDirectOutOfCoreScanline(usize pointComponents, std::optional Date: Tue, 27 Oct 2026 16:40:35 -0400 Subject: [PATCH 23/23] REV: Drop unneeded plugin loading and tidy two review nits - Remove UnitTest::LoadPlugins() from the TEST_CASEs this branch adds and from the ten older DBSCAN TEST_CASEs. Each one constructs its filter or algorithm directly and never looks a filter up by UUID, so it needs no plugins. The SIMPL backwards-compatibility tests, which load pipelines, keep the call. - Give the resident DBSCAN Scanline path the same -54060 message as the out-of-core path, with the actual component count. - Move the TriangleGeom and VertexGeom includes into the simplnx include group. --- .../test/ComputeGBCDPoleFigureTest.cpp | 1 - .../test/ComputeShapesFilterTest.cpp | 1 - .../test/ComputeShapesTriangleGeomTest.cpp | 3 +-- .../test/WriteGBCDGMTFileTest.cpp | 1 - .../Filters/Algorithms/DBSCANScanline.cpp | 2 +- .../test/ComputeFeaturePhasesBinaryTest.cpp | 1 - src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp | 3 --- .../SimplnxCore/test/ComputeKMedoidsTest.cpp | 3 --- .../SimplnxCore/test/ComputeNumFeaturesTest.cpp | 4 ---- .../test/ComputeVertexToTriangleDistancesTest.cpp | 3 +-- src/Plugins/SimplnxCore/test/DBSCANTest.cpp | 14 -------------- 11 files changed, 3 insertions(+), 33 deletions(-) diff --git a/src/Plugins/OrientationAnalysis/test/ComputeGBCDPoleFigureTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeGBCDPoleFigureTest.cpp index 655ff04ae4..fd3a031d6c 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeGBCDPoleFigureTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeGBCDPoleFigureTest.cpp @@ -273,7 +273,6 @@ TEST_CASE("OrientationAnalysis::ComputeGBCDPoleFigureFilter: Phase and Laue Inde TEST_CASE("OrientationAnalysis::ComputeGBCDPoleFigureFilter: Invalid GBCD Component Shape", "[OrientationAnalysis][ComputeGBCDPoleFigureFilter][ComponentShape]") { - UnitTest::LoadPlugins(); const auto componentShape = GENERATE(ShapeType{1}, ShapeType{2, 2, 2, 2, 2, 1}, ShapeType{0, 2, 2, 2, 2, 2}, ShapeType{2, 0, 2, 2, 2, 2}, ShapeType{2, 2, 0, 2, 2, 2}, ShapeType{2, 2, 2, 0, 2, 2}, ShapeType{2, 2, 2, 2, 0, 2}); CAPTURE(componentShape); diff --git a/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp index 85d0cf8db8..73c5cf32ab 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp @@ -190,7 +190,6 @@ TEST_CASE("OrientationAnalysis::ComputeShapesFilter: Geometry cell tuple count", TEST_CASE("OrientationAnalysis::ComputeShapesFilter: Cell Feature Ids requires 1 component", "[OrientationAnalysis][ComputeShapesFilter][ComponentShape]") { - UnitTest::LoadPlugins(); DataStructure dataStructure; auto* imageGeom = ImageGeom::Create(dataStructure, "Image"); REQUIRE(imageGeom != nullptr); diff --git a/src/Plugins/OrientationAnalysis/test/ComputeShapesTriangleGeomTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeShapesTriangleGeomTest.cpp index d755cdfeba..d04d8c89e2 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeShapesTriangleGeomTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeShapesTriangleGeomTest.cpp @@ -1,7 +1,7 @@ #include "OrientationAnalysis/Filters/ComputeShapesTriangleGeomFilter.hpp" #include "OrientationAnalysis/OrientationAnalysis_test_dirs.hpp" -#include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" +#include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" #include "simplnx/Parameters/ArrayCreationParameter.hpp" #include "simplnx/Parameters/DataObjectNameParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" @@ -99,7 +99,6 @@ TEST_CASE("OrientationAnalysis::ComputeShapesTriangleGeom", "[OrientationAnalysi TEST_CASE("OrientationAnalysis::ComputeShapesTriangleGeom: Face Feature Centroids requires 3 components", "[OrientationAnalysis][ComputeShapesTriangleGeom][ComponentShape]") { - UnitTest::LoadPlugins(); DataStructure dataStructure; auto* triangleGeom = TriangleGeom::Create(dataStructure, "Triangles"); REQUIRE(triangleGeom != nullptr); diff --git a/src/Plugins/OrientationAnalysis/test/WriteGBCDGMTFileTest.cpp b/src/Plugins/OrientationAnalysis/test/WriteGBCDGMTFileTest.cpp index a190ab7db0..b1e1f3182e 100644 --- a/src/Plugins/OrientationAnalysis/test/WriteGBCDGMTFileTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/WriteGBCDGMTFileTest.cpp @@ -482,7 +482,6 @@ TEST_CASE("OrientationAnalysis::WriteGBCDGMTFileFilter: SIMPL Backwards Compatib TEST_CASE("OrientationAnalysis::WriteGBCDGMTFileFilter: Invalid GBCD Component Shape", "[OrientationAnalysis][WriteGBCDGMTFile][ComponentShape]") { - UnitTest::LoadPlugins(); const auto componentShape = GENERATE(ShapeType{1}, ShapeType{2, 2, 2, 2, 2, 1}, ShapeType{0, 2, 2, 2, 2, 2}, ShapeType{2, 0, 2, 2, 2, 2}, ShapeType{2, 2, 0, 2, 2, 2}, ShapeType{2, 2, 2, 0, 2, 2}, ShapeType{2, 2, 2, 2, 0, 2}); CAPTURE(componentShape); diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/DBSCANScanline.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/DBSCANScanline.cpp index 720ea5664b..4d269e3109 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/DBSCANScanline.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/DBSCANScanline.cpp @@ -3556,7 +3556,7 @@ struct DBSCANScanlineFunctor } else { - return MakeErrorResult(-54060, "Input components invalid. Only 2 or 3 accepted."); + return MakeErrorResult(-54060, fmt::format("DBSCAN requires 2 or 3 coordinate components, but the clustering array has a component count of {}.", inputArray.getNumberOfComponents())); } } }; diff --git a/src/Plugins/SimplnxCore/test/ComputeFeaturePhasesBinaryTest.cpp b/src/Plugins/SimplnxCore/test/ComputeFeaturePhasesBinaryTest.cpp index 22a2124ade..db7f9ef0e9 100644 --- a/src/Plugins/SimplnxCore/test/ComputeFeaturePhasesBinaryTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeFeaturePhasesBinaryTest.cpp @@ -104,7 +104,6 @@ TEST_CASE("SimplnxCore::ComputeFeaturePhasesBinaryFilter: SIMPL Backwards Compat TEST_CASE("SimplnxCore::ComputeFeaturePhasesBinaryFilter: Cell Feature Ids requires 1 component", "[SimplnxCore][ComputeFeaturePhasesBinaryFilter][ComponentShape]") { - UnitTest::LoadPlugins(); DataStructure dataStructure; auto* cellData = AttributeMatrix::Create(dataStructure, "Cell Data", {2}); REQUIRE(cellData != nullptr); diff --git a/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp b/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp index 5cb93c4a8e..4974efe501 100644 --- a/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp @@ -610,7 +610,6 @@ TEST_CASE("SimplnxCore::ComputeKMeansFilter: SIMPL Backwards Compatibility", "[S TEST_CASE("SimplnxCore::ComputeKMeans: Cell Mask Array requires 1 component", "[SimplnxCore][ComputeKMeans][ComponentShape]") { - UnitTest::LoadPlugins(); DataStructure dataStructure; auto* cellData = AttributeMatrix::Create(dataStructure, "Cell Data", {2}); REQUIRE(cellData != nullptr); @@ -639,7 +638,6 @@ TEST_CASE("SimplnxCore::ComputeKMeans: Cell Mask Array requires 1 component", "[ TEST_CASE("SimplnxCore::ComputeKMeans: Cell Mask Array requires matching tuple counts", "[SimplnxCore][ComputeKMeans][ComponentShape]") { - UnitTest::LoadPlugins(); DataStructure dataStructure; auto* cellData = AttributeMatrix::Create(dataStructure, "Cell Data", {6}); auto* maskData = AttributeMatrix::Create(dataStructure, "Mask Data", {4}); @@ -681,7 +679,6 @@ TEST_CASE("SimplnxCore::ComputeKMeans: Cell Mask Array requires matching tuple c TEST_CASE("SimplnxCore::ComputeKMeans: Reject short StringArray mask in preflight", "[SimplnxCore][ComputeKMeans][ComponentShape]") { - UnitTest::LoadPlugins(); DataStructure dataStructure; auto* cellData = AttributeMatrix::Create(dataStructure, "Cell Data", {6}); auto* maskData = AttributeMatrix::Create(dataStructure, "Mask Data", {4}); diff --git a/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp b/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp index baf8c5b12b..3ba62accde 100644 --- a/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp @@ -479,7 +479,6 @@ TEST_CASE("SimplnxCore::ComputeKMedoidsFilter: SIMPL Backwards Compatibility", " TEST_CASE("SimplnxCore::ComputeKMedoidsFilter: Mask Array requires 1 component", "[SimplnxCore][ComputeKMedoidsFilter][ComponentShape]") { - UnitTest::LoadPlugins(); DataStructure dataStructure; auto* cellData = AttributeMatrix::Create(dataStructure, "Cell Data", {2}); REQUIRE(cellData != nullptr); @@ -508,7 +507,6 @@ TEST_CASE("SimplnxCore::ComputeKMedoidsFilter: Mask Array requires 1 component", TEST_CASE("SimplnxCore::ComputeKMedoidsFilter: Mask Array requires matching tuple counts", "[SimplnxCore][ComputeKMedoidsFilter][ComponentShape]") { - UnitTest::LoadPlugins(); DataStructure dataStructure; auto* cellData = AttributeMatrix::Create(dataStructure, "Cell Data", {6}); auto* maskData = AttributeMatrix::Create(dataStructure, "Mask Data", {4}); @@ -550,7 +548,6 @@ TEST_CASE("SimplnxCore::ComputeKMedoidsFilter: Mask Array requires matching tupl TEST_CASE("SimplnxCore::ComputeKMedoidsFilter: Reject short StringArray mask in preflight", "[SimplnxCore][ComputeKMedoidsFilter][ComponentShape]") { - UnitTest::LoadPlugins(); DataStructure dataStructure; auto* cellData = AttributeMatrix::Create(dataStructure, "Cell Data", {6}); auto* maskData = AttributeMatrix::Create(dataStructure, "Mask Data", {4}); diff --git a/src/Plugins/SimplnxCore/test/ComputeNumFeaturesTest.cpp b/src/Plugins/SimplnxCore/test/ComputeNumFeaturesTest.cpp index 8e40611e41..453bea7777 100644 --- a/src/Plugins/SimplnxCore/test/ComputeNumFeaturesTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeNumFeaturesTest.cpp @@ -50,7 +50,6 @@ Arguments CreatePhaseFixture(DataStructure& dataStructure, const std::vector& void RequireInvalidPhase(const std::vector& values, const std::string& expectedValue, const std::string& expectedTuple) { - UnitTest::LoadPlugins(); DataStructure dataStructure; const auto args = CreatePhaseFixture(dataStructure, values); ComputeNumFeaturesFilter filter; @@ -79,7 +78,6 @@ void RequireInvalidPhase(const std::vector& values, const std::string& ex TEST_CASE("SimplnxCore::ComputeNumFeaturesFilter: Feature Phases requires 1 component", "[SimplnxCore][ComputeNumFeaturesFilter][ComponentShape]") { - UnitTest::LoadPlugins(); DataStructure dataStructure; const auto args = CreatePhaseFixture(dataStructure, {0, 0, 1, 1}, {2}); ComputeNumFeaturesFilter filter; @@ -91,7 +89,6 @@ TEST_CASE("SimplnxCore::ComputeNumFeaturesFilter: Feature Phases requires 1 comp TEST_CASE("SimplnxCore::ComputeNumFeaturesFilter: Feature Phases requires int32", "[SimplnxCore][ComputeNumFeaturesFilter][ComponentShape]") { - UnitTest::LoadPlugins(); DataStructure dataStructure; const auto args = CreatePhaseFixture(dataStructure, {0.0F, 1.0F}); ComputeNumFeaturesFilter filter; @@ -113,7 +110,6 @@ TEST_CASE("SimplnxCore::ComputeNumFeaturesFilter: Rejects negative phase", "[Sim TEST_CASE("SimplnxCore::ComputeNumFeaturesFilter: Counts valid phases and skips feature zero", "[SimplnxCore][ComputeNumFeaturesFilter][ComponentShape]") { - UnitTest::LoadPlugins(); DataStructure dataStructure; const auto args = CreatePhaseFixture(dataStructure, {0, 1, 2, 1, 0, 2, 1}); ComputeNumFeaturesFilter filter; diff --git a/src/Plugins/SimplnxCore/test/ComputeVertexToTriangleDistancesTest.cpp b/src/Plugins/SimplnxCore/test/ComputeVertexToTriangleDistancesTest.cpp index 35608405e3..b3abfc1614 100644 --- a/src/Plugins/SimplnxCore/test/ComputeVertexToTriangleDistancesTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeVertexToTriangleDistancesTest.cpp @@ -1,9 +1,9 @@ #include "SimplnxCore/Filters/ComputeVertexToTriangleDistancesFilter.hpp" #include "SimplnxCore/SimplnxCore_test_dirs.hpp" -#include "simplnx/DataStructure/Geometry/VertexGeom.hpp" #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" +#include "simplnx/DataStructure/Geometry/VertexGeom.hpp" #include "simplnx/Parameters/ArrayCreationParameter.hpp" #include "simplnx/Pipeline/Pipeline.hpp" #include "simplnx/Pipeline/PipelineFilter.hpp" @@ -121,7 +121,6 @@ TEST_CASE("SimplnxCore::ComputeVertexToTriangleDistancesFilter: SIMPL Backwards TEST_CASE("SimplnxCore::ComputeVertexToTriangleDistancesFilter: Triangle Normals requires 3 components", "[SimplnxCore][ComputeVertexToTriangleDistancesFilter][ComponentShape]") { - UnitTest::LoadPlugins(); DataStructure dataStructure; auto* triangleGeom = TriangleGeom::Create(dataStructure, "Triangles"); REQUIRE(triangleGeom != nullptr); diff --git a/src/Plugins/SimplnxCore/test/DBSCANTest.cpp b/src/Plugins/SimplnxCore/test/DBSCANTest.cpp index 7e894e1ee2..e6e9af2f32 100644 --- a/src/Plugins/SimplnxCore/test/DBSCANTest.cpp +++ b/src/Plugins/SimplnxCore/test/DBSCANTest.cpp @@ -426,7 +426,6 @@ Result<> RunDirectOutOfCoreScanline(usize pointComponents, std::optional>(dataStructure, "Points", ShapeType{4}, ShapeType{2}); REQUIRE(points != nullptr); @@ -891,7 +880,6 @@ TEST_CASE("SimplnxCore::DBSCAN: Reject multi-component mask in preflight", "[Sim TEST_CASE("SimplnxCore::DBSCAN: Reject short mask in preflight", "[SimplnxCore][DBSCAN][ComponentShape]") { - UnitTest::LoadPlugins(); DataStructure dataStructure; auto* points = Float32Array::CreateWithStore>(dataStructure, "Points", ShapeType{4}, ShapeType{2}); REQUIRE(points != nullptr); @@ -919,7 +907,6 @@ TEST_CASE("SimplnxCore::DBSCAN: Reject short mask in preflight", "[SimplnxCore][ TEST_CASE("SimplnxCore::DBSCAN: Reject short StringArray mask in preflight", "[SimplnxCore][DBSCAN][ComponentShape]") { - UnitTest::LoadPlugins(); DataStructure dataStructure; auto* points = Float32Array::CreateWithStore>(dataStructure, "Points", ShapeType{4}, ShapeType{2}); REQUIRE(points != nullptr); @@ -943,7 +930,6 @@ TEST_CASE("SimplnxCore::DBSCAN: Reject short StringArray mask in preflight", "[S TEST_CASE("SimplnxCore::DBSCANScanline: Direct out-of-core call rejects invalid input shapes", "[SimplnxCore][DBSCAN][ComponentShape]") { - UnitTest::LoadPlugins(); DataIOCollection& collection = DataStoreUtilities::GetIOCollection(); const std::shared_ptr builtInHDF5Manager = collection.getManager("HDF5"); REQUIRE(builtInHDF5Manager != nullptr);