diff --git a/src/Plugins/OrientationAnalysis/docs/ComputeGBCDPoleFigureFilter.md b/src/Plugins/OrientationAnalysis/docs/ComputeGBCDPoleFigureFilter.md index 42e67b5054..a3c8293b2e 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/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/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/docs/WriteGBCDGMTFileFilter.md b/src/Plugins/OrientationAnalysis/docs/WriteGBCDGMTFileFilter.md index c4f33b475d..55b3150860 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/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/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/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/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/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); +} diff --git a/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp index a53979d1a2..73c5cf32ab 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp @@ -11,6 +11,7 @@ #include "simplnx/Pipeline/PipelineFilter.hpp" #include "simplnx/UnitTest/UnitTestCommon.hpp" +#include #include #include @@ -186,3 +187,41 @@ 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]") +{ + 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); +} diff --git a/src/Plugins/OrientationAnalysis/test/ComputeShapesTriangleGeomTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeShapesTriangleGeomTest.cpp index 7e57f402f5..d04d8c89e2 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,41 @@ TEST_CASE("OrientationAnalysis::ComputeShapesTriangleGeom", "[OrientationAnalysi UnitTest::CheckArraysInheritTupleDims(exemplarDataStructure); } + +TEST_CASE("OrientationAnalysis::ComputeShapesTriangleGeom: Face Feature Centroids requires 3 components", "[OrientationAnalysis][ComputeShapesTriangleGeom][ComponentShape]") +{ + 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); +} 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); +} 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/docs/ComputeKMeansFilter.md b/src/Plugins/SimplnxCore/docs/ComputeKMeansFilter.md index 912c5f4411..956a217c4f 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*. +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 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/docs/ComputeKMedoidsFilter.md b/src/Plugins/SimplnxCore/docs/ComputeKMedoidsFilter.md index 3b19e2f7bd..427e3fa198 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. +When **Use Mask Array** is enabled, **Mask Array** must have 1 component and 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/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/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/docs/DBSCANFilter.md b/src/Plugins/SimplnxCore/docs/DBSCANFilter.md index 5ea77bc10a..20f6e0ccda 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. +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 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..4d269e3109 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() @@ -323,16 +328,14 @@ class ExternalGDCF } if(m_Dimensions != 2 && m_Dimensions != 3) { - 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 {}.", m_Dimensions)); } 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."); + 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) { @@ -3553,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/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/src/SimplnxCore/Filters/ComputeKMeansFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMeansFilter.cpp index b120507443..9ddf6465b9 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" @@ -76,7 +77,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())); @@ -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/src/SimplnxCore/Filters/ComputeKMedoidsFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeKMedoidsFilter.cpp index 73cccb2d74..a471396c1e 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" @@ -69,7 +70,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), @@ -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/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/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/src/SimplnxCore/Filters/DBSCANFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/DBSCANFilter.cpp index aafec862c1..9f4f015de9 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/ComputeFeaturePhasesBinaryTest.cpp b/src/Plugins/SimplnxCore/test/ComputeFeaturePhasesBinaryTest.cpp index 1d781e57c2..db7f9ef0e9 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,29 @@ TEST_CASE("SimplnxCore::ComputeFeaturePhasesBinaryFilter: SIMPL Backwards Compat } } } + +TEST_CASE("SimplnxCore::ComputeFeaturePhasesBinaryFilter: Cell Feature Ids requires 1 component", "[SimplnxCore][ComputeFeaturePhasesBinaryFilter][ComponentShape]") +{ + 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); +} diff --git a/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp b/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp index 24943f78aa..4974efe501 100644 --- a/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeKMeansTest.cpp @@ -1,10 +1,12 @@ #include "SimplnxCore/SimplnxCore_test_dirs.hpp" +#include #include #include "simplnx/Core/Application.hpp" #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" @@ -605,3 +607,104 @@ TEST_CASE("SimplnxCore::ComputeKMeansFilter: SIMPL Backwards Compatibility", "[S } } } + +TEST_CASE("SimplnxCore::ComputeKMeans: Cell Mask Array requires 1 component", "[SimplnxCore][ComputeKMeans][ComponentShape]") +{ + 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); +} + +TEST_CASE("SimplnxCore::ComputeKMeans: Cell Mask Array requires matching tuple counts", "[SimplnxCore][ComputeKMeans][ComponentShape]") +{ + 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); +} + +TEST_CASE("SimplnxCore::ComputeKMeans: Reject short StringArray mask in preflight", "[SimplnxCore][ComputeKMeans][ComponentShape]") +{ + 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); +} diff --git a/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp b/src/Plugins/SimplnxCore/test/ComputeKMedoidsTest.cpp index d253093120..3ba62accde 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 @@ -8,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" @@ -474,3 +476,104 @@ TEST_CASE("SimplnxCore::ComputeKMedoidsFilter: SIMPL Backwards Compatibility", " } } } + +TEST_CASE("SimplnxCore::ComputeKMedoidsFilter: Mask Array requires 1 component", "[SimplnxCore][ComputeKMedoidsFilter][ComponentShape]") +{ + 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); +} + +TEST_CASE("SimplnxCore::ComputeKMedoidsFilter: Mask Array requires matching tuple counts", "[SimplnxCore][ComputeKMedoidsFilter][ComponentShape]") +{ + 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); +} + +TEST_CASE("SimplnxCore::ComputeKMedoidsFilter: Reject short StringArray mask in preflight", "[SimplnxCore][ComputeKMedoidsFilter][ComponentShape]") +{ + 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); +} diff --git a/src/Plugins/SimplnxCore/test/ComputeNumFeaturesTest.cpp b/src/Plugins/SimplnxCore/test/ComputeNumFeaturesTest.cpp index a17279893a..453bea7777 100644 --- a/src/Plugins/SimplnxCore/test/ComputeNumFeaturesTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeNumFeaturesTest.cpp @@ -26,8 +26,108 @@ 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) +{ + 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]") +{ + 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]") +{ + 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]") +{ + 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(); diff --git a/src/Plugins/SimplnxCore/test/ComputeVertexToTriangleDistancesTest.cpp b/src/Plugins/SimplnxCore/test/ComputeVertexToTriangleDistancesTest.cpp index 8981c0b064..b3abfc1614 100644 --- a/src/Plugins/SimplnxCore/test/ComputeVertexToTriangleDistancesTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeVertexToTriangleDistancesTest.cpp @@ -3,11 +3,13 @@ #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" #include "simplnx/UnitTest/UnitTestCommon.hpp" +#include #include namespace fs = std::filesystem; @@ -116,3 +118,45 @@ TEST_CASE("SimplnxCore::ComputeVertexToTriangleDistancesFilter: SIMPL Backwards } } } + +TEST_CASE("SimplnxCore::ComputeVertexToTriangleDistancesFilter: Triangle Normals requires 3 components", "[SimplnxCore][ComputeVertexToTriangleDistancesFilter][ComponentShape]") +{ + 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); +} diff --git a/src/Plugins/SimplnxCore/test/DBSCANTest.cpp b/src/Plugins/SimplnxCore/test/DBSCANTest.cpp index 69d2953dca..e6e9af2f32 100644 --- a/src/Plugins/SimplnxCore/test/DBSCANTest.cpp +++ b/src/Plugins/SimplnxCore/test/DBSCANTest.cpp @@ -6,6 +6,12 @@ #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" #include "simplnx/Pipeline/PipelineFilter.hpp" @@ -14,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; @@ -246,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]") @@ -678,6 +854,144 @@ 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::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::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();