diff --git a/src/Plugins/OrientationAnalysis/docs/AlignSectionsMisorientationFilter.md b/src/Plugins/OrientationAnalysis/docs/AlignSectionsMisorientationFilter.md index b680cfc3c5..d1a5f696c8 100644 --- a/src/Plugins/OrientationAnalysis/docs/AlignSectionsMisorientationFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/AlignSectionsMisorientationFilter.md @@ -65,6 +65,8 @@ In previous versions a file would have been produced instead. If you wish to rec - **Crystal Structures** -- ensemble-level array read from EBSD data or created by [Create Ensemble Info](CreateEnsembleInfoFilter.md). - **Mask** (optional) -- a boolean array marking valid cells, typically produced by a threshold operation such as [Multi-Threshold Objects](../SimplnxCore/MultiThresholdObjectsFilter.md). +**Cell Quaternions** and **Cell Phases** must each have one tuple per cell of the selected geometry. When **Use Mask Array** is enabled, **Cell Mask Array** must also have one tuple per cell. They can be in any Attribute Matrix. + ## Algorithm ### In-Core Path diff --git a/src/Plugins/OrientationAnalysis/docs/AlignSectionsMutualInformationFilter.md b/src/Plugins/OrientationAnalysis/docs/AlignSectionsMutualInformationFilter.md index 7f267e4db7..5b89e50b47 100644 --- a/src/Plugins/OrientationAnalysis/docs/AlignSectionsMutualInformationFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/AlignSectionsMutualInformationFilter.md @@ -66,6 +66,8 @@ In previous versions a file would have been produced instead. If you wish to rec - **Crystal Structures** -- ensemble-level array read from EBSD data or created by [Create Ensemble Info](CreateEnsembleInfoFilter.md). - **Mask** (optional) -- a boolean array marking valid cells, typically produced by a threshold operation such as [Multi-Threshold Objects](../SimplnxCore/MultiThresholdObjectsFilter.md). +**Cell Quaternions** and **Cell Phases** must each have one tuple per cell of the selected geometry. When **Use Mask Array** is enabled, **Cell Mask Array** must also have one tuple per cell. They can be in any Attribute Matrix. + % Auto generated parameter table will be inserted here ## References diff --git a/src/Plugins/OrientationAnalysis/docs/BadDataNeighborOrientationCheckFilter.md b/src/Plugins/OrientationAnalysis/docs/BadDataNeighborOrientationCheckFilter.md index ba46453696..eed1aab6a3 100644 --- a/src/Plugins/OrientationAnalysis/docs/BadDataNeighborOrientationCheckFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/BadDataNeighborOrientationCheckFilter.md @@ -55,6 +55,8 @@ The cells that flipped from false (black) to true (IPF-colored) had Confidence I - **Cell Phases** -- typically read from EBSD data alongside the quaternions. - **Crystal Structures** -- ensemble-level array read from EBSD data or created by [Create Ensemble Info](CreateEnsembleInfoFilter.md). +**Cell Quaternions**, **Cell Phases** and **Cell Mask Array** must each have one tuple per cell of the selected geometry. They can be in any Attribute Matrix. + % Auto generated parameter table will be inserted here ## Example Pipelines diff --git a/src/Plugins/OrientationAnalysis/docs/ComputeFeatureReferenceCAxisMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/docs/ComputeFeatureReferenceCAxisMisorientationsFilter.md index 27dc2c24b8..ee5ecda4f6 100644 --- a/src/Plugins/OrientationAnalysis/docs/ComputeFeatureReferenceCAxisMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/ComputeFeatureReferenceCAxisMisorientationsFilter.md @@ -35,6 +35,8 @@ Results may differ from the DREAM3D 6.6 version by approximately 0.0001 degrees - **Average C-Axes** -- produced by [Compute Average C-Axis Orientations](ComputeAvgCAxesFilter.md). - **Crystal Structures** -- ensemble-level array read from EBSD data or created by [Create Ensemble Info](CreateEnsembleInfoFilter.md). +**Cell Feature Ids**, **Cell Phases** and **Cell Quaternions** must each have one tuple per cell of the selected geometry. They can be in any Attribute Matrix. + ## Algorithm For each cell, the quaternion is converted to an active rotation matrix (transpose of the passive orientation matrix) and applied to the <001> c-axis to find the c-axis direction in the sample reference frame. The angle between this cell-level c-axis and the feature's average c-axis is computed. Angles exceeding 90 degrees are folded to (180 - angle) to account for the antipodal symmetry of hexagonal c-axes. The per-cell misorientation values are then used to compute the average and population standard deviation per feature. diff --git a/src/Plugins/OrientationAnalysis/docs/ComputeKernelAvgMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/docs/ComputeKernelAvgMisorientationsFilter.md index 3c16c3952b..b8a303d0ad 100644 --- a/src/Plugins/OrientationAnalysis/docs/ComputeKernelAvgMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/ComputeKernelAvgMisorientationsFilter.md @@ -65,6 +65,8 @@ In 3D, the Z Radius works the same way, extending into adjacent slices above and For related per-feature misorientation metrics, see the **Compute Feature Reference Misorientations** and **Compute Misorientation** filters. +**Cell Feature Ids**, **Cell Phases** and **Cell Quaternions** must each have one tuple per cell of the selected geometry. They can be in any Attribute Matrix. + ## Algorithm For each valid cell in the Image Geometry, the algorithm examines all cells within the user-specified kernel radius in X, Y, and Z. In per-grain mode, only neighbors with the same feature ID as the center cell are included. In per-voxel mode, any neighbor with a positive feature ID and the same phase as the center cell is included. The crystallographic misorientation angle between the center cell's quaternion and each qualifying neighbor's quaternion is computed using the appropriate LaueOps symmetry operators. The average of these misorientation angles is stored as the KAM value for the center cell. diff --git a/src/Plugins/OrientationAnalysis/docs/ComputeShapesFilter.md b/src/Plugins/OrientationAnalysis/docs/ComputeShapesFilter.md index b0e5c862ad..3e5baebf86 100644 --- a/src/Plugins/OrientationAnalysis/docs/ComputeShapesFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/ComputeShapesFilter.md @@ -32,6 +32,8 @@ For shape analysis on triangle geometry meshes rather than voxelized image geome - **Cell Feature Ids** -- produced by a segmentation filter such as [Segment Features (Misorientation)](EBSDSegmentFeaturesFilter.md) or [Segment Features (Scalar)](../SimplnxCore/ScalarSegmentFeaturesFilter.md). - **Feature Centroids** -- produced by [Compute Feature Centroids](../SimplnxCore/ComputeFeatureCentroidsFilter.md). +**Cell Feature Ids** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + % Auto generated parameter table will be inserted here ## References diff --git a/src/Plugins/OrientationAnalysis/docs/NeighborOrientationCorrelationFilter.md b/src/Plugins/OrientationAnalysis/docs/NeighborOrientationCorrelationFilter.md index 742e828c8d..b3d2078f8d 100644 --- a/src/Plugins/OrientationAnalysis/docs/NeighborOrientationCorrelationFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/NeighborOrientationCorrelationFilter.md @@ -26,6 +26,8 @@ Cell (1,1) has CI = 0.05, which is below a 0.1 minimum, so it is flagged. Its 4 In this example, cell (1,1) has a confidence index < 0.1. Its four neighbors are compared pairwise with each other: (13.0), (14.0), (12.0) and (15.0) are all within a few degrees of each other, so every neighbor participates in several similar pairs. The central cell (1,1) has all of its attributes replaced with those of the neighbor holding the highest similar-pair count. Note that the central cell's own orientation and phase play no role in choosing the replacement — only the mutual similarity of its neighbors matters. +**Confidence Index**, **Cell Phases** and **Cell Quaternions** must each have one tuple per cell of the selected geometry. Replacement copies affect arrays in the Attribute Matrix that contains **Confidence Index**, except the arrays in **Attribute Arrays to Ignore**. Keep **Cell Phases** and **Cell Quaternions** in that matrix when they must also be repaired. + ## Example Data | Example Input/Output Images | @@ -43,13 +45,7 @@ This filter will copy all attribute data from neighboring cells into the target ### Differences from legacy DREAM.3D 6.5.171 -DREAM3D-NX's implementation fixes three defects present in DREAM.3D 6.5.171, so results will differ on most datasets: - -1. 6.5.171 could count a mixed-phase (or unindexed, phase-0) neighbor pair as "similar" by reusing the previous pair's misorientation, occasionally replacing a cell with data from a **different phase**. -2. 6.5.171 ran only half the cleanup passes of the original design (`ceil((6 − Level)/2)` instead of `6 − Level`), leaving deep bad regions partially unfilled. -3. 6.5.171 copied from the *last* neighbor having any similar pair rather than the neighbor with the *highest* similarity count. (When all counts tie — the common case in grain interiors — both versions pick the same neighbor.) - -See `NeighborOrientationCorrelationFilter-D1` through `-D4` in `src/Plugins/OrientationAnalysis/vv/deviations/NeighborOrientationCorrelationFilter.md` for the full analysis and migration guidance. +DREAM3D-NX's implementation fixes three defects present in DREAM.3D 6.5.171. ## Algorithm diff --git a/src/Plugins/OrientationAnalysis/docs/WriteINLFileFilter.md b/src/Plugins/OrientationAnalysis/docs/WriteINLFileFilter.md index 1b1f02138a..99a1f86077 100644 --- a/src/Plugins/OrientationAnalysis/docs/WriteINLFileFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/WriteINLFileFilter.md @@ -73,6 +73,8 @@ Some information about the phase is included in the header section of the file i - **Crystal Structures** -- the **Ensemble**-level array read alongside the phases from the same EBSD reader. - **Cell Feature Ids** -- produced by a segmentation filter such as [Segment Features (Misorientation)](EBSDSegmentFeaturesFilter.md) or [Segment Features (Scalar)](../SimplnxCore/ScalarSegmentFeaturesFilter.md). +**Cell Feature Ids**, **Cell Phases** and **Cell Euler Angles** must each have one tuple per cell of the selected geometry. They can be in any Attribute Matrix. + % Auto generated parameter table will be inserted here ## Example Pipelines diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/AlignSectionsMisorientationFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/AlignSectionsMisorientationFilter.cpp index dfc708102b..37248efc20 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/AlignSectionsMisorientationFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/AlignSectionsMisorientationFilter.cpp @@ -185,6 +185,16 @@ IFilter::PreflightResult AlignSectionsMisorientationFilter::preflightImpl(const return MakePreflightErrorResult(k_InconsistentTupleCount, fmt::format("The following DataArrays all must have equal number of tuples but this was not satisfied.\n{}", tupleValidityCheck.error())); } + std::vector cellArrayPaths = {pQuatsArrayPath, pCellPhasesArrayPath}; + if(pUseGoodVoxels) + { + cellArrayPaths.emplace_back(pGoodVoxelsArrayPath); + } + if(auto tupleResult = ValidateCellTupleCount(dataStructure, inputImageGeometry, cellArrayPaths, -68072); tupleResult.invalid()) + { + return {ConvertResultTo(std::move(tupleResult), {})}; + } + const auto* inputGeom = dataStructure.getDataAs(inputImageGeometry); if(inputGeom == nullptr) { diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/AlignSectionsMutualInformationFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/AlignSectionsMutualInformationFilter.cpp index bc642a508a..73e43f1123 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/AlignSectionsMutualInformationFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/AlignSectionsMutualInformationFilter.cpp @@ -13,6 +13,7 @@ #include "simplnx/Parameters/DataObjectNameParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/FilterUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" #include "simplnx/Utilities/StringUtilities.hpp" @@ -143,6 +144,7 @@ IFilter::PreflightResult AlignSectionsMutualInformationFilter::preflightImpl(con auto pGoodVoxelsArrayPathValue = filterArgs.value(k_MaskArrayPath_Key); auto pCrystalStructuresArrayPathValue = filterArgs.value(k_CrystalStructuresArrayPath_Key); + auto useMask = filterArgs.value(k_UseMask_Key); auto pStoreAlignmentShifts = filterArgs.value(k_StoreAlignmentShifts_Key); nx::core::Result resultOutputActions; @@ -159,13 +161,27 @@ IFilter::PreflightResult AlignSectionsMutualInformationFilter::preflightImpl(con -3541, fmt::format("The Image Geometry is not 3D and cannot be run through this filter. The dimensions are ({})", StringUtilities::formatDimensions3D(imageGeom.getDimensions()))); } - std::vector dataArrayPaths = {pQuatsArrayPathValue, pCellPhasesArrayPathValue, pGoodVoxelsArrayPathValue}; + std::vector dataArrayPaths = {pQuatsArrayPathValue, pCellPhasesArrayPathValue}; + if(useMask) + { + dataArrayPaths.push_back(pGoodVoxelsArrayPathValue); + } auto tupleValidityCheck = dataStructure.validateNumberOfTuples(dataArrayPaths); if(!tupleValidityCheck) { return MakePreflightErrorResult(-3542, fmt::format("The following DataArrays all must have equal number of tuples but this was not satisfied.\n{}", tupleValidityCheck.error())); } + std::vector cellArrayPaths = {pQuatsArrayPathValue, pCellPhasesArrayPathValue}; + if(useMask) + { + cellArrayPaths.emplace_back(pGoodVoxelsArrayPathValue); + } + if(auto tupleResult = ValidateCellTupleCount(dataStructure, imageGeometryPath, cellArrayPaths, -3543); tupleResult.invalid()) + { + return {ConvertResultTo(std::move(tupleResult), {})}; + } + // Handle Array Creation if(pStoreAlignmentShifts) { diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/BadDataNeighborOrientationCheckFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/BadDataNeighborOrientationCheckFilter.cpp index 60a1e70d08..b9e634a24b 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/BadDataNeighborOrientationCheckFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/BadDataNeighborOrientationCheckFilter.cpp @@ -7,6 +7,7 @@ #include "simplnx/Parameters/DataGroupSelectionParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" using namespace nx::core; @@ -97,6 +98,7 @@ IFilter::UniquePointer BadDataNeighborOrientationCheckFilter::clone() const IFilter::PreflightResult BadDataNeighborOrientationCheckFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { + auto imageGeometryPath = filterArgs.value(k_ImageGeometryPath_Key); auto pQuatsArrayPathValue = filterArgs.value(k_QuatsArrayPath_Key); auto pMaskArrayPathValue = filterArgs.value(k_MaskArrayPath_Key); auto pCellPhasesArrayPathValue = filterArgs.value(k_CellPhasesArrayPath_Key); @@ -121,6 +123,11 @@ IFilter::PreflightResult BadDataNeighborOrientationCheckFilter::preflightImpl(co return MakePreflightErrorResult(k_InconsistentTupleCount, fmt::format("The following DataArrays all must have equal number of tuples but this was not satisfied.\n{}", tupleValidityCheck.error())); } + if(auto tupleResult = ValidateCellTupleCount(dataStructure, imageGeometryPath, {pQuatsArrayPathValue, pCellPhasesArrayPathValue, pMaskArrayPathValue}, -6810); tupleResult.invalid()) + { + return {ConvertResultTo(std::move(tupleResult), {})}; + } + resultOutputActions.value().modifiedActions.emplace_back( DataObjectModification{pMaskArrayPathValue, DataObjectModification::ModifiedType::Modified, dataStructure.getData(pMaskArrayPathValue)->getDataObjectType()}); diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureReferenceCAxisMisorientationsFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureReferenceCAxisMisorientationsFilter.cpp index 03a0c63d75..84e377e53c 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureReferenceCAxisMisorientationsFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureReferenceCAxisMisorientationsFilter.cpp @@ -8,6 +8,7 @@ #include "simplnx/Parameters/DataGroupSelectionParameter.hpp" #include "simplnx/Parameters/DataObjectNameParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" using namespace nx::core; @@ -96,6 +97,7 @@ IFilter::UniquePointer ComputeFeatureReferenceCAxisMisorientationsFilter::clone( IFilter::PreflightResult ComputeFeatureReferenceCAxisMisorientationsFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { + auto imageGeometryPath = filterArgs.value(k_ImageGeometryPath_Key); auto pFeatureIdsArrayPathValue = filterArgs.value(k_FeatureIdsArrayPath_Key); auto pCellPhasesArrayPathValue = filterArgs.value(k_CellPhasesArrayPath_Key); auto pQuatsArrayPathValue = filterArgs.value(k_QuatsArrayPath_Key); @@ -114,6 +116,11 @@ IFilter::PreflightResult ComputeFeatureReferenceCAxisMisorientationsFilter::pref return {MakeErrorResult(-9800, fmt::format("The following DataArrays all must have equal number of tuples but this was not satisfied.\n{}", tupleValidityCheck.error()))}; } + if(auto tupleResult = ValidateCellTupleCount(dataStructure, imageGeometryPath, {pFeatureIdsArrayPathValue, pCellPhasesArrayPathValue, pQuatsArrayPathValue}, -9805); tupleResult.invalid()) + { + return {ConvertResultTo(std::move(tupleResult), {})}; + } + ShapeType tupleShape = dataStructure.getDataRefAs(pFeatureIdsArrayPathValue).getTupleShape(); { auto createArrayAction = diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeKernelAvgMisorientationsFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeKernelAvgMisorientationsFilter.cpp index 4542b00b06..b6f0f27d5b 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeKernelAvgMisorientationsFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeKernelAvgMisorientationsFilter.cpp @@ -10,6 +10,7 @@ #include "simplnx/Parameters/DataObjectNameParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" #include "simplnx/Parameters/VectorParameter.hpp" @@ -103,6 +104,11 @@ IFilter::PreflightResult ComputeKernelAvgMisorientationsFilter::preflightImpl(co auto pKernelAverageMisorientationsArrayNameValue = pCellPhasesArrayPathValue.replaceName(filterArgs.value(k_KernelAverageMisorientationsArrayName_Key)); auto inputImageGeometry = filterArgs.value(k_SelectedImageGeometryPath_Key); + if(auto tupleResult = ValidateCellTupleCount(dataStructure, inputImageGeometry, {pFeatureIdsArrayPathValue, pCellPhasesArrayPathValue, pQuatsArrayPathValue}, -67205); tupleResult.invalid()) + { + return {ConvertResultTo(std::move(tupleResult), {})}; + } + PreflightResult preflightResult; nx::core::Result resultOutputActions; diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeShapesFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeShapesFilter.cpp index f970a2c548..f18a77a642 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeShapesFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeShapesFilter.cpp @@ -8,6 +8,7 @@ #include "simplnx/Parameters/ArraySelectionParameter.hpp" #include "simplnx/Parameters/DataObjectNameParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" @@ -99,6 +100,12 @@ IFilter::PreflightResult ComputeShapesFilter::preflightImpl(const DataStructure& auto pAxisEulerAnglesArrayName = filterArgs.value(k_AxisEulerAnglesArrayName_Key); auto pAspectRatiosArrayName = filterArgs.value(k_AspectRatiosArrayName_Key); auto pVolumesArrayName = filterArgs.value(k_VolumesArrayName_Key); + auto tupleCountResult = ValidateCellTupleCount(dataStructure, filterArgs.value(k_SelectedImageGeometryPath_Key), {pFeatureIdsArrayPath}, -12806); + if(tupleCountResult.invalid()) + { + return {ConvertResultTo(std::move(tupleCountResult), {})}; + } + auto featureAttrMatrixPath = pCentroidsArrayPath.getParent(); auto pOmega3sArrayPath = featureAttrMatrixPath.createChildPath(pOmega3sArrayName); auto pAxisLengthsArrayPath = featureAttrMatrixPath.createChildPath(pAxisLengthsArrayName); diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/NeighborOrientationCorrelationFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/NeighborOrientationCorrelationFilter.cpp index cb0c157623..3b795479b1 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/NeighborOrientationCorrelationFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/NeighborOrientationCorrelationFilter.cpp @@ -8,6 +8,7 @@ #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/MultiArraySelectionParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" #include "simplnx/Parameters/NumberParameter.hpp" @@ -174,6 +175,11 @@ IFilter::PreflightResult NeighborOrientationCorrelationFilter::preflightImpl(con MakeErrorResult(k_InvalidNumTuples, fmt::format("The following DataArrays all must have equal number of tuples but this was not satisfied.\n{}", tupleValidityCheck.error()))}; } + if(auto tupleResult = ValidateCellTupleCount(dataStructure, pImageGeomPathValue, {pConfidenceIndexArrayPathValue, pCellPhasesArrayPathValue, pQuatsArrayPathValue}, -580098); tupleResult.invalid()) + { + return {ConvertResultTo(std::move(tupleResult), {})}; + } + // Inform users that the following arrays are going to be modified in place // Cell Data is going to be modified nx::core::AppendDataObjectModifications(dataStructure, resultOutputActions.value().modifiedActions, imageGeom.getCellDataPath(), {}); diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/WriteINLFileFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/WriteINLFileFilter.cpp index 12c04557a3..3aa2f9311f 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/WriteINLFileFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/WriteINLFileFilter.cpp @@ -11,6 +11,7 @@ #include "simplnx/Parameters/FileSystemPathParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" #include @@ -98,8 +99,17 @@ IFilter::UniquePointer WriteINLFileFilter::clone() const IFilter::PreflightResult WriteINLFileFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { + auto imageGeometryPath = filterArgs.value(k_ImageGeomPath_Key); + auto featureIdsPath = filterArgs.value(k_FeatureIdsArrayPath_Key); + auto cellPhasesPath = filterArgs.value(k_CellPhasesArrayPath_Key); + auto eulerAnglesPath = filterArgs.value(k_CellEulerAnglesArrayPath_Key); auto pMaterialNameArrayPathValue = filterArgs.value(k_MaterialNameArrayPath_Key); + if(auto tupleResult = ValidateCellTupleCount(dataStructure, imageGeometryPath, {featureIdsPath, cellPhasesPath, eulerAnglesPath}, -78431); tupleResult.invalid()) + { + return {ConvertResultTo(std::move(tupleResult), {})}; + } + PreflightResult preflightResult; nx::core::Result resultOutputActions; std::vector preflightUpdatedValues; diff --git a/src/Plugins/OrientationAnalysis/test/AlignSectionsMisorientationTest.cpp b/src/Plugins/OrientationAnalysis/test/AlignSectionsMisorientationTest.cpp index 8fa8093bc3..4d5a53feb0 100644 --- a/src/Plugins/OrientationAnalysis/test/AlignSectionsMisorientationTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/AlignSectionsMisorientationTest.cpp @@ -337,3 +337,72 @@ TEST_CASE("OrientationAnalysis::AlignSectionsMisorientationFilter: output test", UnitTest::CheckArraysInheritTupleDims(dataStructure, SmallIn100::k_TupleCheckIgnoredPaths); } + +TEST_CASE("OrientationAnalysis::AlignSectionsMisorientationFilter: Geometry cell tuple counts", "[OrientationAnalysis][AlignSectionsMisorientation][TupleCount]") +{ + // Cases: all short, all long, all matching outside Cell Data, then each input short. + const int testCase = GENERATE(0, 1, 2, 3, 4, 5, 6, 7); + CAPTURE(testCase); + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "TupleGeometry"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 2, 2}); + auto* cellDataPtr = AttributeMatrix::Create(dataStructure, "Cell Data", {2, 2, 2}, imageGeomPtr->getId()); + REQUIRE(cellDataPtr != nullptr); + imageGeomPtr->setCellData(*cellDataPtr); + + AlignSectionsMisorientationFilter filter; + Arguments args = filter.getDefaultArguments(); + args.insertOrAssign(AlignSectionsMisorientationFilter::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"TupleGeometry"}))); + const usize commonCount = testCase == 0 ? 4 : (testCase == 1 ? 12 : 8); + const usize count0 = testCase == 3 ? 4 : commonCount; + auto* inputAM0Ptr = AttributeMatrix::Create(dataStructure, "Input0", {count0}); + REQUIRE(inputAM0Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "Quats", {count0}, {4}, inputAM0Ptr->getId()) != nullptr); + args.insertOrAssign(AlignSectionsMisorientationFilter::k_QuatsArrayPath_Key, std::make_any(DataPath({"Input0", "Quats"}))); + const usize count1 = testCase == 4 ? 4 : commonCount; + auto* inputAM1Ptr = AttributeMatrix::Create(dataStructure, "Input1", {count1}); + REQUIRE(inputAM1Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "Phases", {count1}, {1}, inputAM1Ptr->getId()) != nullptr); + args.insertOrAssign(AlignSectionsMisorientationFilter::k_CellPhasesArrayPath_Key, std::make_any(DataPath({"Input1", "Phases"}))); + const usize count2 = (testCase == 5 || testCase == 6) ? 4 : commonCount; + auto* inputAM2Ptr = AttributeMatrix::Create(dataStructure, "Input2", {count2}); + REQUIRE(inputAM2Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "Mask", {count2}, {1}, inputAM2Ptr->getId()) != nullptr); + args.insertOrAssign(AlignSectionsMisorientationFilter::k_MaskArrayPath_Key, std::make_any(DataPath({"Input2", "Mask"}))); + REQUIRE(UInt32Array::CreateWithStore>(dataStructure, "CrystalStructures", {2}, {1}) != nullptr); + args.insertOrAssign(AlignSectionsMisorientationFilter::k_CrystalStructuresArrayPath_Key, std::make_any(DataPath({"CrystalStructures"}))); + // Disabled masks may be short or absent. + args.insertOrAssign(AlignSectionsMisorientationFilter::k_UseMask_Key, std::make_any(testCase < 6)); + args.insertOrAssign(AlignSectionsMisorientationFilter::k_StoreAlignmentShifts_Key, std::make_any(false)); + if(testCase == 7) + { + args.insertOrAssign(AlignSectionsMisorientationFilter::k_MaskArrayPath_Key, std::make_any(DataPath({"MissingMask"}))); + } + + const auto result = filter.preflight(dataStructure, args); + if(testCase == 2 || testCase >= 6) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + const std::vector inputPaths = {DataPath({"Input0", "Quats"}), DataPath({"Input1", "Phases"}), DataPath({"Input2", "Mask"})}; + if(testCase >= 3) + { + CHECK(errorRef.code == -68063); + CHECK(errorRef.message.find(fmt::format("DataPath: {} | Tuple Count: 4", inputPaths[static_cast(testCase - 3)].toString())) != std::string::npos); + } + else + { + CHECK(errorRef.code == -68072); + CHECK(errorRef.message.find(inputPaths[0].toString()) != std::string::npos); + CHECK(errorRef.message.find(testCase == 1 ? "12 tuples" : "4 tuples") != std::string::npos); + CHECK(errorRef.message.find("8 cells") != std::string::npos); + CHECK(errorRef.message.find("TupleGeometry") != std::string::npos); + } + } +} diff --git a/src/Plugins/OrientationAnalysis/test/AlignSectionsMutualInformationTest.cpp b/src/Plugins/OrientationAnalysis/test/AlignSectionsMutualInformationTest.cpp index c2ec38beaf..1637e9227d 100644 --- a/src/Plugins/OrientationAnalysis/test/AlignSectionsMutualInformationTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/AlignSectionsMutualInformationTest.cpp @@ -4,6 +4,7 @@ #include "OrientationAnalysis/OrientationAnalysis_test_dirs.hpp" #include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Parameters/ArraySelectionParameter.hpp" #include "simplnx/Parameters/BoolParameter.hpp" @@ -244,3 +245,72 @@ TEST_CASE("OrientationAnalysis::AlignSectionsMutualInformationFilter: output tes UnitTest::CheckArraysInheritTupleDims(dataStructure); } + +TEST_CASE("OrientationAnalysis::AlignSectionsMutualInformationFilter: Geometry cell tuple counts", "[OrientationAnalysis][AlignSectionsMutualInformationFilter][TupleCount]") +{ + // Cases: all short, all long, all matching outside Cell Data, then each input short. + const int testCase = GENERATE(0, 1, 2, 3, 4, 5, 6, 7); + CAPTURE(testCase); + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "TupleGeometry"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 2, 2}); + auto* cellDataPtr = AttributeMatrix::Create(dataStructure, "Cell Data", {2, 2, 2}, imageGeomPtr->getId()); + REQUIRE(cellDataPtr != nullptr); + imageGeomPtr->setCellData(*cellDataPtr); + + AlignSectionsMutualInformationFilter filter; + Arguments args = filter.getDefaultArguments(); + args.insertOrAssign(AlignSectionsMutualInformationFilter::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"TupleGeometry"}))); + const usize commonCount = testCase == 0 ? 4 : (testCase == 1 ? 12 : 8); + const usize count0 = testCase == 3 ? 4 : commonCount; + auto* inputAM0Ptr = AttributeMatrix::Create(dataStructure, "Input0", {count0}); + REQUIRE(inputAM0Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "Quats", {count0}, {4}, inputAM0Ptr->getId()) != nullptr); + args.insertOrAssign(AlignSectionsMutualInformationFilter::k_QuatsArrayPath_Key, std::make_any(DataPath({"Input0", "Quats"}))); + const usize count1 = testCase == 4 ? 4 : commonCount; + auto* inputAM1Ptr = AttributeMatrix::Create(dataStructure, "Input1", {count1}); + REQUIRE(inputAM1Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "Phases", {count1}, {1}, inputAM1Ptr->getId()) != nullptr); + args.insertOrAssign(AlignSectionsMutualInformationFilter::k_CellPhasesArrayPath_Key, std::make_any(DataPath({"Input1", "Phases"}))); + const usize count2 = (testCase == 5 || testCase == 6) ? 4 : commonCount; + auto* inputAM2Ptr = AttributeMatrix::Create(dataStructure, "Input2", {count2}); + REQUIRE(inputAM2Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "Mask", {count2}, {1}, inputAM2Ptr->getId()) != nullptr); + args.insertOrAssign(AlignSectionsMutualInformationFilter::k_MaskArrayPath_Key, std::make_any(DataPath({"Input2", "Mask"}))); + REQUIRE(UInt32Array::CreateWithStore>(dataStructure, "CrystalStructures", {2}, {1}) != nullptr); + args.insertOrAssign(AlignSectionsMutualInformationFilter::k_CrystalStructuresArrayPath_Key, std::make_any(DataPath({"CrystalStructures"}))); + // Disabled masks may be short or absent. + args.insertOrAssign(AlignSectionsMutualInformationFilter::k_UseMask_Key, std::make_any(testCase < 6)); + args.insertOrAssign(AlignSectionsMutualInformationFilter::k_StoreAlignmentShifts_Key, std::make_any(false)); + if(testCase == 7) + { + args.insertOrAssign(AlignSectionsMutualInformationFilter::k_MaskArrayPath_Key, std::make_any(DataPath({"MissingMask"}))); + } + + const auto result = filter.preflight(dataStructure, args); + if(testCase == 2 || testCase >= 6) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + const std::vector inputPaths = {DataPath({"Input0", "Quats"}), DataPath({"Input1", "Phases"}), DataPath({"Input2", "Mask"})}; + if(testCase >= 3) + { + CHECK(errorRef.code == -3542); + CHECK(errorRef.message.find(fmt::format("DataPath: {} | Tuple Count: 4", inputPaths[static_cast(testCase - 3)].toString())) != std::string::npos); + } + else + { + CHECK(errorRef.code == -3543); + CHECK(errorRef.message.find(inputPaths[0].toString()) != std::string::npos); + CHECK(errorRef.message.find(testCase == 1 ? "12 tuples" : "4 tuples") != std::string::npos); + CHECK(errorRef.message.find("8 cells") != std::string::npos); + CHECK(errorRef.message.find("TupleGeometry") != std::string::npos); + } + } +} diff --git a/src/Plugins/OrientationAnalysis/test/BadDataNeighborOrientationCheckTest.cpp b/src/Plugins/OrientationAnalysis/test/BadDataNeighborOrientationCheckTest.cpp index 24cfe70aa5..196875b5b7 100644 --- a/src/Plugins/OrientationAnalysis/test/BadDataNeighborOrientationCheckTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/BadDataNeighborOrientationCheckTest.cpp @@ -6,6 +6,7 @@ #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Pipeline/Pipeline.hpp" #include "simplnx/Pipeline/PipelineFilter.hpp" @@ -1845,3 +1846,65 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Phase Ind UnitTest::CheckArraysInheritTupleDims(dataStructure); } + +TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Geometry cell tuple counts", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter][TupleCount]") +{ + // Cases: all short, all long, all matching outside Cell Data, then each input short. + const int testCase = GENERATE(0, 1, 2, 3, 4, 5); + CAPTURE(testCase); + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "TupleGeometry"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 2, 2}); + auto* cellDataPtr = AttributeMatrix::Create(dataStructure, "Cell Data", {2, 2, 2}, imageGeomPtr->getId()); + REQUIRE(cellDataPtr != nullptr); + imageGeomPtr->setCellData(*cellDataPtr); + + BadDataNeighborOrientationCheckFilter filter; + Arguments args = filter.getDefaultArguments(); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_ImageGeometryPath_Key, std::make_any(DataPath({"TupleGeometry"}))); + const usize commonCount = testCase == 0 ? 4 : (testCase == 1 ? 12 : 8); + const usize count0 = testCase == 3 ? 4 : commonCount; + auto* inputAM0Ptr = AttributeMatrix::Create(dataStructure, "Input0", {count0}); + REQUIRE(inputAM0Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "Quats", {count0}, {4}, inputAM0Ptr->getId()) != nullptr); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_QuatsArrayPath_Key, std::make_any(DataPath({"Input0", "Quats"}))); + const usize count1 = testCase == 4 ? 4 : commonCount; + auto* inputAM1Ptr = AttributeMatrix::Create(dataStructure, "Input1", {count1}); + REQUIRE(inputAM1Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "Phases", {count1}, {1}, inputAM1Ptr->getId()) != nullptr); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_CellPhasesArrayPath_Key, std::make_any(DataPath({"Input1", "Phases"}))); + const usize count2 = testCase == 5 ? 4 : commonCount; + auto* inputAM2Ptr = AttributeMatrix::Create(dataStructure, "Input2", {count2}); + REQUIRE(inputAM2Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "Mask", {count2}, {1}, inputAM2Ptr->getId()) != nullptr); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_MaskArrayPath_Key, std::make_any(DataPath({"Input2", "Mask"}))); + REQUIRE(UInt32Array::CreateWithStore>(dataStructure, "CrystalStructures", {2}, {1}) != nullptr); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_CrystalStructuresArrayPath_Key, std::make_any(DataPath({"CrystalStructures"}))); + + const auto result = filter.preflight(dataStructure, args); + if(testCase == 2) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + const std::vector inputPaths = {DataPath({"Input0", "Quats"}), DataPath({"Input1", "Phases"}), DataPath({"Input2", "Mask"})}; + if(testCase >= 3) + { + CHECK(errorRef.code == -6809); + CHECK(errorRef.message.find(fmt::format("DataPath: {} | Tuple Count: 4", inputPaths[static_cast(testCase - 3)].toString())) != std::string::npos); + } + else + { + CHECK(errorRef.code == -6810); + CHECK(errorRef.message.find(inputPaths[0].toString()) != std::string::npos); + CHECK(errorRef.message.find(testCase == 1 ? "12 tuples" : "4 tuples") != std::string::npos); + CHECK(errorRef.message.find("8 cells") != std::string::npos); + CHECK(errorRef.message.find("TupleGeometry") != std::string::npos); + } + } +} diff --git a/src/Plugins/OrientationAnalysis/test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp index a51b4e5a4f..907c528db2 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeFeatureReferenceCAxisMisorientationsTest.cpp @@ -6,6 +6,7 @@ #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Pipeline/Pipeline.hpp" #include "simplnx/Pipeline/PipelineFilter.hpp" @@ -727,3 +728,69 @@ TEST_CASE("OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilte UnitTest::CheckArraysInheritTupleDims(fixture.ds); } + +TEST_CASE("OrientationAnalysis::ComputeFeatureReferenceCAxisMisorientationsFilter: Geometry cell tuple counts", "[OrientationAnalysis][ComputeFeatureReferenceCAxisMisorientationsFilter][TupleCount]") +{ + // Cases: all short, all long, all matching outside Cell Data, then each input short. + const int testCase = GENERATE(0, 1, 2, 3, 4, 5); + CAPTURE(testCase); + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "TupleGeometry"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 2, 2}); + auto* cellDataPtr = AttributeMatrix::Create(dataStructure, "Cell Data", {2, 2, 2}, imageGeomPtr->getId()); + REQUIRE(cellDataPtr != nullptr); + imageGeomPtr->setCellData(*cellDataPtr); + + ComputeFeatureReferenceCAxisMisorientationsFilter filter; + Arguments args = filter.getDefaultArguments(); + args.insertOrAssign(ComputeFeatureReferenceCAxisMisorientationsFilter::k_ImageGeometryPath_Key, std::make_any(DataPath({"TupleGeometry"}))); + const usize commonCount = testCase == 0 ? 4 : (testCase == 1 ? 12 : 8); + const usize count0 = testCase == 3 ? 4 : commonCount; + auto* inputAM0Ptr = AttributeMatrix::Create(dataStructure, "Input0", {count0}); + REQUIRE(inputAM0Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "FeatureIds", {count0}, {1}, inputAM0Ptr->getId()) != nullptr); + args.insertOrAssign(ComputeFeatureReferenceCAxisMisorientationsFilter::k_FeatureIdsArrayPath_Key, std::make_any(DataPath({"Input0", "FeatureIds"}))); + const usize count1 = testCase == 4 ? 4 : commonCount; + auto* inputAM1Ptr = AttributeMatrix::Create(dataStructure, "Input1", {count1}); + REQUIRE(inputAM1Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "Phases", {count1}, {1}, inputAM1Ptr->getId()) != nullptr); + args.insertOrAssign(ComputeFeatureReferenceCAxisMisorientationsFilter::k_CellPhasesArrayPath_Key, std::make_any(DataPath({"Input1", "Phases"}))); + const usize count2 = testCase == 5 ? 4 : commonCount; + auto* inputAM2Ptr = AttributeMatrix::Create(dataStructure, "Input2", {count2}); + REQUIRE(inputAM2Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "Quats", {count2}, {4}, inputAM2Ptr->getId()) != nullptr); + args.insertOrAssign(ComputeFeatureReferenceCAxisMisorientationsFilter::k_QuatsArrayPath_Key, std::make_any(DataPath({"Input2", "Quats"}))); + REQUIRE(UInt32Array::CreateWithStore>(dataStructure, "CrystalStructures", {2}, {1}) != nullptr); + args.insertOrAssign(ComputeFeatureReferenceCAxisMisorientationsFilter::k_CrystalStructuresArrayPath_Key, std::make_any(DataPath({"CrystalStructures"}))); + auto* featuresPtr = AttributeMatrix::Create(dataStructure, "Features", {2}); + REQUIRE(featuresPtr != nullptr); + REQUIRE(Float32Array::CreateWithStore>(dataStructure, "AvgCAxes", {2}, {3}, featuresPtr->getId()) != nullptr); + args.insertOrAssign(ComputeFeatureReferenceCAxisMisorientationsFilter::k_AvgCAxesArrayPath_Key, std::make_any(DataPath({"Features", "AvgCAxes"}))); + + const auto result = filter.preflight(dataStructure, args); + if(testCase == 2) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + const std::vector inputPaths = {DataPath({"Input0", "FeatureIds"}), DataPath({"Input1", "Phases"}), DataPath({"Input2", "Quats"})}; + if(testCase >= 3) + { + CHECK(errorRef.code == -9800); + CHECK(errorRef.message.find(fmt::format("DataPath: {} | Tuple Count: 4", inputPaths[static_cast(testCase - 3)].toString())) != std::string::npos); + } + else + { + CHECK(errorRef.code == -9805); + CHECK(errorRef.message.find(inputPaths[0].toString()) != std::string::npos); + CHECK(errorRef.message.find(testCase == 1 ? "12 tuples" : "4 tuples") != std::string::npos); + CHECK(errorRef.message.find("8 cells") != std::string::npos); + CHECK(errorRef.message.find("TupleGeometry") != std::string::npos); + } + } +} diff --git a/src/Plugins/OrientationAnalysis/test/ComputeKernelAvgMisorientationsTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeKernelAvgMisorientationsTest.cpp index 650c36653b..4cd931fdff 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeKernelAvgMisorientationsTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeKernelAvgMisorientationsTest.cpp @@ -5,6 +5,7 @@ #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Parameters/ArrayCreationParameter.hpp" #include "simplnx/Parameters/ChoicesParameter.hpp" @@ -1080,3 +1081,63 @@ TEST_CASE("OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Bounded C } } } + +TEST_CASE("OrientationAnalysis::ComputeKernelAvgMisorientationsFilter: Geometry cell tuple counts", "[OrientationAnalysis][ComputeKernelAvgMisorientationsFilter][TupleCount]") +{ + // Cases: all short, all long, all matching outside Cell Data, then each input short. + const int testCase = GENERATE(0, 1, 2, 3, 4, 5); + CAPTURE(testCase); + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "TupleGeometry"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 2, 2}); + auto* cellDataPtr = AttributeMatrix::Create(dataStructure, "Cell Data", {2, 2, 2}, imageGeomPtr->getId()); + REQUIRE(cellDataPtr != nullptr); + imageGeomPtr->setCellData(*cellDataPtr); + + ComputeKernelAvgMisorientationsFilter filter; + Arguments args = filter.getDefaultArguments(); + args.insertOrAssign(ComputeKernelAvgMisorientationsFilter::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"TupleGeometry"}))); + const usize commonCount = testCase == 0 ? 4 : (testCase == 1 ? 12 : 8); + const usize count0 = testCase == 3 ? 4 : commonCount; + auto* inputAM0Ptr = AttributeMatrix::Create(dataStructure, "Input0", {count0}); + REQUIRE(inputAM0Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "FeatureIds", {count0}, {1}, inputAM0Ptr->getId()) != nullptr); + args.insertOrAssign(ComputeKernelAvgMisorientationsFilter::k_CellFeatureIdsArrayPath_Key, std::make_any(DataPath({"Input0", "FeatureIds"}))); + const usize count1 = testCase == 4 ? 4 : commonCount; + auto* inputAM1Ptr = AttributeMatrix::Create(dataStructure, "Input1", {count1}); + REQUIRE(inputAM1Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "Phases", {count1}, {1}, inputAM1Ptr->getId()) != nullptr); + args.insertOrAssign(ComputeKernelAvgMisorientationsFilter::k_CellPhasesArrayPath_Key, std::make_any(DataPath({"Input1", "Phases"}))); + const usize count2 = testCase == 5 ? 4 : commonCount; + auto* inputAM2Ptr = AttributeMatrix::Create(dataStructure, "Input2", {count2}); + REQUIRE(inputAM2Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "Quats", {count2}, {4}, inputAM2Ptr->getId()) != nullptr); + args.insertOrAssign(ComputeKernelAvgMisorientationsFilter::k_QuatsArrayPath_Key, std::make_any(DataPath({"Input2", "Quats"}))); + REQUIRE(UInt32Array::CreateWithStore>(dataStructure, "CrystalStructures", {2}, {1}) != nullptr); + args.insertOrAssign(ComputeKernelAvgMisorientationsFilter::k_CrystalStructuresArrayPath_Key, std::make_any(DataPath({"CrystalStructures"}))); + const bool useFeatureIds = GENERATE(true, false); + CAPTURE(useFeatureIds); + args.insertOrAssign(ComputeKernelAvgMisorientationsFilter::k_UseFeatureIds_Key, std::make_any(useFeatureIds)); + + const auto result = filter.preflight(dataStructure, args); + if(testCase == 2) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + { + const auto& errorRef = result.outputActions.errors()[0]; + CHECK(errorRef.code == -67205); + const std::vector inputPaths = {DataPath({"Input0", "FeatureIds"}), DataPath({"Input1", "Phases"}), DataPath({"Input2", "Quats"})}; + const usize inputIndex = testCase >= 3 ? static_cast(testCase - 3) : 0; + CHECK(errorRef.message.find(inputPaths[inputIndex].toString()) != std::string::npos); + CHECK(errorRef.message.find(testCase == 1 ? "12 tuples" : "4 tuples") != std::string::npos); + CHECK(errorRef.message.find("8 cells") != std::string::npos); + CHECK(errorRef.message.find("TupleGeometry") != std::string::npos); + } + } +} diff --git a/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp index 8a7c17fbfa..a53979d1a2 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeShapesFilterTest.cpp @@ -2,6 +2,10 @@ #include "OrientationAnalysis/OrientationAnalysis_test_dirs.hpp" #include "simplnx/Core/Application.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataStore.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Parameters/ArrayCreationParameter.hpp" #include "simplnx/Pipeline/Pipeline.hpp" #include "simplnx/Pipeline/PipelineFilter.hpp" @@ -137,3 +141,48 @@ TEST_CASE("OrientationAnalysis::ComputeShapesFilter: SIMPL Backwards Compatibili } } } + +TEST_CASE("OrientationAnalysis::ComputeShapesFilter: Geometry cell tuple count", "[OrientationAnalysis][ComputeShapesFilter][TupleCount]") +{ + using FilterType = ComputeShapesFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Grid"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({4, 3, 1}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{1, 3, 4}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + // Equal counts remain valid in another Attribute Matrix with a different tuple shape. + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 0), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"Grid"}))); + args.insertOrAssign(FilterType::k_CellFeatureIdsArrayPath_Key, std::make_any(inputPath)); + auto* featureAMPtr = AttributeMatrix::Create(dataStructure, "Feature Data", ShapeType{2}, imageGeomPtr->getId()); + REQUIRE(featureAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Centroids", std::make_shared>(ShapeType{2}, ShapeType{3}, 0), featureAMPtr->getId()) != nullptr); + args.insertOrAssign(FilterType::k_CentroidsArrayPath_Key, std::make_any(DataPath({"Grid", "Feature Data", "Centroids"}))); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -12806); + REQUIRE(errorRef.message.find(inputPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", tuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Grid' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/OrientationAnalysis/test/NeighborOrientationCorrelationTest.cpp b/src/Plugins/OrientationAnalysis/test/NeighborOrientationCorrelationTest.cpp index e26de4baec..9a2c25a52c 100644 --- a/src/Plugins/OrientationAnalysis/test/NeighborOrientationCorrelationTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/NeighborOrientationCorrelationTest.cpp @@ -6,6 +6,7 @@ #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/DataStructure/NeighborList.hpp" #include "simplnx/DataStructure/StringArray.hpp" @@ -1133,3 +1134,116 @@ TEST_CASE("OrientationAnalysis::NeighborOrientationCorrelationFilter: Class 4 - VerifyAgainstSnapshot(dataStructure, before, {}); UnitTest::CheckArraysInheritTupleDims(dataStructure); } + +TEST_CASE("OrientationAnalysis::NeighborOrientationCorrelationFilter: Geometry cell tuple counts", "[OrientationAnalysis][NeighborOrientationCorrelationFilter][TupleCount]") +{ + // Cases 0-5 cover short, long, and matching inputs. Case 6 keeps matching + // inputs in separate matrices to check the repair boundary. + const int testCase = GENERATE(0, 1, 2, 3, 4, 5, 6); + CAPTURE(testCase); + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "TupleGeometry"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 2, 2}); + auto* cellDataPtr = AttributeMatrix::Create(dataStructure, "Cell Data", {2, 2, 2}, imageGeomPtr->getId()); + REQUIRE(cellDataPtr != nullptr); + imageGeomPtr->setCellData(*cellDataPtr); + + NeighborOrientationCorrelationFilter filter; + Arguments args = filter.getDefaultArguments(); + args.insertOrAssign(NeighborOrientationCorrelationFilter::k_ImageGeometryPath_Key, std::make_any(DataPath({"TupleGeometry"}))); + const usize commonCount = testCase == 0 ? 4 : (testCase == 1 ? 12 : 8); + const usize count0 = testCase == 3 ? 4 : commonCount; + auto* inputAM0Ptr = AttributeMatrix::Create(dataStructure, "Input0", {count0}); + REQUIRE(inputAM0Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "Confidence", {count0}, {1}, inputAM0Ptr->getId()) != nullptr); + args.insertOrAssign(NeighborOrientationCorrelationFilter::k_CorrelationArrayPath_Key, std::make_any(DataPath({"Input0", "Confidence"}))); + const usize count1 = testCase == 4 ? 4 : commonCount; + const std::string phasesParentName = testCase == 2 ? "Input0" : "Input1"; + auto* inputAM1Ptr = testCase == 2 ? inputAM0Ptr : AttributeMatrix::Create(dataStructure, phasesParentName, {count1}); + REQUIRE(inputAM1Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "Phases", {count1}, {1}, inputAM1Ptr->getId()) != nullptr); + args.insertOrAssign(NeighborOrientationCorrelationFilter::k_CellPhasesArrayPath_Key, std::make_any(DataPath({phasesParentName, "Phases"}))); + const usize count2 = testCase == 5 ? 4 : commonCount; + const std::string quatsParentName = testCase == 2 ? "Input0" : "Input2"; + auto* inputAM2Ptr = testCase == 2 ? inputAM0Ptr : AttributeMatrix::Create(dataStructure, quatsParentName, {count2}); + REQUIRE(inputAM2Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "Quats", {count2}, {4}, inputAM2Ptr->getId()) != nullptr); + args.insertOrAssign(NeighborOrientationCorrelationFilter::k_QuatsArrayPath_Key, std::make_any(DataPath({quatsParentName, "Quats"}))); + REQUIRE(UInt32Array::CreateWithStore>(dataStructure, "CrystalStructures", {2}, {1}) != nullptr); + args.insertOrAssign(NeighborOrientationCorrelationFilter::k_CrystalStructuresArrayPath_Key, std::make_any(DataPath({"CrystalStructures"}))); + args.insertOrAssign(NeighborOrientationCorrelationFilter::k_Level_Key, std::make_any(5)); + + const auto result = filter.preflight(dataStructure, args); + if(testCase == 2 || testCase == 6) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({"Input0", "Confidence"}))); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(args.value(NeighborOrientationCorrelationFilter::k_CellPhasesArrayPath_Key))); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(args.value(NeighborOrientationCorrelationFilter::k_QuatsArrayPath_Key))); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({"CrystalStructures"}))); + auto& confidence = dataStructure.getDataRefAs(DataPath({"Input0", "Confidence"})); + auto& phases = dataStructure.getDataRefAs(args.value(NeighborOrientationCorrelationFilter::k_CellPhasesArrayPath_Key)); + auto& quats = dataStructure.getDataRefAs(args.value(NeighborOrientationCorrelationFilter::k_QuatsArrayPath_Key)); + auto& crystalStructures = dataStructure.getDataRefAs(DataPath({"CrystalStructures"})); + confidence.fill(0.9F); + phases.fill(1); + quats.fill(0.0F); + crystalStructures.fill(ebsdlib::CrystalStructure::Cubic_High); + for(usize cell = 0; cell < 8; cell++) + { + quats[cell * 4 + 3] = 1.0F; + } + // At corner 0, neighbors 1, 2, and 4 form one identical phase-1 group. + // All similarity counts tie, so the final +Z neighbor (4) supplies the repair. + confidence[0] = 0.01F; + phases[0] = 0; + quats[2] = 1.0F; + quats[3] = 0.0F; + + auto* ignoredPtr = Int32Array::CreateWithStore>(dataStructure, "Ignored", {8}, {1}, inputAM0Ptr->getId()); + REQUIRE(ignoredPtr != nullptr); + for(usize cell = 0; cell < 8; cell++) + { + (*ignoredPtr)[cell] = static_cast(100 + cell); + } + args.insertOrAssign(NeighborOrientationCorrelationFilter::k_IgnoredDataArrayPaths_Key, std::make_any>(std::vector{DataPath({"Input0", "Ignored"})})); + + const auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + for(usize cell = 0; cell < 8; cell++) + { + CAPTURE(cell); + const bool externalBadCell = testCase == 6 && cell == 0; + CHECK(confidence[cell] == 0.9F); + CHECK(phases[cell] == (externalBadCell ? 0 : 1)); + CHECK(quats[cell * 4] == 0.0F); + CHECK(quats[cell * 4 + 1] == 0.0F); + CHECK(quats[cell * 4 + 2] == (externalBadCell ? 1.0F : 0.0F)); + CHECK(quats[cell * 4 + 3] == (externalBadCell ? 0.0F : 1.0F)); + CHECK((*ignoredPtr)[cell] == static_cast(100 + cell)); + } + UnitTest::CheckArraysInheritTupleDims(dataStructure); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + const std::vector inputPaths = {DataPath({"Input0", "Confidence"}), DataPath({"Input1", "Phases"}), DataPath({"Input2", "Quats"})}; + if(testCase >= 3) + { + CHECK(errorRef.code == -580093); + CHECK(errorRef.message.find(fmt::format("DataPath: {} | Tuple Count: 4", inputPaths[static_cast(testCase - 3)].toString())) != std::string::npos); + } + else + { + CHECK(errorRef.code == -580098); + CHECK(errorRef.message.find(inputPaths[0].toString()) != std::string::npos); + CHECK(errorRef.message.find(testCase == 1 ? "12 tuples" : "4 tuples") != std::string::npos); + CHECK(errorRef.message.find("8 cells") != std::string::npos); + CHECK(errorRef.message.find("TupleGeometry") != std::string::npos); + } + } +} diff --git a/src/Plugins/OrientationAnalysis/test/WriteINLFileTest.cpp b/src/Plugins/OrientationAnalysis/test/WriteINLFileTest.cpp index 3dca2faa2a..bac6cce61e 100644 --- a/src/Plugins/OrientationAnalysis/test/WriteINLFileTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/WriteINLFileTest.cpp @@ -7,6 +7,7 @@ #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/DataStructure/StringArray.hpp" #include "simplnx/Parameters/FileSystemPathParameter.hpp" @@ -214,3 +215,65 @@ TEST_CASE("OrientationAnalysis::WriteINLFileFilter: SIMPL Backwards Compatibilit } } } + +TEST_CASE("OrientationAnalysis::WriteINLFileFilter: Geometry cell tuple counts", "[OrientationAnalysis][WriteINLFileFilter][TupleCount]") +{ + // Cases: all short, all long, all matching outside Cell Data, then each input short. + const int testCase = GENERATE(0, 1, 2, 3, 4, 5); + CAPTURE(testCase); + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "TupleGeometry"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 2, 2}); + auto* cellDataPtr = AttributeMatrix::Create(dataStructure, "Cell Data", {2, 2, 2}, imageGeomPtr->getId()); + REQUIRE(cellDataPtr != nullptr); + imageGeomPtr->setCellData(*cellDataPtr); + + WriteINLFileFilter filter; + Arguments args = filter.getDefaultArguments(); + args.insertOrAssign(WriteINLFileFilter::k_ImageGeomPath_Key, std::make_any(DataPath({"TupleGeometry"}))); + const usize commonCount = testCase == 0 ? 4 : (testCase == 1 ? 12 : 8); + const usize count0 = testCase == 3 ? 4 : commonCount; + auto* inputAM0Ptr = AttributeMatrix::Create(dataStructure, "Input0", {count0}); + REQUIRE(inputAM0Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "FeatureIds", {count0}, {1}, inputAM0Ptr->getId()) != nullptr); + args.insertOrAssign(WriteINLFileFilter::k_FeatureIdsArrayPath_Key, std::make_any(DataPath({"Input0", "FeatureIds"}))); + const usize count1 = testCase == 4 ? 4 : commonCount; + auto* inputAM1Ptr = AttributeMatrix::Create(dataStructure, "Input1", {count1}); + REQUIRE(inputAM1Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "Phases", {count1}, {1}, inputAM1Ptr->getId()) != nullptr); + args.insertOrAssign(WriteINLFileFilter::k_CellPhasesArrayPath_Key, std::make_any(DataPath({"Input1", "Phases"}))); + const usize count2 = testCase == 5 ? 4 : commonCount; + auto* inputAM2Ptr = AttributeMatrix::Create(dataStructure, "Input2", {count2}); + REQUIRE(inputAM2Ptr != nullptr); + REQUIRE(DataArray::CreateWithStore>(dataStructure, "EulerAngles", {count2}, {3}, inputAM2Ptr->getId()) != nullptr); + args.insertOrAssign(WriteINLFileFilter::k_CellEulerAnglesArrayPath_Key, std::make_any(DataPath({"Input2", "EulerAngles"}))); + REQUIRE(UInt32Array::CreateWithStore>(dataStructure, "CrystalStructures", {2}, {1}) != nullptr); + args.insertOrAssign(WriteINLFileFilter::k_CrystalStructuresArrayPath_Key, std::make_any(DataPath({"CrystalStructures"}))); + REQUIRE(StringArray::CreateWithValues(dataStructure, "Materials", {2}, {"Unknown", "Phase1"}) != nullptr); + REQUIRE(Int32Array::CreateWithStore>(dataStructure, "NumFeatures", {2}, {1}) != nullptr); + args.insertOrAssign(WriteINLFileFilter::k_MaterialNameArrayPath_Key, std::make_any(DataPath({"Materials"}))); + args.insertOrAssign(WriteINLFileFilter::k_NumFeaturesArrayPath_Key, std::make_any(DataPath({"NumFeatures"}))); + args.insertOrAssign(WriteINLFileFilter::k_OutputFile_Key, std::make_any(fs::path(unit_test::k_BinaryTestOutputDir.view()) / "tuple-count.inl")); + + const auto result = filter.preflight(dataStructure, args); + if(testCase == 2) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + { + const auto& errorRef = result.outputActions.errors()[0]; + CHECK(errorRef.code == -78431); + const std::vector inputPaths = {DataPath({"Input0", "FeatureIds"}), DataPath({"Input1", "Phases"}), DataPath({"Input2", "EulerAngles"})}; + const usize inputIndex = testCase >= 3 ? static_cast(testCase - 3) : 0; + CHECK(errorRef.message.find(inputPaths[inputIndex].toString()) != std::string::npos); + CHECK(errorRef.message.find(testCase == 1 ? "12 tuples" : "4 tuples") != std::string::npos); + CHECK(errorRef.message.find("8 cells") != std::string::npos); + CHECK(errorRef.message.find("TupleGeometry") != std::string::npos); + } + } +} diff --git a/src/Plugins/SimplnxCore/docs/AddBadDataFilter.md b/src/Plugins/SimplnxCore/docs/AddBadDataFilter.md index c8095347ce..429f8c926b 100644 --- a/src/Plugins/SimplnxCore/docs/AddBadDataFilter.md +++ b/src/Plugins/SimplnxCore/docs/AddBadDataFilter.md @@ -14,7 +14,7 @@ Synthetic microstructures generated by reconstruction or packing pipelines have ### Random vs. Boundary Noise -The filter supports two independent noise modes; either, both, or neither can be enabled. +Enable **Add Random Noise**, **Add Boundary Noise**, or both. At least one noise mode must be enabled. - **Random Noise** (Poisson) -- bad cells are scattered uniformly throughout the volume. Models random non-indexed points caused by detector dropout, surface contamination, or low signal. - **Boundary Noise** -- bad cells are placed only on cells near a **Feature** boundary. Models the practical reality that EBSD signal is weakest right at grain boundaries, where the beam straddles two crystal orientations. @@ -44,6 +44,8 @@ This filter needs to know which cells are near a feature boundary. That informat - **Boundary Euclidean Distances** (integer array) -- produced by [Compute Euclidean Distance Map](ComputeEuclideanDistMapFilter.md), then cast to integer with [Convert Data Type](ConvertDataFilter.md). - **Image Geometry** containing cell-level data -- typically a synthetic structure from a packing or reconstruction pipeline. +**Boundary Euclidean Distances** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + % Auto generated parameter table will be inserted here ## License & Copyright diff --git a/src/Plugins/SimplnxCore/docs/AlignSectionsFeatureCentroidFilter.md b/src/Plugins/SimplnxCore/docs/AlignSectionsFeatureCentroidFilter.md index 0335189cd7..6a3ea5284f 100644 --- a/src/Plugins/SimplnxCore/docs/AlignSectionsFeatureCentroidFilter.md +++ b/src/Plugins/SimplnxCore/docs/AlignSectionsFeatureCentroidFilter.md @@ -24,10 +24,6 @@ Unlike the misorientation-based and mutual-information-based alignment methods, If *Use Reference Slice* is enabled, each section is shifted to align with a single fixed reference slice rather than its immediate neighbor. This prevents small errors from accumulating across many sections. -### Linear Background Subtraction - -Some combinations of sample geometry and internal features can cause the alignment to introduce a gradual "shear" across the sample. Enabling *Linear Background Subtraction* corrects for this by fitting a line to the X and Y shifts along the Z-direction and removing the linear trend. This effectively keeps the top and bottom sections of the sample fixed relative to each other. - ## Algorithm The algorithm proceeds in two phases: @@ -70,6 +66,8 @@ In previous versions a file would have been produced instead. If you wish to rec - **Mask** -- a boolean array marking valid cells, typically produced by a threshold operation such as [Multi-Threshold Objects](MultiThresholdObjectsFilter.md) applied to quality or confidence-index arrays. +**Mask Array** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + % Auto generated parameter table will be inserted here ## Example Pipelines diff --git a/src/Plugins/SimplnxCore/docs/ComputeBoundingBoxStatsFilter.md b/src/Plugins/SimplnxCore/docs/ComputeBoundingBoxStatsFilter.md index 165565b97c..5901b84026 100644 --- a/src/Plugins/SimplnxCore/docs/ComputeBoundingBoxStatsFilter.md +++ b/src/Plugins/SimplnxCore/docs/ComputeBoundingBoxStatsFilter.md @@ -32,6 +32,8 @@ Special operations occur for certain statistics if the supplied array is of type - **Unified Bounds Array** -- a 6-component bounds array produced by the *Unified Array* output option of [Compute Feature Bounding Boxes](ComputeFeatureBoundsFilter.md). +**Attribute Array to Compute Statistics** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + % Auto generated parameter table will be inserted here ## Example Pipelines diff --git a/src/Plugins/SimplnxCore/docs/ComputeEuclideanDistMapFilter.md b/src/Plugins/SimplnxCore/docs/ComputeEuclideanDistMapFilter.md index b83391d873..7f1fc22547 100644 --- a/src/Plugins/SimplnxCore/docs/ComputeEuclideanDistMapFilter.md +++ b/src/Plugins/SimplnxCore/docs/ComputeEuclideanDistMapFilter.md @@ -30,6 +30,8 @@ The following algorithm explains the process: - **Cell Feature Ids** -- the Feature that owns each Cell, produced by a segmentation filter such as [Segment Features (Scalar)](ScalarSegmentFeaturesFilter.md). +**Cell Feature Ids** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + ### Performance The bounded-memory scanline implementation is used for every out-of-core array and for most in-memory workloads. Boundary seeds are identified with rolling previous/current/next Z-slices of Feature IDs. On volumes without non-positive Feature IDs, exact Manhattan distances are computed with one forward and one backward sweep per enabled map. If non-positive Feature IDs are present, an exact layer-synchronous fallback preserves their behavior as non-traversable cells. Every store access is a bulk Z-slice transfer. diff --git a/src/Plugins/SimplnxCore/docs/ComputeFeatureCentroidsFilter.md b/src/Plugins/SimplnxCore/docs/ComputeFeatureCentroidsFilter.md index 22ba78d62f..a2ca6d66ce 100644 --- a/src/Plugins/SimplnxCore/docs/ComputeFeatureCentroidsFilter.md +++ b/src/Plugins/SimplnxCore/docs/ComputeFeatureCentroidsFilter.md @@ -10,6 +10,8 @@ This **Filter** calculates the *centroid* of each **Feature** by determining the A **Feature** with no **Cells** (an unused Feature Id) keeps a centroid of (0, 0, 0). +**Cell Feature Ids** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + ## Algorithm The algorithm iterates over all voxels, accumulating each voxel's XYZ coordinate (computed from its flat index, the geometry origin, and spacing) into a per-feature Kahan sum. After all voxels are processed, each feature's centroid is the accumulated sum divided by the voxel count. diff --git a/src/Plugins/SimplnxCore/docs/ComputeFeatureNeighborsFilter.md b/src/Plugins/SimplnxCore/docs/ComputeFeatureNeighborsFilter.md index 4232698c10..333ea907c4 100644 --- a/src/Plugins/SimplnxCore/docs/ComputeFeatureNeighborsFilter.md +++ b/src/Plugins/SimplnxCore/docs/ComputeFeatureNeighborsFilter.md @@ -23,7 +23,7 @@ The main feature-level outputs are: - **Neighbor List** -- for each feature, the list of *Feature Ids* of the features it touches. - **Number of Neighbors** -- for each feature, the integer count of its contiguous neighbors. Equivalent to the length of each *Neighbor List* entry. -- **Shared Surface Area List** -- for each pair of neighboring features, the number of cell-faces they share. This is in **cell-face units** (a dimensionless count), not physical area. To convert to physical area, multiply by the area of one cell face (which depends on the cell spacing and the orientation of the shared face). +- **Shared Surface Area List** -- the total shared boundary area for each neighboring Feature pair, in squared geometry length units. The calculation uses the cell spacing and the face orientation. Two optional outputs can also be stored: @@ -36,6 +36,8 @@ Two optional outputs can also be stored: This filter handles Image Geometries of all dimensions (0D/1D/2D/3D). Thus, it is up to the user to ensure spacing is set inline with intended behavior, specifically for Shared Surface Area List calculation. For more details see the Image Geometry section of the Geometry documentation (currently in the python docs). +**Cell Feature Ids** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + ## 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/ComputeFeatureSizesFilter.md b/src/Plugins/SimplnxCore/docs/ComputeFeatureSizesFilter.md index 193b2e2655..d9d6ee591c 100644 --- a/src/Plugins/SimplnxCore/docs/ComputeFeatureSizesFilter.md +++ b/src/Plugins/SimplnxCore/docs/ComputeFeatureSizesFilter.md @@ -87,6 +87,8 @@ To illustrate why this is think about an image geometry with the dimensions of ` - **Feature Ids** -- a per-**Cell** integer label array produced by a segmentation filter such as [Segment Features (Scalar)](ScalarSegmentFeaturesFilter.md). +**Cell Feature Ids** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + % Auto generated parameter table will be inserted here ## Example Pipelines diff --git a/src/Plugins/SimplnxCore/docs/ComputeMomentInvariants2DFilter.md b/src/Plugins/SimplnxCore/docs/ComputeMomentInvariants2DFilter.md index 6b7d675774..e52f2300dc 100644 --- a/src/Plugins/SimplnxCore/docs/ComputeMomentInvariants2DFilter.md +++ b/src/Plugins/SimplnxCore/docs/ComputeMomentInvariants2DFilter.md @@ -25,6 +25,8 @@ The figures below, from [1], can help classify objects by their Omega values. An - **Cell Feature Ids** -- the Feature that owns each Cell, produced by a segmentation filter such as [Segment Features (Scalar)](ScalarSegmentFeaturesFilter.md). - **Feature Rect** -- the min/max XY pixel coordinates (bounding corners) of each Feature, produced by [Compute Feature Corners](ComputeFeatureRectFilter.md). +**Cell Feature Ids** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + % Auto generated parameter table will be inserted here # Citations diff --git a/src/Plugins/SimplnxCore/docs/ComputeSurfaceFeaturesFilter.md b/src/Plugins/SimplnxCore/docs/ComputeSurfaceFeaturesFilter.md index 985aa69f56..83d69c9b2e 100644 --- a/src/Plugins/SimplnxCore/docs/ComputeSurfaceFeaturesFilter.md +++ b/src/Plugins/SimplnxCore/docs/ComputeSurfaceFeaturesFilter.md @@ -38,6 +38,8 @@ If the structure/data is actually 2D, then the dimension that is planar is not c - **Cell Feature Ids** -- produced by a segmentation filter such as [Segment Features (Misorientation)](../OrientationAnalysis/EBSDSegmentFeaturesFilter.md) or [Segment Features (Scalar)](ScalarSegmentFeaturesFilter.md). +**Cell Feature Ids** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + % Auto generated parameter table will be inserted here ## Example Pipelines diff --git a/src/Plugins/SimplnxCore/docs/ErodeDilateMaskFilter.md b/src/Plugins/SimplnxCore/docs/ErodeDilateMaskFilter.md index ad7b81cdcf..b389a2b3b3 100644 --- a/src/Plugins/SimplnxCore/docs/ErodeDilateMaskFilter.md +++ b/src/Plugins/SimplnxCore/docs/ErodeDilateMaskFilter.md @@ -40,6 +40,8 @@ Chaining an erode and a dilate pass (with equal iteration counts) performs the t - **Mask Array** -- a boolean cell-level array, typically produced by [Multi-Threshold Objects](MultiThresholdObjectsFilter.md) applied to an EBSD confidence index, image quality, or similar scalar. +**Mask Array Path** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + ## Algorithm This filter performs iterative morphological erosion or dilation directly on a boolean mask array within an ImageGeom grid. Unlike the Erode/Dilate Bad Data filter, this filter operates only on the mask and does not propagate changes to sibling data arrays. diff --git a/src/Plugins/SimplnxCore/docs/ExtractFeatureBoundaries2DFilter.md b/src/Plugins/SimplnxCore/docs/ExtractFeatureBoundaries2DFilter.md index 3b8dd4d2dc..cc67bb3f50 100644 --- a/src/Plugins/SimplnxCore/docs/ExtractFeatureBoundaries2DFilter.md +++ b/src/Plugins/SimplnxCore/docs/ExtractFeatureBoundaries2DFilter.md @@ -43,6 +43,8 @@ After edge creation, duplicate vertices are eliminated to ensure a clean, connec - **Image Geometry** -- a 2D **Image Geometry** (Z dimension of 1) holding the Feature IDs. - **Feature IDs** -- a per-cell integer array. It may be a true segmentation produced by [Segment Features (Scalar)](ScalarSegmentFeaturesFilter.md), or any per-cell integer/mask array produced by a thresholding filter such as [Multi-Threshold Objects](MultiThresholdObjectsFilter.md). +**Feature Ids** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + % Auto generated parameter table will be inserted here ## Example Pipelines diff --git a/src/Plugins/SimplnxCore/docs/FlyingEdges3DFilter.md b/src/Plugins/SimplnxCore/docs/FlyingEdges3DFilter.md index b690144853..bd3fcdd3ea 100644 --- a/src/Plugins/SimplnxCore/docs/FlyingEdges3DFilter.md +++ b/src/Plugins/SimplnxCore/docs/FlyingEdges3DFilter.md @@ -30,6 +30,8 @@ Use this filter to extract a smooth surface from continuous scalar data (CT dens ![A 3D contour surface extracted from an Image Geometry at the chosen Contour Value.](Images/FlyingEdges3D_1.png) +**Data Array to Contour** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + % Auto generated parameter table will be inserted here ## License & Copyright diff --git a/src/Plugins/SimplnxCore/docs/IdentifySampleFilter.md b/src/Plugins/SimplnxCore/docs/IdentifySampleFilter.md index dfa14deb64..7164db69be 100644 --- a/src/Plugins/SimplnxCore/docs/IdentifySampleFilter.md +++ b/src/Plugins/SimplnxCore/docs/IdentifySampleFilter.md @@ -50,6 +50,8 @@ When *Process Data Slice-By-Slice* is enabled, the *Slice-By-Slice Plane* parame - **Good Voxels Mask** -- a boolean array marking cells as *good* or *bad*, typically produced by [Multi-Threshold Objects](MultiThresholdObjectsFilter.md) applied to EBSD confidence index, image quality, or a similar scalar. +**Mask Array** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + % Auto generated parameter table will be inserted here ## Example Pipelines diff --git a/src/Plugins/SimplnxCore/docs/InitializeImageGeomCellDataFilter.md b/src/Plugins/SimplnxCore/docs/InitializeImageGeomCellDataFilter.md index d170bde09e..6597998fb3 100644 --- a/src/Plugins/SimplnxCore/docs/InitializeImageGeomCellDataFilter.md +++ b/src/Plugins/SimplnxCore/docs/InitializeImageGeomCellDataFilter.md @@ -37,7 +37,7 @@ For boolean arrays in random modes, each cell is set to a uniformly-chosen true/ ### Required Input Sources - **Input Image Geometry** -- the geometry whose cell data will be modified in-place. Typically produced by [Create Image Geometry](CreateImageGeometryFilter.md), [Read Image Stack](../ImageProcessing/ReadImageStackFilter.md), or an EBSD reader. -- **Cell Attribute Arrays to Initialize** -- one or more cell-level arrays already attached to the geometry's Cell Attribute Matrix. +- **Cell Arrays** -- one or more arrays with one tuple per cell of the selected Image Geometry. The arrays can be in any Attribute Matrix. % Auto generated parameter table will be inserted here diff --git a/src/Plugins/SimplnxCore/docs/M3CSurfaceMeshingFilter.md b/src/Plugins/SimplnxCore/docs/M3CSurfaceMeshingFilter.md index c8a9805141..eaa1466c0d 100644 --- a/src/Plugins/SimplnxCore/docs/M3CSurfaceMeshingFilter.md +++ b/src/Plugins/SimplnxCore/docs/M3CSurfaceMeshingFilter.md @@ -185,6 +185,8 @@ This implementation ports the slice-by-slice algorithm of the legacy DREAM.3D `M based on the algorithm of Wu & Sullivan, "Multiple material marching cubes algorithm," *International Journal for Numerical Methods in Engineering*, 58(2):189–207, 2003. +**Cell Feature Ids** and **Cell Attribute Arrays to Transfer** must each have one tuple per cell of the selected geometry. They can be in any Attribute Matrix. + ## Comparison of Surface Meshing Filters DREAM3D-NX provides three **Filters** that convert a segmented grid into a multi-material triangle surface mesh. All three produce the same output data model (a **Triangle Geometry** with **Face Labels** and **Node Types**), so they are interchangeable inputs to downstream mesh **Filters**; they differ in how the triangles are generated and therefore in mesh smoothness, triangle count, and performance. diff --git a/src/Plugins/SimplnxCore/docs/NearestPointFuseRegularGridsFilter.md b/src/Plugins/SimplnxCore/docs/NearestPointFuseRegularGridsFilter.md index 0079726d98..53f5646a23 100644 --- a/src/Plugins/SimplnxCore/docs/NearestPointFuseRegularGridsFilter.md +++ b/src/Plugins/SimplnxCore/docs/NearestPointFuseRegularGridsFilter.md @@ -27,6 +27,8 @@ The *Sampling* geometry is left unchanged. The *Reference* geometry keeps its ow - **Reference Image Geometry** and **Sampling Image Geometry** -- two **Image Geometries** (each with a **Cell Attribute Matrix**), typically created by separate import or resampling steps. +**Sampling Cell Attribute Matrix** must have one tuple per cell of **Sampling Image Geometry**. **Reference Cell Attribute Matrix** must have one tuple per cell of **Reference Image Geometry**. + ## Algorithm The filter maps each reference-grid coordinate to the containing sampling-grid **Cell** without interpolation. For in-memory arrays, the direct implementation preserves the original cell-by-cell traversal. When either a sampled numeric or Boolean **Data Array** or its newly created reference counterpart is out-of-core, the scanline implementation is selected. It calculates the source index for each X, Y, and Z coordinate once, reads only the required sampling row into a bounded buffer, and writes each completed reference row in one bulk operation. Its working memory is proportional to the largest row and the three grid axes, rather than the number of **Cells**. Only numeric and Boolean **Data Arrays** are copied; strings and neighbor lists are skipped. diff --git a/src/Plugins/SimplnxCore/docs/PartitionGeometryFilter.md b/src/Plugins/SimplnxCore/docs/PartitionGeometryFilter.md index 2a288567e6..fb53eb655e 100644 --- a/src/Plugins/SimplnxCore/docs/PartitionGeometryFilter.md +++ b/src/Plugins/SimplnxCore/docs/PartitionGeometryFilter.md @@ -121,6 +121,8 @@ On all partitioning modes, a vertex mask can be used with a vertex-based input g - **Partition Grid Geometry** (only when *Partitioning Mode* is *Existing Partition Grid*) -- an Image Geometry produced by a prior run of this filter. - **Vertex Mask** (optional, for vertex-based input geometries) -- a boolean cell-level array on the input geometry's vertices. +**Input Geometry Cell Attribute Matrix** must have one tuple per cell of **Input Geometry to Partition**. This requirement applies to Image and Rectilinear Grid geometries in every partitioning mode. + % Auto generated parameter table will be inserted here ## Example Pipelines diff --git a/src/Plugins/SimplnxCore/docs/ReplaceElementAttributesWithNeighborValuesFilter.md b/src/Plugins/SimplnxCore/docs/ReplaceElementAttributesWithNeighborValuesFilter.md index 0aafdce33d..77795f7f95 100644 --- a/src/Plugins/SimplnxCore/docs/ReplaceElementAttributesWithNeighborValuesFilter.md +++ b/src/Plugins/SimplnxCore/docs/ReplaceElementAttributesWithNeighborValuesFilter.md @@ -80,6 +80,8 @@ The black pixels in the original are unindexed cells (Error > 0) and are filled - **Selected Scalar Array** -- any single-component cell-level scalar suitable for thresholding. Common sources: confidence/quality arrays from [Read H5EBSD](../OrientationAnalysis/ReadH5EbsdFilter.md), [Read CTF Data](../OrientationAnalysis/ReadCtfDataFilter.md), or [Read ANG Data](../OrientationAnalysis/ReadAngDataFilter.md), or any computed per-cell scalar. +**Input Comparison Array** must have one tuple per cell of the selected geometry. When **Loop Until Gone** is enabled, select a comparison array in the geometry's Cell Data Attribute Matrix. Otherwise, another Attribute Matrix is allowed. + ## Algorithm This filter iteratively replaces voxel data that fails a user-defined threshold comparison with data from the best-scoring face neighbor. diff --git a/src/Plugins/SimplnxCore/docs/RequireMinNumNeighborsFilter.md b/src/Plugins/SimplnxCore/docs/RequireMinNumNeighborsFilter.md index 6cfc99a8b4..0b635c2de4 100644 --- a/src/Plugins/SimplnxCore/docs/RequireMinNumNeighborsFilter.md +++ b/src/Plugins/SimplnxCore/docs/RequireMinNumNeighborsFilter.md @@ -47,7 +47,7 @@ This **Filter** operates in place and does not create a new geometry or output a ## Validation and Failure Conditions - Preflight fails with `-55575` when *Cell Feature Ids* is not in the selected **Image Geometry**'s Cell Attribute Matrix. -- Preflight fails with `-55571` when *Cell Feature Ids* does not contain exactly one tuple per **Image Geometry** cell. +- Preflight fails with `-59571` when *Cell Feature Ids* does not contain exactly one tuple per **Image Geometry** cell. - Preflight fails with `-55573` when an array in *Cell Arrays to Ignore* is not in that Cell Attribute Matrix. - Preflight fails with `-55574` when the parent of *Number of Neighbors* is not an Attribute Matrix directly under the selected **Image Geometry**, or is its Cell Attribute Matrix. - Preflight fails with `-252` when *Number of Neighbors* and the enabled *Feature Phases* array do not have matching tuple counts. diff --git a/src/Plugins/SimplnxCore/docs/ScalarSegmentFeaturesFilter.md b/src/Plugins/SimplnxCore/docs/ScalarSegmentFeaturesFilter.md index 217423ca81..afc57f56b1 100644 --- a/src/Plugins/SimplnxCore/docs/ScalarSegmentFeaturesFilter.md +++ b/src/Plugins/SimplnxCore/docs/ScalarSegmentFeaturesFilter.md @@ -89,6 +89,8 @@ If the input data represents a **periodic** structure (i.e., features are allowe - **Input Scalar Array** -- any single-component cell-level scalar array suitable for thresholding. Common sources include image readers such as [Read Image Stack](../ImageProcessing/ReadImageStackFilter.md), phase or quality arrays from EBSD readers such as [Read H5EBSD](../OrientationAnalysis/ReadH5EbsdFilter.md), or any computed per-cell scalar from an earlier filter in the pipeline. - **Mask Array** (optional) -- a boolean array marking valid cells, typically produced by [Multi-Threshold Objects](MultiThresholdObjectsFilter.md). +**Scalar Array to Segment** must have one tuple per cell of the selected geometry. When **Use Mask Array** is enabled, **Cell Mask Array** must also have one tuple per cell. They can be in any Attribute Matrix. + % Auto generated parameter table will be inserted here ## Example Pipelines diff --git a/src/Plugins/SimplnxCore/docs/WriteAbaqusHexahedronFilter.md b/src/Plugins/SimplnxCore/docs/WriteAbaqusHexahedronFilter.md index 094cabfe19..3c9843e349 100644 --- a/src/Plugins/SimplnxCore/docs/WriteAbaqusHexahedronFilter.md +++ b/src/Plugins/SimplnxCore/docs/WriteAbaqusHexahedronFilter.md @@ -43,6 +43,8 @@ When *Use Reduced Integration Elements* is enabled, the `_sects.inp` file includ The maximum feature ID is limited to 100,000,000. Larger IDs cause an error during execution; renumber the features to be contiguous. The node and element label limit is 999,999,999. Mesh counts above this limit cause a preflight error. +**Cell Feature Ids** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + ### Example Output The headers in the snippets below read `DREAM3DLib Version 5.1.566`; these version strings are illustrative and will reflect the actual version that wrote the file. The filter is based on a Python script developed by Matthew W. Priddy (Georgia Tech, early 2015). diff --git a/src/Plugins/SimplnxCore/docs/WriteAvizoRectilinearCoordinateFilter.md b/src/Plugins/SimplnxCore/docs/WriteAvizoRectilinearCoordinateFilter.md index 8590261f7b..d43b5ec97b 100644 --- a/src/Plugins/SimplnxCore/docs/WriteAvizoRectilinearCoordinateFilter.md +++ b/src/Plugins/SimplnxCore/docs/WriteAvizoRectilinearCoordinateFilter.md @@ -41,6 +41,8 @@ The *Write Binary File* parameter selects the on-disk encoding: *false* (the def - **Image Geometry** -- the geometry whose dimensions and coordinates are written. - **Cell Feature Ids** -- an Int32 per-cell label array, produced by a segmentation filter such as [Segment Features (Scalar)](ScalarSegmentFeaturesFilter.md). +**Cell Feature Ids** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + % Auto generated parameter table will be inserted here ## Example Pipelines diff --git a/src/Plugins/SimplnxCore/docs/WriteAvizoUniformCoordinateFilter.md b/src/Plugins/SimplnxCore/docs/WriteAvizoUniformCoordinateFilter.md index 1301d5a552..33665fb8cc 100644 --- a/src/Plugins/SimplnxCore/docs/WriteAvizoUniformCoordinateFilter.md +++ b/src/Plugins/SimplnxCore/docs/WriteAvizoUniformCoordinateFilter.md @@ -43,6 +43,8 @@ The *Write Binary File* parameter selects the on-disk encoding: *false* (the def - **Image Geometry** -- the geometry whose dimensions and bounding box are written. - **Cell Feature Ids** -- an Int32 per-cell label array, produced by a segmentation filter such as [Segment Features (Scalar)](ScalarSegmentFeaturesFilter.md). +**Cell Feature Ids** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + % Auto generated parameter table will be inserted here ## Example Pipelines diff --git a/src/Plugins/SimplnxCore/docs/WriteLosAlamosFFTFilter.md b/src/Plugins/SimplnxCore/docs/WriteLosAlamosFFTFilter.md index e62a7fe75b..a1c8a2086a 100644 --- a/src/Plugins/SimplnxCore/docs/WriteLosAlamosFFTFilter.md +++ b/src/Plugins/SimplnxCore/docs/WriteLosAlamosFFTFilter.md @@ -25,6 +25,8 @@ The Euler angles are in degrees. X, Y, Z are integer indices into the **Image** - **Cell Feature Ids** -- produced by a segmentation filter such as [Segment Features (Scalar)](ScalarSegmentFeaturesFilter.md). - **Cell Phases** -- a per-cell phase index, read alongside the Euler angles from the EBSD readers above. +**Cell Feature Ids**, **Cell Euler Angles** and **Cell Phases** must each have one tuple per cell of the selected geometry. They can be in any Attribute Matrix. + ### Example Output The output file (columns: Phi1, Phi, Phi2, X, Y, Z, Feature_ID, Phase_ID): diff --git a/src/Plugins/SimplnxCore/docs/WriteSPParksSitesFilter.md b/src/Plugins/SimplnxCore/docs/WriteSPParksSitesFilter.md index 7cc8629520..6212af82e2 100644 --- a/src/Plugins/SimplnxCore/docs/WriteSPParksSitesFilter.md +++ b/src/Plugins/SimplnxCore/docs/WriteSPParksSitesFilter.md @@ -26,6 +26,8 @@ More information can be found in the SPPARKS documentation for [read_sites](http - **Image Geometry** -- the voxel grid whose cells become SPPARKS sites. - **Cell Feature Ids** -- produced by a segmentation filter such as [Segment Features (Scalar)](ScalarSegmentFeaturesFilter.md). The Feature Id at each cell becomes that site's value. +**Cell Feature Ids** must have one tuple per cell of the selected geometry. It can be in any Attribute Matrix. + ### Example Output ```text diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/AddBadDataFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/AddBadDataFilter.cpp index 11d03d6425..4f87fd976d 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/AddBadDataFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/AddBadDataFilter.cpp @@ -12,6 +12,7 @@ #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/FilterUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" @@ -108,6 +109,12 @@ IFilter::PreflightResult AddBadDataFilter::preflightImpl(const DataStructure& da auto pImageGeometryPathValue = filterArgs.value(k_ImageGeometryPath_Key); auto pSeedArrayNameValue = filterArgs.value(k_SeedArrayName_Key); + auto tupleCountResult = ValidateCellTupleCount(dataStructure, pImageGeometryPathValue, {pGBEuclideanDistancesArrayPathValue}, -76238); + if(tupleCountResult.invalid()) + { + return {ConvertResultTo(std::move(tupleCountResult), {})}; + } + PreflightResult preflightResult; nx::core::Result resultOutputActions; std::vector preflightUpdatedValues; diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/M3CSurfaceMeshing.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/M3CSurfaceMeshing.cpp index 37afc98fd3..a6a34e83be 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/M3CSurfaceMeshing.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/Algorithms/M3CSurfaceMeshing.cpp @@ -3256,11 +3256,6 @@ Result<> M3CSurfaceMeshing::runOutOfCore(const std::vector& dispa return MakeErrorResult(-90546, "M3C out-of-core input dimensions are zero or overflow the cell count."); } const usize cellCount = dims[0] * dims[1] * dims[2]; - if(featureIdsStore.getNumberOfTuples() != cellCount) - { - return MakeErrorResult(-90547, "M3C out-of-core FeatureIds tuple count does not match the Image Geometry."); - } - // First bounded pass preserves initialize_micro's zero-feature renumbering // without retaining a second copy of the cell data. Large bounded batches // are heap-backed so this path remains within the default Windows stack. diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/AlignSectionsFeatureCentroidFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/AlignSectionsFeatureCentroidFilter.cpp index 7675c5acc0..53b6550dc7 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/AlignSectionsFeatureCentroidFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/AlignSectionsFeatureCentroidFilter.cpp @@ -12,6 +12,7 @@ #include "simplnx/Parameters/DataObjectNameParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/FilterUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" @@ -27,7 +28,6 @@ constexpr nx::core::int32 k_InputRepresentationTypeError = -68001; constexpr nx::core::int32 k_OutputRepresentationTypeError = -68002; constexpr nx::core::int32 k_InputComponentDimensionError = -68003; constexpr nx::core::int32 k_InputComponentCountError = -68004; -constexpr nx::core::int32 k_InconsistentTupleCount = -68063; constexpr nx::core::int32 k_OutOfRangeReferenceSliceValue = -68064; } // namespace @@ -143,21 +143,17 @@ IFilter::PreflightResult AlignSectionsFeatureCentroidFilter::preflightImpl(const auto pStoreAlignmentShifts = filterArgs.value(k_StoreAlignmentShifts_Key); - nx::core::Result resultOutputActions; - - if(pReferenceSliceValue < 0) + auto tupleCountResult = ValidateCellTupleCount(dataStructure, inputImageGeometry, {pGoodVoxelsArrayPath}, -68071); + if(tupleCountResult.invalid()) { - return MakePreflightErrorResult(k_OutOfRangeReferenceSliceValue, fmt::format("Reference Slice value ({}) must be ZERO or greater.", pReferenceSliceValue)); + return {ConvertResultTo(std::move(tupleCountResult), {})}; } - std::vector dataPaths; - dataPaths.push_back(pGoodVoxelsArrayPath); + nx::core::Result resultOutputActions; - // Ensure all DataArrays have the same number of Tuples - auto tupleValidityCheck = dataStructure.validateNumberOfTuples(dataPaths); - if(!tupleValidityCheck) + if(pReferenceSliceValue < 0) { - return MakePreflightErrorResult(k_InconsistentTupleCount, fmt::format("The following DataArrays all must have equal number of tuples but this was not satisfied.\n{}", tupleValidityCheck.error())); + return MakePreflightErrorResult(k_OutOfRangeReferenceSliceValue, fmt::format("Reference Slice value ({}) must be ZERO or greater.", pReferenceSliceValue)); } // Handle Array Creation diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeBoundingBoxStatsFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeBoundingBoxStatsFilter.cpp index b3ee58cf2f..bfc855a58d 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeBoundingBoxStatsFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeBoundingBoxStatsFilter.cpp @@ -16,6 +16,7 @@ #include "simplnx/Parameters/BoolParameter.hpp" #include "simplnx/Parameters/DataObjectNameParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/FilterUtilities.hpp" using namespace nx::core; @@ -250,6 +251,12 @@ IFilter::PreflightResult ComputeBoundingBoxStatsFilter::preflightImpl(const Data auto pCreateNewAMValue = filterArgs.value(k_CreateNewAM_Key); + auto tupleCountResult = ValidateCellTupleCount(dataStructure, pGeometryPathValue, {pInputArrayPathValue}, -59204); + if(tupleCountResult.invalid()) + { + return {ConvertResultTo(std::move(tupleCountResult), {})}; + } + FloatVec3 imageGeomSpacing = dataStructure.getDataAs(pGeometryPathValue)->getSpacing(); if(std::count(imageGeomSpacing.begin(), imageGeomSpacing.end(), 0.0f) != 0) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeEuclideanDistMapFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeEuclideanDistMapFilter.cpp index 7ba966d4db..364ae07b1b 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeEuclideanDistMapFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeEuclideanDistMapFilter.cpp @@ -10,6 +10,7 @@ #include "simplnx/Parameters/BoolParameter.hpp" #include "simplnx/Parameters/DataObjectNameParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" @@ -109,6 +110,12 @@ IFilter::PreflightResult ComputeEuclideanDistMapFilter::preflightImpl(const Data auto pCalculateDistToQuadPoints = filterArgs.value(k_DoQuadPoints_Key); auto pFeatureIdsArrayPathValue = filterArgs.value(k_CellFeatureIdsArrayPath_Key); + auto tupleCountResult = ValidateCellTupleCount(dataStructure, filterArgs.value(k_SelectedImageGeometryPath_Key), {pFeatureIdsArrayPathValue}, -12805); + if(tupleCountResult.invalid()) + { + return {ConvertResultTo(std::move(tupleCountResult), {})}; + } + DataPath parentGroup = pFeatureIdsArrayPathValue.getParent(); PreflightResult preflightResult; diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeatureCentroidsFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeatureCentroidsFilter.cpp index adecbe4cb5..4d8d062998 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeatureCentroidsFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeatureCentroidsFilter.cpp @@ -11,6 +11,7 @@ #include "simplnx/Parameters/BoolParameter.hpp" #include "simplnx/Parameters/DataObjectNameParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" @@ -94,6 +95,12 @@ IFilter::PreflightResult ComputeFeatureCentroidsFilter::preflightImpl(const Data auto pCentroidsArrayName = filterArgs.value(k_CentroidsArrayName_Key); DataPath pCentroidsArrayPath = featureAttrMatrixPath.createChildPath(pCentroidsArrayName); + auto tupleCountResult = ValidateCellTupleCount(dataStructure, filterArgs.value(k_SelectedImageGeometryPath_Key), {pFeatureIdsArrayPathValue}, -12701); + if(tupleCountResult.invalid()) + { + return {ConvertResultTo(std::move(tupleCountResult), {})}; + } + PreflightResult preflightResult; nx::core::Result resultOutputActions; diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeatureNeighborsFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeatureNeighborsFilter.cpp index ce9000799e..06a674c541 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeatureNeighborsFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeatureNeighborsFilter.cpp @@ -12,6 +12,7 @@ #include "simplnx/Parameters/DataGroupSelectionParameter.hpp" #include "simplnx/Parameters/DataObjectNameParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" #include @@ -114,6 +115,12 @@ IFilter::PreflightResult ComputeFeatureNeighborsFilter::preflightImpl(const Data auto surfaceFeaturesName = filterArgs.value(k_SurfaceFeaturesName_Key); auto featureAttrMatrixPath = filterArgs.value(k_CellFeaturesPath_Key); + auto tupleCountResult = ValidateCellTupleCount(dataStructure, imageGeomPath, {featureIdsPath}, -24501); + if(tupleCountResult.invalid()) + { + return {ConvertResultTo(std::move(tupleCountResult), {})}; + } + DataPath boundaryCellsPath = featureIdsPath.replaceName(boundaryCellsName); DataPath numNeighborsPath = featureAttrMatrixPath.createChildPath(numNeighborsName); DataPath neighborListPath = featureAttrMatrixPath.createChildPath(neighborListName); diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeatureSizesFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeatureSizesFilter.cpp index b0747177ab..e8a016045a 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeatureSizesFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeFeatureSizesFilter.cpp @@ -10,6 +10,7 @@ #include "simplnx/Parameters/DataGroupSelectionParameter.hpp" #include "simplnx/Parameters/DataObjectNameParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" namespace nx::core @@ -100,6 +101,12 @@ IFilter::PreflightResult ComputeFeatureSizesFilter::preflightImpl(const DataStru auto volumesName = filterArgs.value(k_VolumesName_Key); auto equivalentDiametersName = filterArgs.value(k_EquivalentDiametersName_Key); auto numElementsName = filterArgs.value(k_NumElementsName_Key); + auto tupleCountResult = ValidateCellTupleCount(dataStructure, geometryPath, {featureIdsPath}, -74771); + if(tupleCountResult.invalid()) + { + return {ConvertResultTo(std::move(tupleCountResult), {})}; + } + DataPath volumesPath = featureAttributeMatrixPath.createChildPath(volumesName); DataPath equivalentDiametersPath = featureAttributeMatrixPath.createChildPath(equivalentDiametersName); DataPath numElementsPath = featureAttributeMatrixPath.createChildPath(numElementsName); diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeMomentInvariants2DFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeMomentInvariants2DFilter.cpp index 847433a4ef..3401eec541 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeMomentInvariants2DFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeMomentInvariants2DFilter.cpp @@ -11,6 +11,7 @@ #include "simplnx/Parameters/DataGroupSelectionParameter.hpp" #include "simplnx/Parameters/DataObjectNameParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" @@ -99,6 +100,12 @@ IFilter::PreflightResult ComputeMomentInvariants2DFilter::preflightImpl(const Da auto pOmega2ArrayNameValue = filterArgs.value(k_Omega2ArrayName_Key); auto pSaveCentralMomentsValue = filterArgs.value(k_SaveCentralMoments_Key); + auto tupleCountResult = ValidateCellTupleCount(dataStructure, pGeometryPathValue, {pFeatureIdsArrayPathValue}, -9511); + if(tupleCountResult.invalid()) + { + return {ConvertResultTo(std::move(tupleCountResult), {})}; + } + const DataPath omega1Path = pFeatureAttributeMatrixPathValue.createChildPath(pOmega1ArrayNameValue); const DataPath omega2Path = pFeatureAttributeMatrixPathValue.createChildPath(pOmega2ArrayNameValue); diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeSurfaceFeaturesFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeSurfaceFeaturesFilter.cpp index a674b1e09a..39adc9eb96 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeSurfaceFeaturesFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ComputeSurfaceFeaturesFilter.cpp @@ -92,6 +92,12 @@ IFilter::PreflightResult ComputeSurfaceFeaturesFilter::preflightImpl(const DataS auto pCellFeaturesAttributeMatrixPathValue = filterArgs.value(k_CellFeatureAttributeMatrixPath_Key); auto pSurfaceFeaturesArrayNameValue = filterArgs.value(k_SurfaceFeaturesArrayName_Key); + auto tupleCountResult = ValidateCellTupleCount(dataStructure, pFeatureGeometryPathValue, {filterArgs.value(k_CellFeatureIdsArrayPath_Key)}, -1099); + if(tupleCountResult.invalid()) + { + return {ConvertResultTo(std::move(tupleCountResult), {})}; + } + const auto& featureGeometry = dataStructure.getDataRefAs(pFeatureGeometryPathValue); usize geometryDimensionality = featureGeometry.getDimensionality(); if(geometryDimensionality != 3 && geometryDimensionality != 2) diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateMaskFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateMaskFilter.cpp index 6433694243..36856c88a7 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateMaskFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ErodeDilateMaskFilter.cpp @@ -9,6 +9,7 @@ #include "simplnx/Parameters/DataGroupSelectionParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/FilterUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" @@ -93,6 +94,12 @@ IFilter::PreflightResult ErodeDilateMaskFilter::preflightImpl(const DataStructur auto pMaskArrayPathValue = filterArgs.value(k_MaskArrayPath_Key); auto pNumIterationsValue = filterArgs.value(k_NumIterations_Key); + auto tupleCountResult = ValidateCellTupleCount(dataStructure, filterArgs.value(k_SelectedImageGeometryPath_Key), {pMaskArrayPathValue}, -14702); + if(tupleCountResult.invalid()) + { + return {ConvertResultTo(std::move(tupleCountResult), {})}; + } + // The erode/dilate loop runs NumIterations times, so a value below 1 performs no passes and returns // the mask untouched. DREAM3D 6.5.171 rejected that in dataCheck rather than silently no-op'ing, and // this guard restores that behavior so a mistyped iteration count is reported to the user. diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ExtractFeatureBoundaries2DFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ExtractFeatureBoundaries2DFilter.cpp index dc77754c0c..4035c8a9e6 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ExtractFeatureBoundaries2DFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ExtractFeatureBoundaries2DFilter.cpp @@ -13,6 +13,7 @@ #include "simplnx/Parameters/DataGroupCreationParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" using namespace nx::core; @@ -104,6 +105,11 @@ IFilter::PreflightResult ExtractFeatureBoundaries2DFilter::preflightImpl(const D auto pCustomZValue = filterArgs.value(k_CustomZValue_Key); auto pExtractVirtualSampleEdges = filterArgs.value(k_ExtractVirtualSampleEdges_Key); + if(auto result = ValidateCellTupleCount(dataStructure, pInputImageGeometryPathValue, {pFeatureIdsArrayPathValue}, -1019); result.invalid()) + { + return {ConvertResultTo(std::move(result), {})}; + } + PreflightResult preflightResult; nx::core::Result resultOutputActions; std::vector preflightUpdatedValues; diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/FlyingEdges3DFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/FlyingEdges3DFilter.cpp index c6500387df..2f5792df87 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/FlyingEdges3DFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/FlyingEdges3DFilter.cpp @@ -11,6 +11,7 @@ #include "simplnx/Parameters/DataObjectNameParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" using namespace nx::core; namespace @@ -90,6 +91,12 @@ IFilter::PreflightResult FlyingEdges3DFilter::preflightImpl(const DataStructure& { auto pImageGeomPath = filterArgs.value(k_SelectedImageGeometryPath_Key); auto pTriangleGeomName = filterArgs.value(k_CreatedTriangleGeometryPath_Key); + auto pContouringArrayPath = filterArgs.value(k_SelectedDataArrayPath_Key); + + if(auto result = ValidateCellTupleCount(dataStructure, pImageGeomPath, {pContouringArrayPath}, -10510); result.invalid()) + { + return {ConvertResultTo(std::move(result), {})}; + } nx::core::Result resultOutputActions; std::vector preflightUpdatedValues; diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/IdentifySampleFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/IdentifySampleFilter.cpp index 0c705e6b60..849c05bc32 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/IdentifySampleFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/IdentifySampleFilter.cpp @@ -8,6 +8,7 @@ #include "simplnx/Parameters/ChoicesParameter.hpp" #include "simplnx/Parameters/DataGroupSelectionParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/FilterUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" @@ -91,6 +92,12 @@ IFilter::PreflightResult IdentifySampleFilter::preflightImpl(const DataStructure const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { const auto goodVoxelsArrayPath = filterArgs.value(k_MaskArrayPath_Key); + const auto imageGeomPath = filterArgs.value(k_SelectedImageGeometryPath_Key); + + if(auto result = ValidateCellTupleCount(dataStructure, imageGeomPath, {goodVoxelsArrayPath}, -45463); result.invalid()) + { + return {ConvertResultTo(std::move(result), {})}; + } nx::core::Result resultOutputActions; diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/InitializeImageGeomCellDataFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/InitializeImageGeomCellDataFilter.cpp index 0b31ac6e14..298e820809 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/InitializeImageGeomCellDataFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/InitializeImageGeomCellDataFilter.cpp @@ -13,6 +13,7 @@ #include "simplnx/Parameters/MultiArraySelectionParameter.hpp" #include "simplnx/Parameters/NumberParameter.hpp" #include "simplnx/Parameters/VectorParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/FilterUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" @@ -172,6 +173,11 @@ IFilter::PreflightResult InitializeImageGeomCellDataFilter::preflightImpl(const auto initRangeVec = filterArgs.value>(k_InitRange_Key); auto pSeedArrayNameValue = filterArgs.value(k_SeedArrayName_Key); + if(auto result = ValidateCellTupleCount(dataStructure, imageGeomPath, cellArrayPaths, -3557); result.invalid()) + { + return {ConvertResultTo(std::move(result), {})}; + } + uint64 xMin = minPoint.at(0); uint64 yMin = minPoint.at(1); uint64 zMin = minPoint.at(2); @@ -218,21 +224,9 @@ IFilter::PreflightResult InitializeImageGeomCellDataFilter::preflightImpl(const errors.push_back(Error{-3556, fmt::format("The Z Max you entered of {} is greater than your Max Z Point of {}", zMax, static_cast(imageGeom.getNumZCells()) - 1)}); } - SizeVec3 imageDims = imageGeom.getDimensions(); - - std::vector reversedImageDims(std::make_reverse_iterator(imageDims.end()), std::make_reverse_iterator(imageDims.begin())); - for(const DataPath& path : cellArrayPaths) { const auto& dataArray = dataStructure.getDataRefAs(path); - ShapeType tupleShape = dataArray.getIDataStoreRef().getTupleShape(); - - if(tupleShape.size() != reversedImageDims.size()) - { - errors.push_back(Error{-5560, fmt::format("DataArray at '{}' does not match dimensions of ImageGeometry at '{}'", path.toString(), imageGeomPath.toString())}); - continue; - } - std::optional maybeError = ExecuteNeighborFunction(CheckInitializationFunctor{}, dataArray.getDataType(), dataArray.getName(), initType, initValue, initRange); // NO BOOL if(maybeError.has_value()) { diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/M3CSurfaceMeshingFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/M3CSurfaceMeshingFilter.cpp index 327170986d..22babb8bbf 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/M3CSurfaceMeshingFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/M3CSurfaceMeshingFilter.cpp @@ -14,6 +14,7 @@ #include "simplnx/Parameters/DataObjectNameParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/MultiArraySelectionParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/Meshing/TriangleUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" @@ -141,6 +142,18 @@ IFilter::PreflightResult M3CSurfaceMeshingFilter::preflightImpl(const DataStruct auto pNodeTypesName = filterArgs.value(k_NodeTypesArrayName_Key); auto pFaceGroupDataName = filterArgs.value(k_FaceDataGroupName_Key); auto pFaceLabelsName = filterArgs.value(k_FaceLabelsArrayName_Key); + auto pGridGeometryPath = filterArgs.value(k_GridGeometryDataPath_Key); + + std::vector cellArrayPaths = {pFeatureIdsArrayPath}; + for(const auto& path : pSelectedDataArrayPaths) + { + cellArrayPaths.emplace_back(path); + } + + if(auto result = ValidateCellTupleCount(dataStructure, pGridGeometryPath, cellArrayPaths, -90201); result.invalid()) + { + return {ConvertResultTo(std::move(result), {})}; + } const DataPath pFaceGroupDataPath = pTriangleGeometryPath.createChildPath(pFaceGroupDataName); @@ -171,15 +184,9 @@ IFilter::PreflightResult M3CSurfaceMeshingFilter::preflightImpl(const DataStruct // For each Cell/Feature array to transfer, create a matching face array with the component shape // doubled (a value for each of the two features on either side of every triangle face). - const usize featureIdsTupleCount = dataStructure.getDataRefAs(pFeatureIdsArrayPath).getNumberOfTuples(); for(const auto& selectedDataPath : pSelectedDataArrayPaths) { const auto& iDataArray = dataStructure.getDataRefAs(selectedDataPath); - if(iDataArray.getNumberOfTuples() != featureIdsTupleCount) - { - return {MakeErrorResult(-90200, fmt::format("Cannot transfer Cell array '{}': its tuple count ({}) does not match the FeatureIds tuple count ({}).", selectedDataPath.toString(), - iDataArray.getNumberOfTuples(), featureIdsTupleCount))}; - } auto compShape = iDataArray.getComponentShape(); compShape.insert(compShape.begin(), 2); auto createArrayAction = diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/NearestPointFuseRegularGridsFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/NearestPointFuseRegularGridsFilter.cpp index 5633146816..ee98a6c3cd 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/NearestPointFuseRegularGridsFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/NearestPointFuseRegularGridsFilter.cpp @@ -14,6 +14,9 @@ #include "simplnx/Parameters/NumberParameter.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" +#include +#include + using namespace nx::core; namespace nx::core @@ -96,6 +99,21 @@ IFilter::PreflightResult NearestPointFuseRegularGridsFilter::preflightImpl(const auto pSamplingCellAttributeMatrixPathValue = filterArgs.value(k_SamplingCellAttributeMatrixPath_Key); auto pReferenceCellAttributeMatrixPathValue = filterArgs.value(k_ReferenceCellAttributeMatrixPath_Key); + const std::array, 2> matrixGeometryPaths = { + std::make_tuple("Sampling Cell Attribute Matrix", pSamplingCellAttributeMatrixPathValue, pSamplingGeometryPathValue), + std::make_tuple("Reference Cell Attribute Matrix", pReferenceCellAttributeMatrixPathValue, pReferenceGeometryPathValue)}; + for(const auto& [parameterName, matrixPath, geometryPath] : matrixGeometryPaths) + { + const usize tupleCount = dataStructure.getDataRefAs(matrixPath).getNumberOfTuples(); + const usize cellCount = dataStructure.getDataRefAs(geometryPath).getNumberOfCells(); + if(tupleCount != cellCount) + { + return {MakeErrorResult(-5565, + fmt::format("**{}** Attribute Matrix '{}' has {} tuples, but the selected geometry '{}' has {} cells. Select an Attribute Matrix with one tuple per cell.", + parameterName, matrixPath.toString(), tupleCount, geometryPath.toString(), cellCount))}; + } + } + nx::core::Result resultOutputActions; std::vector preflightUpdatedValues; diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/PartitionGeometryFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/PartitionGeometryFilter.cpp index d5d338ba27..f14ffb5c15 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/PartitionGeometryFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/PartitionGeometryFilter.cpp @@ -1,7 +1,14 @@ #include "PartitionGeometryFilter.hpp" #include "simplnx/Common/Array.hpp" +#include "simplnx/Common/Result.hpp" +#include "simplnx/Common/Types.hpp" #include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataObject.hpp" +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/DataStructure.hpp" +#include "simplnx/DataStructure/Geometry/INodeGeometry0D.hpp" #include "simplnx/DataStructure/Geometry/RectGridGeom.hpp" #include "simplnx/DataStructure/Geometry/VertexGeom.hpp" #include "simplnx/Filter/Actions/CreateArrayAction.hpp" @@ -16,8 +23,19 @@ #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/NumberParameter.hpp" #include "simplnx/Parameters/VectorParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/GeometryUtilities.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include + using namespace nx::core; namespace @@ -35,6 +53,25 @@ const ChoicesParameter::ValueType k_AdvancedModeIndex = 1; const ChoicesParameter::ValueType k_BoundingBoxModeIndex = 2; const ChoicesParameter::ValueType k_ExistingSchemeModeIndex = 3; +/** + * @brief Checks the selected tuple count against vertex metadata without reading coordinate values. + * @param geometry Supplies the vertex count. + * @param geometryPath Selected geometry path, including linked selections. + * @param tupleCount Number of tuples in the selected data. + * @param dataPath Selected data path, including linked selections. + * @return Error -3043 if the counts differ; otherwise, a valid result. + */ +Result<> validateVertexTupleCount(const INodeGeometry0D& geometry, const DataPath& geometryPath, usize tupleCount, const DataPath& dataPath) +{ + const usize vertexCount = geometry.getVerticesRef().getNumberOfTuples(); + if(tupleCount != vertexCount) + { + return MakeErrorResult(-3043, fmt::format("The selected data '{}' has {} tuples, but the selected node geometry '{}' has {} vertices. Select data with one tuple per geometry vertex.", + dataPath.toString(), tupleCount, geometryPath.toString(), vertexCount)); + } + return {}; +} + /** * @brief Generates the display text that describes the input geometry, * shown as a preflight updated value in the user interface. @@ -353,6 +390,15 @@ IFilter::PreflightResult PartitionGeometryFilter::preflightImpl(const DataStruct const auto& attrMatrix = dataStructure.getDataRefAs(pInputGeomCellAMPathValue); const auto& iGeom = dataStructure.getDataRefAs({pInputGeometryToPartitionValue}); + if(iGeom.getGeomType() == IGeometry::Type::Image && pPartitioningModeValue == k_ExistingSchemeModeIndex) + { + const auto& geometry = dataStructure.getDataRefAs(pInputGeometryToPartitionValue); + auto result = ValidateCellTupleCount(geometry, pInputGeometryToPartitionValue, attrMatrix.getNumberOfTuples(), pInputGeomCellAMPathValue, -3045); + if(result.invalid()) + { + return {.outputActions = ConvertResultTo(std::move(result), {})}; + } + } std::string inputGeometryInformation; Result psInfo; switch(iGeom.getGeomType()) @@ -370,10 +416,10 @@ IFilter::PreflightResult PartitionGeometryFilter::preflightImpl(const DataStruct } case IGeometry::Type::RectGrid: { const auto& geometry = dataStructure.getDataRefAs({pInputGeometryToPartitionValue}); - if(attrMatrix.getNumberOfTuples() != geometry.getNumberOfCells()) + auto cellTupleResult = ValidateCellTupleCount(geometry, pInputGeometryToPartitionValue, attrMatrix.getNumberOfTuples(), pInputGeomCellAMPathValue, -3010); + if(cellTupleResult.invalid()) { - return {MakeErrorResult(-3010, fmt::format("{}: The attribute matrix '{}' does not have the same tuple count ({}) as geometry \"{}\"'s cell count ({}).", humanName(), - attrMatrix.getName(), attrMatrix.getNumberOfTuples(), geometry.getName(), geometry.getNumberOfCells()))}; + return {.outputActions = ConvertResultTo(std::move(cellTupleResult), {})}; } psInfo = GeneratePartitioningSchemeInfo(geometry, dataStructure, filterArgs); inputGeometryInformation = "Rectilinear grid geometry space unknown during preflight."; @@ -489,6 +535,20 @@ IFilter::PreflightResult PartitionGeometryFilter::preflightImpl(const DataStruct return {MakeErrorResult(-3019, fmt::format("{}: The input geometry is {}, which is not vertex-based. Vertex mask cannot be used.", humanName(), iGeom.getTypeName()))}; } + if(pUseVertexMask) + { + const auto maskPath = filterArgs.value(k_VertexMaskPath_Key); + // ArraySelectionParameter also accepts StringArray and NeighborList for Boolean selections. + // Keep the local type check until the parameter validates supported array kinds. + // TODO(#1833): Revisit this guard after the shared fix preserves generic array support. + if(dataStructure.getDataAs(maskPath) == nullptr) + { + const auto& selectedObject = dataStructure.getDataRefAs(maskPath); + return MakePreflightErrorResult(-3044, fmt::format("The selected Vertex Mask object '{}' has type '{}', but a scalar Boolean DataArray is required. Select a scalar Boolean DataArray.", + maskPath.toString(), selectedObject.getTypeName())); + } + } + return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; } @@ -497,12 +557,11 @@ Result PartitionGeometryFilter::generateNodeBased const DataPath& attrMatrixPath) const { const auto& geometry = dataStructure.getDataRefAs({geometryToPartitionPath}); - const IGeometry::SharedVertexList& vertexList = geometry.getVerticesRef(); const auto& attrMatrix = dataStructure.getDataRefAs(attrMatrixPath); - if(attrMatrix.getNumberOfTuples() != vertexList.getNumberOfTuples()) + auto vertexTupleResult = validateVertexTupleCount(geometry, geometryToPartitionPath, attrMatrix.getNumberOfTuples(), attrMatrixPath); + if(vertexTupleResult.invalid()) { - return {MakeErrorResult(-3014, fmt::format("{}: The attribute matrix '{}' does not have the same tuple count ({}) as geometry \"{}\"'s vertex count ({}).", - humanName(), attrMatrix.getName(), attrMatrix.getNumberOfTuples(), geometry.getName(), geometry.getNumberOfVertices()))}; + return {ConvertResultTo(std::move(vertexTupleResult), {})}; } Result<> dimensionalityResult = DataCheckDimensionality(geometry); if(dimensionalityResult.invalid()) @@ -555,15 +614,16 @@ Result<> PartitionGeometryFilter::dataCheckPartitioningMode(const DataStructure& auto pInputGeomCellAMPathValue = filterArgs.value(k_InputGeometryCellAttributeMatrixPath_Key); const auto& attrMatrix = dataStructure.getDataRefAs({pInputGeomCellAMPathValue}); + const auto geometryToPartitionPath = filterArgs.value(k_InputGeometryToPartition_Key); switch(partitioningMode) { case PartitioningMode::Basic: - return dataCheckBasicMode(numOfPartitionsPerAxis, geometryToPartition, attrMatrix); + return dataCheckBasicMode(numOfPartitionsPerAxis, geometryToPartition, attrMatrix, geometryToPartitionPath, pInputGeomCellAMPathValue); case PartitioningMode::Advanced: - return dataCheckAdvancedMode(numOfPartitionsPerAxis, pCellLengthValue, geometryToPartition, attrMatrix); + return dataCheckAdvancedMode(numOfPartitionsPerAxis, pCellLengthValue, geometryToPartition, attrMatrix, geometryToPartitionPath, pInputGeomCellAMPathValue); case PartitioningMode::BoundingBox: - return dataCheckBoundingBoxMode(numOfPartitionsPerAxis, pMinGridCoordValue, pMaxGridCoordValue, geometryToPartition, attrMatrix); + return dataCheckBoundingBoxMode(numOfPartitionsPerAxis, pMinGridCoordValue, pMaxGridCoordValue, geometryToPartition, attrMatrix, geometryToPartitionPath, pInputGeomCellAMPathValue); case PartitioningMode::ExistingPartitionGrid: return DataCheckExistingGeometryMode(); } @@ -573,7 +633,8 @@ Result<> PartitionGeometryFilter::dataCheckPartitioningMode(const DataStructure& // ----------------------------------------------------------------------------- template -Result<> PartitionGeometryFilter::dataCheckBasicMode(const SizeVec3& numOfPartitionsPerAxis, const GeomType& geometryToPartition, const AttributeMatrix& attrMatrix) const +Result<> PartitionGeometryFilter::dataCheckBasicMode(const SizeVec3& numOfPartitionsPerAxis, const GeomType& geometryToPartition, const AttributeMatrix& attrMatrix, + const DataPath& geometryToPartitionPath, const DataPath& attrMatrixPath) const { Result<> result = DataCheckNumberOfPartitions(numOfPartitionsPerAxis); if(result.invalid()) @@ -581,7 +642,7 @@ Result<> PartitionGeometryFilter::dataCheckBasicMode(const SizeVec3& numOfPartit return result; } - result = dataCheckPartitioningScheme(geometryToPartition, attrMatrix); + result = dataCheckPartitioningScheme(geometryToPartition, attrMatrix, geometryToPartitionPath, attrMatrixPath); if(result.invalid()) { return result; @@ -593,7 +654,7 @@ Result<> PartitionGeometryFilter::dataCheckBasicMode(const SizeVec3& numOfPartit // ----------------------------------------------------------------------------- template Result<> PartitionGeometryFilter::dataCheckAdvancedMode(const SizeVec3& numOfPartitionsPerAxis, const FloatVec3& lengthPerPartition, const GeomType& geometryToPartition, - const AttributeMatrix& attrMatrix) const + const AttributeMatrix& attrMatrix, const DataPath& geometryToPartitionPath, const DataPath& attrMatrixPath) const { Result<> result = DataCheckNumberOfPartitions(numOfPartitionsPerAxis); if(result.invalid()) @@ -614,7 +675,7 @@ Result<> PartitionGeometryFilter::dataCheckAdvancedMode(const SizeVec3& numOfPar return MakeErrorResult(-3005, fmt::format("{}: Length Per Partition - The Z value ({}) cannot be negative.", humanName(), lengthPerPartition.getZ())); } - result = dataCheckPartitioningScheme(geometryToPartition, attrMatrix); + result = dataCheckPartitioningScheme(geometryToPartition, attrMatrix, geometryToPartitionPath, attrMatrixPath); if(result.invalid()) { return result; @@ -626,7 +687,7 @@ Result<> PartitionGeometryFilter::dataCheckAdvancedMode(const SizeVec3& numOfPar // ----------------------------------------------------------------------------- template Result<> PartitionGeometryFilter::dataCheckBoundingBoxMode(const SizeVec3& numOfPartitionsPerAxis, const FloatVec3& llCoord, const FloatVec3& urCoord, const GeomType& geometryToPartition, - const AttributeMatrix& attrMatrix) const + const AttributeMatrix& attrMatrix, const DataPath& geometryToPartitionPath, const DataPath& attrMatrixPath) const { Result<> result = DataCheckNumberOfPartitions(numOfPartitionsPerAxis); if(result.invalid()) @@ -649,7 +710,7 @@ Result<> PartitionGeometryFilter::dataCheckBoundingBoxMode(const SizeVec3& numOf return MakeErrorResult(-3008, fmt::format("{}: Lower Left Coordinate - Z value ({}) is larger than the upper right coordinate Z value ({}).", humanName(), llCoord.getZ(), urCoord.getZ())); } - result = dataCheckPartitioningScheme(geometryToPartition, attrMatrix); + result = dataCheckPartitioningScheme(geometryToPartition, attrMatrix, geometryToPartitionPath, attrMatrixPath); if(result.invalid()) { return result; @@ -667,27 +728,17 @@ Result<> PartitionGeometryFilter::DataCheckExistingGeometryMode() // ----------------------------------------------------------------------------- template -Result<> PartitionGeometryFilter::dataCheckPartitioningScheme(const GeomType& geometryToPartition, const AttributeMatrix& attrMatrix) const +Result<> PartitionGeometryFilter::dataCheckPartitioningScheme(const GeomType& geometryToPartition, const AttributeMatrix& attrMatrix, const DataPath& geometryToPartitionPath, + const DataPath& attrMatrixPath) const { if constexpr(std::is_same_v || std::is_same_v) { - if(attrMatrix.getNumberOfTuples() != geometryToPartition.getNumberOfCells()) - { - return MakeErrorResult(-3009, fmt::format("{}: The attribute matrix '{}' does not have the same tuple count ({}) as geometry \"{}\"'s cell count ({}).", humanName(), attrMatrix.getName(), - attrMatrix.getNumberOfTuples(), geometryToPartition.getName(), geometryToPartition.getNumberOfCells())); - } + return ValidateCellTupleCount(geometryToPartition, geometryToPartitionPath, attrMatrix.getNumberOfTuples(), attrMatrixPath, -3009); } else { - const IGeometry::SharedVertexList& vertexList = geometryToPartition.getVertices(); - if(attrMatrix.getNumberOfTuples() != vertexList.getNumberOfTuples()) - { - return MakeErrorResult(-3010, fmt::format("{}: The attribute matrix '{}' does not have the same tuple count ({}) as geometry \"{}\"'s vertex count ({}).", humanName(), attrMatrix.getName(), - attrMatrix.getNumberOfTuples(), geometryToPartition.getName(), vertexList.getNumberOfTuples())); - } + return validateVertexTupleCount(geometryToPartition, geometryToPartitionPath, attrMatrix.getNumberOfTuples(), attrMatrixPath); } - - return {}; } // ----------------------------------------------------------------------------- diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/PartitionGeometryFilter.hpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/PartitionGeometryFilter.hpp index 51ecf2c695..ce2ef58154 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/PartitionGeometryFilter.hpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/PartitionGeometryFilter.hpp @@ -11,6 +11,10 @@ #include "simplnx/Filter/FilterTraits.hpp" #include "simplnx/Filter/IFilter.hpp" +#include +#include +#include + namespace nx::core { /** @@ -153,20 +157,66 @@ class SIMPLNXCORE_EXPORT PartitionGeometryFilter : public IFilter template Result<> dataCheckPartitioningMode(const DataStructure& dataStructure, const Arguments& filterArgs, const GeomType& geometryToPartition) const; + /** + * @brief Checks partition counts before the geometry data count. + * @tparam GeomType Image, rectilinear, or node geometry type. + * @param numOfPartitionsPerAxis Partition count on each axis; each count must exceed zero. + * @param geometryToPartition Geometry that supplies the cell or vertex count. + * @param attrMatrix Selected matrix whose tuple count must match the geometry data count. + * @param geometryToPartitionPath Actual selected geometry path for geometry diagnostics, including linked selections. + * @param attrMatrixPath Actual selected matrix path for geometry diagnostics, including linked selections. + * @return First partition-count or geometry-data error, or a valid empty result. + */ template - Result<> dataCheckBasicMode(const SizeVec3& numOfPartitionsPerAxis, const GeomType& geometryToPartition, const AttributeMatrix& attrMatrix) const; + Result<> dataCheckBasicMode(const SizeVec3& numOfPartitionsPerAxis, const GeomType& geometryToPartition, const AttributeMatrix& attrMatrix, const DataPath& geometryToPartitionPath, + const DataPath& attrMatrixPath) const; + /** + * @brief Checks partition counts and lengths before the geometry data count. + * @tparam GeomType Image, rectilinear, or node geometry type. + * @param numOfPartitionsPerAxis Partition count on each axis; each count must exceed zero. + * @param lengthPerPartition Partition length on each axis; negative values are invalid. + * @param geometryToPartition Geometry that supplies the cell or vertex count. + * @param attrMatrix Selected matrix whose tuple count must match the geometry data count. + * @param geometryToPartitionPath Actual selected geometry path for geometry diagnostics, including linked selections. + * @param attrMatrixPath Actual selected matrix path for geometry diagnostics, including linked selections. + * @return First partition-count, length, or geometry-data error, or a valid empty result. + */ template - Result<> dataCheckAdvancedMode(const SizeVec3& numOfPartitionsPerAxis, const FloatVec3& lengthPerPartition, const GeomType& geometryToPartition, const AttributeMatrix& attrMatrix) const; + Result<> dataCheckAdvancedMode(const SizeVec3& numOfPartitionsPerAxis, const FloatVec3& lengthPerPartition, const GeomType& geometryToPartition, const AttributeMatrix& attrMatrix, + const DataPath& geometryToPartitionPath, const DataPath& attrMatrixPath) const; + /** + * @brief Checks partition counts and bounds before the geometry data count. + * @tparam GeomType Image, rectilinear, or node geometry type. + * @param numOfPartitionsPerAxis Partition count on each axis; each count must exceed zero. + * @param llCoord Lower corner; each coordinate must not exceed the corresponding upper corner coordinate. + * @param urCoord Upper corner of the partition grid. + * @param geometryToPartition Geometry that supplies the cell or vertex count. + * @param attrMatrix Selected matrix whose tuple count must match the geometry data count. + * @param geometryToPartitionPath Actual selected geometry path for geometry diagnostics, including linked selections. + * @param attrMatrixPath Actual selected matrix path for geometry diagnostics, including linked selections. + * @return First partition-count, bounds, or geometry-data error, or a valid empty result. + */ template - Result<> dataCheckBoundingBoxMode(const SizeVec3& numOfPartitionsPerAxis, const FloatVec3& llCoord, const FloatVec3& urCoord, const GeomType& geometryToPartition, - const AttributeMatrix& attrMatrix) const; + Result<> dataCheckBoundingBoxMode(const SizeVec3& numOfPartitionsPerAxis, const FloatVec3& llCoord, const FloatVec3& urCoord, const GeomType& geometryToPartition, const AttributeMatrix& attrMatrix, + const DataPath& geometryToPartitionPath, const DataPath& attrMatrixPath) const; static Result<> DataCheckExistingGeometryMode(); + /** + * @brief Checks the selected matrix tuple count against geometry cells or vertices. + * @tparam GeomType Image, rectilinear, or node geometry type. + * @param geometryToPartition Geometry that supplies the cell or vertex count. + * @param attrMatrix Selected matrix whose tuple count must match the geometry data count. + * @param geometryToPartitionPath Actual selected geometry path for geometry diagnostics, including linked selections. + * @param attrMatrixPath Actual selected matrix path for geometry diagnostics, including linked selections. + * @return Count-mismatch error, or a valid empty result when counts match. + * + * Grid validation compares counts only; tuple shapes and ownership need not match. + */ template - Result<> dataCheckPartitioningScheme(const GeomType& geometryToPartition, const AttributeMatrix& attrMatrix) const; + Result<> dataCheckPartitioningScheme(const GeomType& geometryToPartition, const AttributeMatrix& attrMatrix, const DataPath& geometryToPartitionPath, const DataPath& attrMatrixPath) const; /** * @brief Helper method that data checks the Number Of Partitions Per Axis variable. diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReplaceElementAttributesWithNeighborValuesFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReplaceElementAttributesWithNeighborValuesFilter.cpp index 2a28112cdc..35a8e4af49 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReplaceElementAttributesWithNeighborValuesFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ReplaceElementAttributesWithNeighborValuesFilter.cpp @@ -10,6 +10,7 @@ #include "simplnx/Parameters/DataGroupSelectionParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/FilterUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" @@ -83,13 +84,19 @@ IFilter::UniquePointer ReplaceElementAttributesWithNeighborValuesFilter::clone() IFilter::PreflightResult ReplaceElementAttributesWithNeighborValuesFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { - auto pComparisonDataPath = filterArgs.value(k_ComparisonDataPath); + auto geometryPath = filterArgs.value(k_SelectedImageGeometryPath_Key); + auto inputArrayPath = filterArgs.value(k_ComparisonDataPath); + + if(auto tupleResult = ValidateCellTupleCount(dataStructure, geometryPath, {inputArrayPath}, -15001); tupleResult.invalid()) + { + return {ConvertResultTo(std::move(tupleResult), {})}; + } nx::core::Result resultOutputActions; // Inform users that the following arrays are going to be modified in place // Cell Data is going to be modified - nx::core::AppendDataObjectModifications(dataStructure, resultOutputActions.value().modifiedActions, pComparisonDataPath.getParent(), {}); + nx::core::AppendDataObjectModifications(dataStructure, resultOutputActions.value().modifiedActions, inputArrayPath.getParent(), {}); return {std::move(resultOutputActions)}; } diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp index 0d24b593e9..17b6a60791 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/RequireMinNumNeighborsFilter.cpp @@ -2,7 +2,9 @@ #include "SimplnxCore/Filters/Algorithms/RequireMinNumNeighbors.hpp" +#include "simplnx/Common/Result.hpp" #include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataObject.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Filter/Actions/DeleteDataAction.hpp" #include "simplnx/Parameters/ArraySelectionParameter.hpp" @@ -15,12 +17,21 @@ #include "simplnx/Utilities/FilterUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" +#include + +#include +#include +#include +#include +#include +#include + namespace nx::core { namespace { constexpr int32 k_InconsistentTupleCount = -252; -constexpr int32 k_FeatureIdsTupleCountMismatch = -55571; +constexpr int32 k_FeatureIdsTupleCountMismatch = -59571; } // namespace @@ -114,13 +125,21 @@ IFilter::PreflightResult RequireMinNumNeighborsFilter::preflightImpl(const DataS const auto cellDataPath = dataStructure.getDataRefAs(imageGeomPath).getCellDataPath(); featureIdsPath = MapToGeometryCellDataPath(cellDataPath, featureIdsPath); - const auto& featureIds = dataStructure.getDataRefAs(featureIdsPath); + // ArraySelectionParameter also accepts StringArray and NeighborList for numeric selections. + // Keep the local type checks until the parameter validates supported array kinds. + // TODO(#1833): Revisit these guards after the shared fix preserves generic array support. + const auto* featureIdsPtr = dataStructure.getDataAs(featureIdsPath); + if(featureIdsPtr == nullptr) + { + const auto& selectedObject = dataStructure.getDataRefAs(featureIdsPath); + return MakePreflightErrorResult(-59574, fmt::format("The selected Feature Ids object '{}' has type '{}', but a scalar int32 DataArray is required. Select a scalar int32 DataArray.", + featureIdsPath.toString(), selectedObject.getTypeName())); + } const auto& imageGeom = dataStructure.getDataRefAs(imageGeomPath); - if(featureIds.getNumberOfTuples() != imageGeom.getNumberOfCells()) + auto cellTupleResult = ValidateCellTupleCount(imageGeom, imageGeomPath, featureIdsPtr->getNumberOfTuples(), featureIdsPath, k_FeatureIdsTupleCountMismatch); + if(cellTupleResult.invalid()) { - return MakePreflightErrorResult(k_FeatureIdsTupleCountMismatch, - fmt::format("FeatureIds array '{}' contains {} tuples, but Image Geometry '{}' contains {} cells. Select a FeatureIds array that matches the Image Geometry.", - featureIdsPath.toString(), featureIds.getNumberOfTuples(), imageGeomPath.toString(), imageGeom.getNumberOfCells())); + return {.outputActions = ConvertResultTo(std::move(cellTupleResult), {})}; } auto ignoredArraysResult = ValidateArraysInGeometryCellData(dataStructure, imageGeomPath, ignoredDataArrayPaths, -55573, "Cell Arrays to Ignore"); @@ -154,6 +173,19 @@ IFilter::PreflightResult RequireMinNumNeighborsFilter::preflightImpl(const DataS return MakePreflightErrorResult(k_InconsistentTupleCount, fmt::format("The following DataArrays all must have equal number of tuples but this was not satisfied.\n{}", tupleValidityCheck.error())); } + if(dataStructure.getDataAs(numNeighborsPath) == nullptr) + { + const auto& selectedObject = dataStructure.getDataRefAs(numNeighborsPath); + return MakePreflightErrorResult(-59575, fmt::format("The selected Number of Neighbors object '{}' has type '{}', but a scalar int32 DataArray is required. Select a scalar int32 DataArray.", + numNeighborsPath.toString(), selectedObject.getTypeName())); + } + if(applyToSinglePhase && dataStructure.getDataAs(featurePhasesPath) == nullptr) + { + const auto& selectedObject = dataStructure.getDataRefAs(featurePhasesPath); + return MakePreflightErrorResult(-59576, fmt::format("The selected Feature Phases object '{}' has type '{}', but a scalar int32 DataArray is required. Select a scalar int32 DataArray.", + featurePhasesPath.toString(), selectedObject.getTypeName())); + } + // Report arrays that the filter modifies in place. // Cell Data arrays are modified. nx::core::AppendDataObjectModifications(dataStructure, resultOutputActions.value().modifiedActions, cellDataPath, {}); diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ScalarSegmentFeaturesFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ScalarSegmentFeaturesFilter.cpp index ac31074d0a..e8634e6527 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ScalarSegmentFeaturesFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/ScalarSegmentFeaturesFilter.cpp @@ -11,6 +11,7 @@ #include "simplnx/Parameters/DataObjectNameParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" using namespace nx::core; @@ -112,26 +113,19 @@ IFilter::PreflightResult ScalarSegmentFeaturesFilter::preflightImpl(const DataSt DataPath cellFeaturesPath = gridGeomPath.createChildPath(cellFeaturesName); DataPath activeArrayPath = cellFeaturesPath.createChildPath(activeArrayName); - const auto& gridGeometry = dataStructure.getDataRefAs(gridGeomPath); - auto gridDims = gridGeometry.getDimensions(); - const std::vector cellTupleDims = {gridDims[2], gridDims[1], gridDims[0]}; - std::vector dataPaths; - - dataPaths.push_back(inputDataPath); - - bool useGoodVoxels = filterArgs.value(k_UseMask_Key); - if(useGoodVoxels) + std::vector cellArrayPaths = {inputDataPath}; + if(filterArgs.value(k_UseMask_Key)) { - DataPath goodVoxelsPath = filterArgs.value(k_MaskArrayPath_Key); - dataPaths.push_back(goodVoxelsPath); + cellArrayPaths.emplace_back(filterArgs.value(k_MaskArrayPath_Key)); } - - auto tupleValidityCheck = dataStructure.validateNumberOfTuples(dataPaths); - if(!tupleValidityCheck) + if(auto tupleResult = ValidateCellTupleCount(dataStructure, gridGeomPath, cellArrayPaths, -654); tupleResult.invalid()) { - return {MakeErrorResult(-651, fmt::format("The following DataArrays all must have equal number of tuples but this was not satisfied.\n{}", tupleValidityCheck.error()))}; + return {ConvertResultTo(std::move(tupleResult), {})}; } + const auto& gridGeometry = dataStructure.getDataRefAs(gridGeomPath); + auto gridDims = gridGeometry.getDimensions(); + const std::vector cellTupleDims = {gridDims[2], gridDims[1], gridDims[0]}; auto createFeatureIdsAction = std::make_unique(DataType::int32, cellTupleDims, std::vector{1}, featureIdsPath, "", "0"); auto createFeatureGroupAction = std::make_unique(cellFeaturesPath, std::vector{1}); diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteAbaqusHexahedronFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteAbaqusHexahedronFilter.cpp index 104ff28f6f..f60613bac4 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteAbaqusHexahedronFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteAbaqusHexahedronFilter.cpp @@ -13,6 +13,7 @@ #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/NumberParameter.hpp" #include "simplnx/Parameters/StringParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" namespace fs = std::filesystem; @@ -104,6 +105,20 @@ IFilter::UniquePointer WriteAbaqusHexahedronFilter::clone() const IFilter::PreflightResult WriteAbaqusHexahedronFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { + auto geometryPath = filterArgs.value(k_ImageGeometryPath_Key); + auto inputArrayPath = filterArgs.value(k_FeatureIdsArrayPath_Key); + + const auto& imageGeom = dataStructure.getDataRefAs(geometryPath); + if(auto sizeResult = AbaqusElementSets::validateMeshSize(imageGeom.getDimensions()); sizeResult.invalid()) + { + return {ConvertResultTo(std::move(sizeResult), {})}; + } + + if(auto tupleResult = ValidateCellTupleCount(dataStructure, geometryPath, {inputArrayPath}, -1124); tupleResult.invalid()) + { + return {ConvertResultTo(std::move(tupleResult), {})}; + } + const auto prefix = filterArgs.value(k_FilePrefix_Key); if(auto prefixResult = AbaqusElementSets::validateFilePrefix(prefix, -1122); prefixResult.invalid()) { @@ -123,11 +138,6 @@ IFilter::PreflightResult WriteAbaqusHexahedronFilter::preflightImpl(const DataSt return {ConvertResultTo(std::move(targetsResult), {})}; } - const auto& imageGeom = dataStructure.getDataRefAs(filterArgs.value(k_ImageGeometryPath_Key)); - if(auto sizeResult = AbaqusElementSets::validateMeshSize(imageGeom.getDimensions()); sizeResult.invalid()) - { - return {ConvertResultTo(std::move(sizeResult), {})}; - } return {}; } diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteAvizoRectilinearCoordinateFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteAvizoRectilinearCoordinateFilter.cpp index f87293c6c8..6a4d98995a 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteAvizoRectilinearCoordinateFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteAvizoRectilinearCoordinateFilter.cpp @@ -11,6 +11,7 @@ #include "simplnx/Parameters/FileSystemPathParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/StringParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" #include @@ -85,6 +86,14 @@ IFilter::UniquePointer WriteAvizoRectilinearCoordinateFilter::clone() const IFilter::PreflightResult WriteAvizoRectilinearCoordinateFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { + auto geometryPath = filterArgs.value(k_GeometryPath_Key); + auto inputArrayPath = filterArgs.value(k_FeatureIdsArrayPath_Key); + + if(auto tupleResult = ValidateCellTupleCount(dataStructure, geometryPath, {inputArrayPath}, -5834); tupleResult.invalid()) + { + return {ConvertResultTo(std::move(tupleResult), {})}; + } + return {}; } diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteAvizoUniformCoordinateFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteAvizoUniformCoordinateFilter.cpp index a98cb49102..015ec95ccb 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteAvizoUniformCoordinateFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteAvizoUniformCoordinateFilter.cpp @@ -11,6 +11,7 @@ #include "simplnx/Parameters/FileSystemPathParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/StringParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" #include @@ -85,6 +86,14 @@ IFilter::UniquePointer WriteAvizoUniformCoordinateFilter::clone() const IFilter::PreflightResult WriteAvizoUniformCoordinateFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { + auto geometryPath = filterArgs.value(k_GeometryPath_Key); + auto inputArrayPath = filterArgs.value(k_FeatureIdsArrayPath_Key); + + if(auto tupleResult = ValidateCellTupleCount(dataStructure, geometryPath, {inputArrayPath}, -5835); tupleResult.invalid()) + { + return {ConvertResultTo(std::move(tupleResult), {})}; + } + return {}; } diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteLosAlamosFFTFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteLosAlamosFFTFilter.cpp index d03d1cf8a0..3fe52effcd 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteLosAlamosFFTFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteLosAlamosFFTFilter.cpp @@ -9,6 +9,7 @@ #include "simplnx/Parameters/DataGroupSelectionParameter.hpp" #include "simplnx/Parameters/FileSystemPathParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" #include @@ -88,17 +89,14 @@ IFilter::UniquePointer WriteLosAlamosFFTFilter::clone() const IFilter::PreflightResult WriteLosAlamosFFTFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { - auto pFeatureIdsArrayPathValue = filterArgs.value(k_FeatureIdsArrayPath_Key); - auto pCellEulerAnglesArrayPathValue = filterArgs.value(k_CellEulerAnglesArrayPath_Key); - auto pCellPhasesArrayPathValue = filterArgs.value(k_CellPhasesArrayPath_Key); + auto geometryPath = filterArgs.value(k_ImageGeomPath); + auto inputArrayPath = filterArgs.value(k_FeatureIdsArrayPath_Key); + auto cellEulerAnglesPath = filterArgs.value(k_CellEulerAnglesArrayPath_Key); + auto cellPhasesPath = filterArgs.value(k_CellPhasesArrayPath_Key); - usize fIdsTupCount = dataStructure.getDataAs(pFeatureIdsArrayPathValue)->getNumberOfTuples(); - usize eulersTupCount = dataStructure.getDataAs(pCellEulerAnglesArrayPathValue)->getNumberOfTuples(); - usize phasesTupCount = dataStructure.getDataAs(pCellPhasesArrayPathValue)->getNumberOfTuples(); - - if(fIdsTupCount != eulersTupCount || fIdsTupCount != phasesTupCount) + if(auto tupleResult = ValidateCellTupleCount(dataStructure, geometryPath, {inputArrayPath, cellEulerAnglesPath, cellPhasesPath}, -73461); tupleResult.invalid()) { - return MakePreflightErrorResult(-73460, fmt::format("Tuple Dimensions don't match: Feature Ids - {} || Euler Angles - {} || Phases - {}", fIdsTupCount, eulersTupCount, phasesTupCount)); + return {ConvertResultTo(std::move(tupleResult), {})}; } return {}; diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteOnScaleTableFileFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteOnScaleTableFileFilter.cpp index 7e44bc6c7d..fa4b4f2874 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteOnScaleTableFileFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteOnScaleTableFileFilter.cpp @@ -2,7 +2,10 @@ #include "SimplnxCore/Filters/Algorithms/WriteOnScaleTableFile.hpp" +#include "simplnx/Common/Result.hpp" +#include "simplnx/DataStructure/DataObject.hpp" #include "simplnx/DataStructure/Geometry/IGridGeometry.hpp" +#include "simplnx/DataStructure/IDataArray.hpp" #include "simplnx/DataStructure/StringArray.hpp" #include "simplnx/Parameters/ArraySelectionParameter.hpp" // This header declares DataContainerSelectionFilterParameterConverter. @@ -12,10 +15,18 @@ #include "simplnx/Parameters/GeometrySelectionParameter.hpp" #include "simplnx/Parameters/StringParameter.hpp" #include "simplnx/Parameters/VectorParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" #include "simplnx/Utilities/StringUtilities.hpp" +#include + +#include #include +#include +#include +#include +#include namespace fs = std::filesystem; using namespace nx::core; @@ -97,11 +108,20 @@ IFilter::PreflightResult WriteOnScaleTableFileFilter::preflightImpl(const DataSt const auto geometryPath = filterArgs.value(k_InputGeometryPath_Key); const auto& geometry = dataStructure.getDataRefAs(geometryPath); const auto featureIdsPath = filterArgs.value(k_FeatureIdsArrayPath_Key); - const auto& featureIds = dataStructure.getDataRefAs(featureIdsPath); - if(featureIds.getNumberOfTuples() != geometry.getNumberOfCells()) + // ArraySelectionParameter also accepts StringArray and NeighborList for numeric selections. + // Keep the local type check until the parameter validates supported array kinds. + // TODO(#1833): Revisit this guard after the shared fix preserves generic array support. + const auto* featureIdsPtr = dataStructure.getDataAs(featureIdsPath); + if(featureIdsPtr == nullptr) + { + const auto& selectedObject = dataStructure.getDataRefAs(featureIdsPath); + return MakePreflightErrorResult(-12042, fmt::format("The selected Feature Ids object '{}' has type '{}', but a scalar integer DataArray is required. Select a scalar integer DataArray.", + featureIdsPath.toString(), selectedObject.getTypeName())); + } + auto cellTupleResult = ValidateCellTupleCount(geometry, geometryPath, featureIdsPtr->getNumberOfTuples(), featureIdsPath, -12024); + if(cellTupleResult.invalid()) { - return MakePreflightErrorResult(-12024, fmt::format("The array '{}' has {} tuples, but the grid geometry '{}' has {} cells.", featureIdsPath.toString(), featureIds.getNumberOfTuples(), - geometryPath.toString(), geometry.getNumberOfCells())); + return {.outputActions = ConvertResultTo(std::move(cellTupleResult), {})}; } const auto phaseNamesPath = filterArgs.value(k_PhaseNamesArrayPath_Key); diff --git a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteSPParksSitesFilter.cpp b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteSPParksSitesFilter.cpp index 230aaeff7e..975417fc01 100644 --- a/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteSPParksSitesFilter.cpp +++ b/src/Plugins/SimplnxCore/src/SimplnxCore/Filters/WriteSPParksSitesFilter.cpp @@ -9,6 +9,7 @@ #include "simplnx/Parameters/DataGroupSelectionParameter.hpp" #include "simplnx/Parameters/FileSystemPathParameter.hpp" #include "simplnx/Parameters/GeometrySelectionParameter.hpp" +#include "simplnx/Utilities/DataArrayUtilities.hpp" #include "simplnx/Utilities/SIMPLConversion.hpp" #include @@ -83,9 +84,13 @@ IFilter::UniquePointer WriteSPParksSitesFilter::clone() const IFilter::PreflightResult WriteSPParksSitesFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { - auto pFeatureIdsArrayPathValue = filterArgs.value(k_FeatureIdsArrayPath_Key); + auto geometryPath = filterArgs.value(k_ImageGeomPath); + auto inputArrayPath = filterArgs.value(k_FeatureIdsArrayPath_Key); - usize fIdsTupCount = dataStructure.getDataAs(pFeatureIdsArrayPathValue)->getNumberOfTuples(); + if(auto tupleResult = ValidateCellTupleCount(dataStructure, geometryPath, {inputArrayPath}, -77453); tupleResult.invalid()) + { + return {ConvertResultTo(std::move(tupleResult), {})}; + } return {}; } diff --git a/src/Plugins/SimplnxCore/test/AddBadDataTest.cpp b/src/Plugins/SimplnxCore/test/AddBadDataTest.cpp index 7c0e7bf319..c21f998509 100644 --- a/src/Plugins/SimplnxCore/test/AddBadDataTest.cpp +++ b/src/Plugins/SimplnxCore/test/AddBadDataTest.cpp @@ -4,6 +4,7 @@ #include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Utilities/AlgorithmDispatch.hpp" #include "simplnx/Utilities/DataStoreUtilities.hpp" @@ -108,3 +109,46 @@ TEST_CASE("SimplnxCore::AddBadDataFilter: SIMPL Backwards Compatibility", "[Simp } } } + +TEST_CASE("SimplnxCore::AddBadDataFilter: Geometry cell tuple count", "[SimplnxCore][AddBadDataFilter][TupleCount]") +{ + using FilterType = AddBadDataFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{2, 3, 2}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + // Matching arrays remain valid outside the geometry's Cell Attribute Matrix, with a different tuple shape. + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 0), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_ImageGeometryPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_GBEuclideanDistancesArrayPath_Key, std::make_any(inputPath)); + args.insertOrAssign(FilterType::k_PoissonNoise_Key, std::make_any(true)); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -76238); + REQUIRE(errorRef.message.find(inputPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", tuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/AlignSectionsFeatureCentroidTest.cpp b/src/Plugins/SimplnxCore/test/AlignSectionsFeatureCentroidTest.cpp index 0abebb2cc3..29c1e534a0 100644 --- a/src/Plugins/SimplnxCore/test/AlignSectionsFeatureCentroidTest.cpp +++ b/src/Plugins/SimplnxCore/test/AlignSectionsFeatureCentroidTest.cpp @@ -2,6 +2,8 @@ #include "SimplnxCore/SimplnxCore_test_dirs.hpp" #include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Parameters/ArraySelectionParameter.hpp" #include "simplnx/Parameters/ChoicesParameter.hpp" @@ -138,3 +140,45 @@ TEST_CASE("SimplnxCore::AlignSectionsFeatureCentroidFilter: output test", "[Reco UnitTest::CheckArraysInheritTupleDims(dataStructure); } + +TEST_CASE("SimplnxCore::AlignSectionsFeatureCentroidFilter: Geometry cell tuple count", "[Reconstruction][AlignSectionsFeatureCentroidFilter][TupleCount]") +{ + using FilterType = AlignSectionsFeatureCentroidFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{2, 3, 2}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + // Matching arrays remain valid outside the geometry's Cell Attribute Matrix, with a different tuple shape. + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 0), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_MaskArrayPath_Key, std::make_any(inputPath)); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -68071); + REQUIRE(errorRef.message.find(inputPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", tuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/ComputeBoundingBoxStatsTest.cpp b/src/Plugins/SimplnxCore/test/ComputeBoundingBoxStatsTest.cpp index 41c97c9378..29c67a926e 100644 --- a/src/Plugins/SimplnxCore/test/ComputeBoundingBoxStatsTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeBoundingBoxStatsTest.cpp @@ -1,6 +1,10 @@ #include "SimplnxCore/Filters/ComputeBoundingBoxStatsFilter.hpp" #include "SimplnxCore/SimplnxCore_test_dirs.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataStore.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/UnitTest/UnitTestCommon.hpp" #include "simplnx/Utilities/AlgorithmDispatch.hpp" #include "simplnx/Utilities/DataStoreUtilities.hpp" @@ -1136,3 +1140,49 @@ TEST_CASE("SimplnxCore::ComputeBoundingBoxStatsFilter: Attribute Matrix Handling SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); } } + +TEST_CASE("SimplnxCore::ComputeBoundingBoxStatsFilter: Geometry cell tuple count", "[SimplnxCore][ComputeBoundingBoxStatsFilter][TupleCount]") +{ + using FilterType = ComputeBoundingBoxStatsFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{2, 3, 2}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + // Matching arrays remain valid outside the geometry's Cell Attribute Matrix, with a different tuple shape. + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 0), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_GeometryPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_InputArrayPath_Key, std::make_any(inputPath)); + REQUIRE(Float32Array::Create(dataStructure, "Bounds", std::make_shared>(ShapeType{1}, ShapeType{6}, 0)) != nullptr); + args.insertOrAssign(FilterType::k_UnifiedBoundsPath_Key, std::make_any(DataPath({"Bounds"}))); + args.insertOrAssign(FilterType::k_CreateNewAM_Key, std::make_any(true)); + args.insertOrAssign(FilterType::k_CalculateMean_Key, std::make_any(true)); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -59204); + REQUIRE(errorRef.message.find(inputPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", tuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/ComputeEuclideanDistMapTest.cpp b/src/Plugins/SimplnxCore/test/ComputeEuclideanDistMapTest.cpp index 637a9ac7c0..2a76830c37 100644 --- a/src/Plugins/SimplnxCore/test/ComputeEuclideanDistMapTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeEuclideanDistMapTest.cpp @@ -4,6 +4,7 @@ #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Parameters/ArrayCreationParameter.hpp" #include "simplnx/Parameters/BoolParameter.hpp" @@ -538,3 +539,46 @@ TEST_CASE("SimplnxCore::ComputeEuclideanDistMapFilter: SIMPL Backwards Compatibi } } } + +TEST_CASE("SimplnxCore::ComputeEuclideanDistMapFilter: Geometry cell tuple count", "[SimplnxCore][ComputeEuclideanDistMapFilter][TupleCount]") +{ + using FilterType = ComputeEuclideanDistMapFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{2, 3, 2}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + // Matching arrays remain valid outside the geometry's Cell Attribute Matrix, with a different tuple shape. + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 0), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_CellFeatureIdsArrayPath_Key, std::make_any(inputPath)); + args.insertOrAssign(FilterType::k_DoBoundaries_Key, std::make_any(true)); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -12805); + REQUIRE(errorRef.message.find(inputPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", tuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/ComputeFeatureCentroidsTest.cpp b/src/Plugins/SimplnxCore/test/ComputeFeatureCentroidsTest.cpp index ba71dd21b1..1a7ad8d848 100644 --- a/src/Plugins/SimplnxCore/test/ComputeFeatureCentroidsTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeFeatureCentroidsTest.cpp @@ -375,3 +375,47 @@ TEST_CASE("SimplnxCore::ComputeFeatureCentroidsFilter: SIMPL Backwards Compatibi } } } + +TEST_CASE("SimplnxCore::ComputeFeatureCentroidsFilter: Geometry cell tuple count", "[SimplnxCore][ComputeFeatureCentroidsFilter][TupleCount]") +{ + using FilterType = ComputeFeatureCentroidsFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{2, 3, 2}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + // Matching arrays remain valid outside the geometry's Cell Attribute Matrix, with a different tuple shape. + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 0), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_CellFeatureIdsArrayPath_Key, std::make_any(inputPath)); + REQUIRE(AttributeMatrix::Create(dataStructure, "Feature Data", ShapeType{2}, imageGeomPtr->getId()) != nullptr); + args.insertOrAssign(FilterType::k_FeatureAttributeMatrixPath_Key, std::make_any(DataPath({"Image", "Feature Data"}))); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -12701); + REQUIRE(errorRef.message.find(inputPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", tuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/ComputeFeatureNeighborsTest.cpp b/src/Plugins/SimplnxCore/test/ComputeFeatureNeighborsTest.cpp index a68f29e076..407d66ad1c 100644 --- a/src/Plugins/SimplnxCore/test/ComputeFeatureNeighborsTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeFeatureNeighborsTest.cpp @@ -2,6 +2,10 @@ #include "SimplnxCore/SimplnxCore_test_dirs.hpp" #include "simplnx/Core/Application.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataStore.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Pipeline/Pipeline.hpp" #include "simplnx/Pipeline/PipelineFilter.hpp" #include "simplnx/UnitTest/UnitTestCommon.hpp" @@ -1265,3 +1269,47 @@ TEST_CASE("SimplnxCore::ComputeFeatureNeighborsFilter: SIMPL Backwards Compatibi } } } + +TEST_CASE("SimplnxCore::ComputeFeatureNeighborsFilter: Geometry cell tuple count", "[SimplnxCore][ComputeFeatureNeighborsFilter][TupleCount]") +{ + using FilterType = ComputeFeatureNeighborsFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{2, 3, 2}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + // Matching arrays remain valid outside the geometry's Cell Attribute Matrix, with a different tuple shape. + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 0), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_FeatureIdsPath_Key, std::make_any(inputPath)); + REQUIRE(AttributeMatrix::Create(dataStructure, "Feature Data", ShapeType{2}, imageGeomPtr->getId()) != nullptr); + args.insertOrAssign(FilterType::k_CellFeaturesPath_Key, std::make_any(DataPath({"Image", "Feature Data"}))); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -24501); + REQUIRE(errorRef.message.find(inputPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", tuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/ComputeFeatureSizesTest.cpp b/src/Plugins/SimplnxCore/test/ComputeFeatureSizesTest.cpp index d32cd9ec19..4da5571525 100644 --- a/src/Plugins/SimplnxCore/test/ComputeFeatureSizesTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeFeatureSizesTest.cpp @@ -1292,6 +1292,8 @@ TEST_CASE("SimplnxCore::ComputeFeatureSizes: Invalid: Preflight Failure", "[Simp // A one-dimensional X extent is invalid for this filter. imageGeom.setDimensions(SizeVec3{std::array{5, 1, 1}}); { + auto resizeResult = dataStructure.getDataRefAs(Test::k_CellAMPath).resizeTuples({1, 1, 5}); + SIMPLNX_RESULT_REQUIRE_VALID(resizeResult); ComputeFeatureSizesFilter filter; Arguments args; @@ -1304,11 +1306,17 @@ TEST_CASE("SimplnxCore::ComputeFeatureSizes: Invalid: Preflight Failure", "[Simp args.insert(ComputeFeatureSizesFilter::k_NumElementsName_Key, std::make_any(Test::k_NumElementsName)); auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + CHECK(preflightResult.outputActions.errors()[0].code == -74770); + CHECK(preflightResult.outputActions.errors()[0].message.find("requires at least 2 valid dimensions") != std::string::npos); + CHECK(preflightResult.outputActions.errors()[0].message.find("Two or more 1's") != std::string::npos); } // A one-dimensional Y extent is invalid for this filter. imageGeom.setDimensions(SizeVec3{std::array{1, 5, 1}}); { + auto resizeResult = dataStructure.getDataRefAs(Test::k_CellAMPath).resizeTuples({1, 5, 1}); + SIMPLNX_RESULT_REQUIRE_VALID(resizeResult); ComputeFeatureSizesFilter filter; Arguments args; @@ -1321,11 +1329,17 @@ TEST_CASE("SimplnxCore::ComputeFeatureSizes: Invalid: Preflight Failure", "[Simp args.insert(ComputeFeatureSizesFilter::k_NumElementsName_Key, std::make_any(Test::k_NumElementsName)); auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + CHECK(preflightResult.outputActions.errors()[0].code == -74770); + CHECK(preflightResult.outputActions.errors()[0].message.find("requires at least 2 valid dimensions") != std::string::npos); + CHECK(preflightResult.outputActions.errors()[0].message.find("Two or more 1's") != std::string::npos); } // A one-dimensional Z extent is invalid for this filter. imageGeom.setDimensions(SizeVec3{std::array{1, 1, 5}}); { + auto resizeResult = dataStructure.getDataRefAs(Test::k_CellAMPath).resizeTuples({5, 1, 1}); + SIMPLNX_RESULT_REQUIRE_VALID(resizeResult); ComputeFeatureSizesFilter filter; Arguments args; @@ -1338,11 +1352,17 @@ TEST_CASE("SimplnxCore::ComputeFeatureSizes: Invalid: Preflight Failure", "[Simp args.insert(ComputeFeatureSizesFilter::k_NumElementsName_Key, std::make_any(Test::k_NumElementsName)); auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + CHECK(preflightResult.outputActions.errors()[0].code == -74770); + CHECK(preflightResult.outputActions.errors()[0].message.find("requires at least 2 valid dimensions") != std::string::npos); + CHECK(preflightResult.outputActions.errors()[0].message.find("Two or more 1's") != std::string::npos); } // A single-cell geometry is also invalid for this filter. imageGeom.setDimensions(SizeVec3{std::array{1, 1, 1}}); { + auto resizeResult = dataStructure.getDataRefAs(Test::k_CellAMPath).resizeTuples({1, 1, 1}); + SIMPLNX_RESULT_REQUIRE_VALID(resizeResult); ComputeFeatureSizesFilter filter; Arguments args; @@ -1355,6 +1375,10 @@ TEST_CASE("SimplnxCore::ComputeFeatureSizes: Invalid: Preflight Failure", "[Simp args.insert(ComputeFeatureSizesFilter::k_NumElementsName_Key, std::make_any(Test::k_NumElementsName)); auto preflightResult = filter.preflight(dataStructure, args); SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + CHECK(preflightResult.outputActions.errors()[0].code == -74770); + CHECK(preflightResult.outputActions.errors()[0].message.find("requires at least 2 valid dimensions") != std::string::npos); + CHECK(preflightResult.outputActions.errors()[0].message.find("Two or more 1's") != std::string::npos); } // The optional output supports manual inspection of the feature-size arrays. @@ -1470,3 +1494,57 @@ TEST_CASE("SimplnxCore::ComputeFeatureSizesFilter: SIMPL Backwards Compatibility } } } + +TEST_CASE("SimplnxCore::ComputeFeatureSizes: Geometry cell tuple count", "[SimplnxCore][ComputeFeatureSizes][TupleCount]") +{ + using FilterType = ComputeFeatureSizesFilter; + DataStructure dataStructure; + const bool useRectGrid = GENERATE(false, true); + CAPTURE(useRectGrid); + IGridGeometry* gridGeomPtr = nullptr; + if(useRectGrid) + { + gridGeomPtr = RectGridGeom::Create(dataStructure, "Grid"); + } + else + { + gridGeomPtr = ImageGeom::Create(dataStructure, "Grid"); + } + REQUIRE(gridGeomPtr != nullptr); + gridGeomPtr->setDimensions({4, 3, 1}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{1, 3, 4}, gridGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + gridGeomPtr->setCellData(*cellAMPtr); + + // Equal counts remain valid in another Attribute Matrix with a different tuple shape. + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 0), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_GeometryPath_Key, std::make_any(DataPath({"Grid"}))); + args.insertOrAssign(FilterType::k_CellFeatureIdsArrayPath_Key, std::make_any(inputPath)); + auto* featureAMPtr = AttributeMatrix::Create(dataStructure, "Feature Data", ShapeType{2}, gridGeomPtr->getId()); + REQUIRE(featureAMPtr != nullptr); + args.insertOrAssign(FilterType::k_CellFeatureAttributeMatrixPath_Key, std::make_any(DataPath({"Grid", "Feature Data"}))); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -74771); + REQUIRE(errorRef.message.find(inputPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", tuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Grid' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/ComputeMomentInvariants2DTest.cpp b/src/Plugins/SimplnxCore/test/ComputeMomentInvariants2DTest.cpp index 928f07d806..b553595b46 100644 --- a/src/Plugins/SimplnxCore/test/ComputeMomentInvariants2DTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeMomentInvariants2DTest.cpp @@ -13,6 +13,9 @@ #include "simplnx/Common/Numbers.hpp" #include "simplnx/Core/Application.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Pipeline/Pipeline.hpp" #include "simplnx/Pipeline/PipelineFilter.hpp" @@ -246,3 +249,49 @@ TEST_CASE("SimplnxCore::ComputeMomentInvariants2DFilter: SIMPL Backwards Compati } } } + +TEST_CASE("SimplnxCore::ComputeMomentInvariants2DFilter: Geometry cell tuple count", "[SimplnxCore][ComputeMomentInvariants2DFilter][TupleCount]") +{ + using FilterType = ComputeMomentInvariants2DFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Grid"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({4, 3, 1}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{1, 3, 4}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + // Equal counts remain valid in another Attribute Matrix with a different tuple shape. + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 0), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_ImageGeometryPath_Key, std::make_any(DataPath({"Grid"}))); + args.insertOrAssign(FilterType::k_FeatureIdsArrayPath_Key, std::make_any(inputPath)); + auto* featureAMPtr = AttributeMatrix::Create(dataStructure, "Feature Data", ShapeType{2}, imageGeomPtr->getId()); + REQUIRE(featureAMPtr != nullptr); + args.insertOrAssign(FilterType::k_FeatureAttributeMatrixPath_Key, std::make_any(DataPath({"Grid", "Feature Data"}))); + REQUIRE(DataArray::Create(dataStructure, "Feature Rect", std::make_shared>(ShapeType{2}, ShapeType{6}, 0), featureAMPtr->getId()) != nullptr); + args.insertOrAssign(FilterType::k_FeatureRectArrayPath_Key, std::make_any(DataPath({"Grid", "Feature Data", "Feature Rect"}))); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -9511); + REQUIRE(errorRef.message.find(inputPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", tuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Grid' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/ComputeSurfaceFeaturesTest.cpp b/src/Plugins/SimplnxCore/test/ComputeSurfaceFeaturesTest.cpp index 09f3547aa0..f763455d15 100644 --- a/src/Plugins/SimplnxCore/test/ComputeSurfaceFeaturesTest.cpp +++ b/src/Plugins/SimplnxCore/test/ComputeSurfaceFeaturesTest.cpp @@ -5,6 +5,7 @@ #include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Parameters/DynamicTableParameter.hpp" #include "simplnx/UnitTest/UnitTestCommon.hpp" @@ -323,3 +324,47 @@ TEST_CASE("SimplnxCore::ComputeSurfaceFeaturesFilter: 200x200x200 block features UnitTest::CheckArraysInheritTupleDims(dataStructure); } + +TEST_CASE("SimplnxCore::ComputeSurfaceFeatures: Geometry cell tuple count", "[SimplnxCore][ComputeSurfaceFeatures][TupleCount]") +{ + using FilterType = ComputeSurfaceFeaturesFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Grid"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({4, 3, 1}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{1, 3, 4}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + // Equal counts remain valid in another Attribute Matrix with a different tuple shape. + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 0), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_FeatureGeometryPath_Key, std::make_any(DataPath({"Grid"}))); + args.insertOrAssign(FilterType::k_CellFeatureIdsArrayPath_Key, std::make_any(inputPath)); + auto* featureAMPtr = AttributeMatrix::Create(dataStructure, "Feature Data", ShapeType{2}, imageGeomPtr->getId()); + REQUIRE(featureAMPtr != nullptr); + args.insertOrAssign(FilterType::k_CellFeatureAttributeMatrixPath_Key, std::make_any(DataPath({"Grid", "Feature Data"}))); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -1099); + REQUIRE(errorRef.message.find(inputPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", tuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Grid' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/ErodeDilateMaskTest.cpp b/src/Plugins/SimplnxCore/test/ErodeDilateMaskTest.cpp index f67186bc20..b304631bed 100644 --- a/src/Plugins/SimplnxCore/test/ErodeDilateMaskTest.cpp +++ b/src/Plugins/SimplnxCore/test/ErodeDilateMaskTest.cpp @@ -9,6 +9,7 @@ #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataStore.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Parameters/ArraySelectionParameter.hpp" #include "simplnx/Parameters/BoolParameter.hpp" @@ -820,3 +821,44 @@ TEST_CASE("SimplnxCore::ErodeDilateMaskFilter: SIMPL Backwards Compatibility", " } } } + +TEST_CASE("SimplnxCore::ErodeDilateMaskFilter: Geometry cell tuple count", "[SimplnxCore][ErodeDilateMaskFilter][TupleCount]") +{ + using FilterType = ErodeDilateMaskFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Grid"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({4, 3, 1}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{1, 3, 4}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + // Equal counts remain valid in another Attribute Matrix with a different tuple shape. + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 0), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"Grid"}))); + args.insertOrAssign(FilterType::k_MaskArrayPath_Key, std::make_any(inputPath)); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -14702); + REQUIRE(errorRef.message.find(inputPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", tuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Grid' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/ExtractFeatureBoundaries2DTest.cpp b/src/Plugins/SimplnxCore/test/ExtractFeatureBoundaries2DTest.cpp index 543ee65a5d..5b8b0e5381 100644 --- a/src/Plugins/SimplnxCore/test/ExtractFeatureBoundaries2DTest.cpp +++ b/src/Plugins/SimplnxCore/test/ExtractFeatureBoundaries2DTest.cpp @@ -281,3 +281,47 @@ TEST_CASE("SimplnxCore::ExtractFeatureBoundaries2DFilter: Z Min No Edges", "[Sim VerifyOutput(dataStructure, DataPath({"Feature Boundaries Min Z No Edges"}), k_OutputEdgeGeometryPath); UnitTest::CheckArraysInheritTupleDims(dataStructure); } + +TEST_CASE("SimplnxCore::ExtractFeatureBoundaries2DFilter: Geometry cell tuple count", "[SimplnxCore][ExtractFeatureBoundaries2DFilter][TupleCount]") +{ + using FilterType = ExtractFeatureBoundaries2DFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({4, 3, 1}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{12}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + // A matching array is valid outside Cell Data; short and long arrays must fail preflight. + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(otherAMPtr->getShape(), ShapeType{1}, 0), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_InputImageGeometryPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_FeatureIdsArrayPath_Key, std::make_any(inputPath)); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -1019); + const DataPath expectedPath = inputPath; + const usize expectedTuples = tuples; + REQUIRE(errorRef.message.find(expectedPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", expectedTuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/FlyingEdges3DTest.cpp b/src/Plugins/SimplnxCore/test/FlyingEdges3DTest.cpp index 21eb9a96e7..a44b9e225f 100644 --- a/src/Plugins/SimplnxCore/test/FlyingEdges3DTest.cpp +++ b/src/Plugins/SimplnxCore/test/FlyingEdges3DTest.cpp @@ -1,6 +1,9 @@ #include "SimplnxCore/Filters/FlyingEdges3DFilter.hpp" #include "SimplnxCore/SimplnxCore_test_dirs.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/DataStructure/Geometry/TriangleGeom.hpp" #include "simplnx/Parameters/ArrayCreationParameter.hpp" #include "simplnx/Parameters/DataGroupCreationParameter.hpp" @@ -103,3 +106,47 @@ TEST_CASE("SimplnxCore::Image Contouring Valid Execution", "[SimplnxCore][Flying UnitTest::CheckArraysInheritTupleDims(dataStructure); } + +TEST_CASE("SimplnxCore::Image Contouring FlyingEdges3D: Geometry cell tuple count", "[SimplnxCore][FlyingEdges3D][TupleCount]") +{ + using FilterType = FlyingEdges3DFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{12}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + // A matching array is valid outside Cell Data; short and long arrays must fail preflight. + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(otherAMPtr->getShape(), ShapeType{1}, 0), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_SelectedDataArrayPath_Key, std::make_any(inputPath)); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -10510); + const DataPath expectedPath = inputPath; + const usize expectedTuples = tuples; + REQUIRE(errorRef.message.find(expectedPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", expectedTuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/IdentifySampleTest.cpp b/src/Plugins/SimplnxCore/test/IdentifySampleTest.cpp index 2d92dc2685..ba4e199ff1 100644 --- a/src/Plugins/SimplnxCore/test/IdentifySampleTest.cpp +++ b/src/Plugins/SimplnxCore/test/IdentifySampleTest.cpp @@ -336,3 +336,47 @@ TEST_CASE("SimplnxCore::IdentifySampleFilter: 2D Empty X Non-Square {1,3,4}", "[ DataStructure dataStructure = ::CreateNonSquare2DMaskDataStructure(SizeVec3{1, 3, 4}); scope.execute([&] { ::RunIdentifySampleAndCheck(dataStructure); }); } + +TEST_CASE("SimplnxCore::IdentifySampleFilter: Geometry cell tuple count", "[SimplnxCore][IdentifySampleFilter][TupleCount]") +{ + using FilterType = IdentifySampleFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{12}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + // A matching array is valid outside Cell Data; short and long arrays must fail preflight. + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(otherAMPtr->getShape(), ShapeType{1}, 0), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_MaskArrayPath_Key, std::make_any(inputPath)); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -45463); + const DataPath expectedPath = inputPath; + const usize expectedTuples = tuples; + REQUIRE(errorRef.message.find(expectedPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", expectedTuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/InitializeImageGeomCellDataTest.cpp b/src/Plugins/SimplnxCore/test/InitializeImageGeomCellDataTest.cpp index 7fd810ae02..8b76979390 100644 --- a/src/Plugins/SimplnxCore/test/InitializeImageGeomCellDataTest.cpp +++ b/src/Plugins/SimplnxCore/test/InitializeImageGeomCellDataTest.cpp @@ -3,6 +3,7 @@ #include "simplnx/Common/TypeTraits.hpp" #include "simplnx/Core/Application.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataArray.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Parameters/ChoicesParameter.hpp" @@ -452,3 +453,80 @@ TEST_CASE("SimplnxCore::InitializeImageGeomCellDataFilter: SIMPL Backwards Compa } } } + +TEST_CASE("SimplnxCore::InitializeImageGeomCellDataFilter: Geometry cell tuple count", "[SimplnxCore][InitializeImageGeomCellDataFilter][TupleCount]") +{ + using FilterType = InitializeImageGeomCellDataFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{12}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + // A matching array is valid outside Cell Data; short and long arrays must fail preflight. + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{1, 1, tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(otherAMPtr->getShape(), ShapeType{1}, 0), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + const bool shortSecondInput = GENERATE(false, true); + CAPTURE(shortSecondInput); + auto* secondAMPtr = AttributeMatrix::Create(dataStructure, "Second Data", ShapeType{1, 1, shortSecondInput ? usize{10} : usize{12}}); + REQUIRE(secondAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Second Values", std::make_shared>(secondAMPtr->getShape(), ShapeType{1}, 0), secondAMPtr->getId()) != nullptr); + const DataPath secondPath({"Second Data", "Second Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_ImageGeometryPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_CellArrayPaths_Key, std::make_any>(std::vector{inputPath, secondPath})); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12 && !shortSecondInput) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -3557); + const DataPath expectedPath = tuples != 12 ? inputPath : secondPath; + const usize expectedTuples = tuples != 12 ? tuples : usize{10}; + REQUIRE(errorRef.message.find(expectedPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", expectedTuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} + +TEST_CASE("SimplnxCore::InitializeImageGeomCellDataFilter: External tuple shapes", "[SimplnxCore][InitializeImageGeomCellDataFilter][TupleCount]") +{ + const auto tupleShape = GENERATE((ShapeType{12}), (ShapeType{3, 4}), (ShapeType{1, 1, 12})); + CAPTURE(tupleShape); + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({3, 4, 1}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{1, 4, 3}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", tupleShape); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(Int32Array::CreateWithStore>(dataStructure, "Values", tupleShape, ShapeType{1}, otherAMPtr->getId()) != nullptr); + + InitializeImageGeomCellDataFilter filter; + Arguments args; + args.insertOrAssign(InitializeImageGeomCellDataFilter::k_ImageGeometryPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(InitializeImageGeomCellDataFilter::k_CellArrayPaths_Key, std::make_any>(std::vector{DataPath({"Other Data", "Values"})})); + args.insertOrAssign(InitializeImageGeomCellDataFilter::k_MinPoint_Key, std::make_any>(std::vector{0, 0, 0})); + args.insertOrAssign(InitializeImageGeomCellDataFilter::k_MaxPoint_Key, std::make_any>(std::vector{2, 3, 0})); + + const auto result = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} diff --git a/src/Plugins/SimplnxCore/test/M3CSurfaceMeshingTest.cpp b/src/Plugins/SimplnxCore/test/M3CSurfaceMeshingTest.cpp index a8a7481355..a63102b6c2 100644 --- a/src/Plugins/SimplnxCore/test/M3CSurfaceMeshingTest.cpp +++ b/src/Plugins/SimplnxCore/test/M3CSurfaceMeshingTest.cpp @@ -667,8 +667,8 @@ TEST_CASE("SimplnxCore::M3CSurfaceMeshingFilter: Preflight Error Paths", "[Simpl { UnitTest::LoadPlugins(); - // Error -90200 requires selected cell arrays and FeatureIds to have equal tuple counts. - SECTION("Cell transfer array tuple mismatch -> error -90200") + // Error -90201 requires selected cell arrays to match the geometry cell count. + SECTION("Cell transfer array tuple mismatch -> error -90201") { DataStructure dataStructure; auto* imageGeom = ImageGeom::Create(dataStructure, "ImageGeometry"); @@ -695,15 +695,15 @@ TEST_CASE("SimplnxCore::M3CSurfaceMeshingFilter: Preflight Error Paths", "[Simpl M3CSurfaceMeshingFilter filter; auto preflightResult = filter.preflight(dataStructure, args); REQUIRE(preflightResult.outputActions.invalid()); - bool has90200 = false; + bool has90201 = false; for(const auto& err : preflightResult.outputActions.errors()) { - if(err.code == -90200) + if(err.code == -90201) { - has90200 = true; + has90201 = true; } } - REQUIRE(has90200); + REQUIRE(has90201); } // M3C node coordinates require uniform cell spacing, so preflight rejects RectGridGeom input. @@ -1537,3 +1537,54 @@ TEST_CASE("SimplnxCore::M3CSurfaceMeshingFilter: Saddle tie does not add handles UnitTest::CheckArraysInheritTupleDims(dataStructure); } + +TEST_CASE("SimplnxCore::M3CSurfaceMeshingFilter: Geometry cell tuple count", "[SimplnxCore][M3CSurfaceMeshingFilter][TupleCount]") +{ + using FilterType = M3CSurfaceMeshingFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{12}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + // A matching array is valid outside Cell Data; short and long arrays must fail preflight. + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(otherAMPtr->getShape(), ShapeType{1}, 0), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + const bool shortSecondInput = GENERATE(false, true); + CAPTURE(shortSecondInput); + auto* secondAMPtr = AttributeMatrix::Create(dataStructure, "Second Data", ShapeType{1, 1, shortSecondInput ? usize{10} : tuples}); + REQUIRE(secondAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Second Values", std::make_shared>(secondAMPtr->getShape(), ShapeType{1}, 0), secondAMPtr->getId()) != nullptr); + const DataPath secondPath({"Second Data", "Second Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_GridGeometryDataPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_FeatureIdsArrayPath_Key, std::make_any(inputPath)); + args.insertOrAssign(FilterType::k_SelectedDataArrayPaths_Key, std::make_any>(std::vector{secondPath})); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12 && !shortSecondInput) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -90201); + const DataPath expectedPath = tuples != 12 ? inputPath : secondPath; + const usize expectedTuples = tuples != 12 ? tuples : usize{10}; + REQUIRE(errorRef.message.find(expectedPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", expectedTuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/NearestPointFuseRegularGridsTest.cpp b/src/Plugins/SimplnxCore/test/NearestPointFuseRegularGridsTest.cpp index bf4cb7030a..e48198ff5b 100644 --- a/src/Plugins/SimplnxCore/test/NearestPointFuseRegularGridsTest.cpp +++ b/src/Plugins/SimplnxCore/test/NearestPointFuseRegularGridsTest.cpp @@ -395,3 +395,57 @@ TEST_CASE("SimplnxCore::NearestPointFuseRegularGridsFilter: SIMPL Backwards Comp } } } + +TEST_CASE("SimplnxCore::NearestPointFuseRegularGridsFilter: Geometry cell tuple count", "[SimplnxCore][NearestPointFuseRegularGridsFilter][TupleCount]") +{ + using FilterType = NearestPointFuseRegularGridsFilter; + DataStructure dataStructure; + auto* samplingGeomPtr = ImageGeom::Create(dataStructure, "Sampling"); + auto* referenceGeomPtr = ImageGeom::Create(dataStructure, "Reference"); + REQUIRE(samplingGeomPtr != nullptr); + REQUIRE(referenceGeomPtr != nullptr); + samplingGeomPtr->setDimensions({2, 3, 2}); + referenceGeomPtr->setDimensions({2, 5, 2}); + samplingGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + referenceGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* samplingCellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{12}, samplingGeomPtr->getId()); + auto* referenceCellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{20}, referenceGeomPtr->getId()); + REQUIRE(samplingCellAMPtr != nullptr); + REQUIRE(referenceCellAMPtr != nullptr); + samplingGeomPtr->setCellData(*samplingCellAMPtr); + referenceGeomPtr->setCellData(*referenceCellAMPtr); + + const auto counts = GENERATE((std::pair{11, 20}), (std::pair{12, 19}), (std::pair{12, 20}), (std::pair{13, 20}), + (std::pair{12, 21}), (std::pair{0, 20}), (std::pair{12, 0})); + const auto [samplingTuples, referenceTuples] = counts; + CAPTURE(samplingTuples, referenceTuples); + // Both selected matrices are outside their geometry's Cell Data; the reference matrix starts empty. + auto* sampleAMPtr = AttributeMatrix::Create(dataStructure, "Other Sampling Data", ShapeType{samplingTuples}); + REQUIRE(sampleAMPtr != nullptr); + REQUIRE(AttributeMatrix::Create(dataStructure, "Other Reference Data", ShapeType{referenceTuples}) != nullptr); + REQUIRE(Int32Array::Create(dataStructure, "Values", std::make_shared(ShapeType{samplingTuples}, ShapeType{1}, 0), sampleAMPtr->getId()) != nullptr); + REQUIRE(StringArray::CreateWithValues(dataStructure, "Strings", ShapeType{samplingTuples}, std::vector(samplingTuples, "value"), sampleAMPtr->getId()) != nullptr); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_SamplingGeometryPath_Key, std::make_any(DataPath({"Sampling"}))); + args.insertOrAssign(FilterType::k_ReferenceGeometryPath_Key, std::make_any(DataPath({"Reference"}))); + args.insertOrAssign(FilterType::k_SamplingCellAttributeMatrixPath_Key, std::make_any(DataPath({"Other Sampling Data"}))); + args.insertOrAssign(FilterType::k_ReferenceCellAttributeMatrixPath_Key, std::make_any(DataPath({"Other Reference Data"}))); + const auto result = filter.preflight(dataStructure, args); + if(samplingTuples == 12 && referenceTuples == 20) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -5565); + const bool samplingMismatch = samplingTuples != 12; + REQUIRE(errorRef.message.find(samplingMismatch ? "Other Sampling Data" : "Other Reference Data") != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", samplingMismatch ? samplingTuples : referenceTuples)) != std::string::npos); + REQUIRE(errorRef.message.find(samplingMismatch ? "'Sampling' has 12 cells" : "'Reference' has 20 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/PartitionGeometryTest.cpp b/src/Plugins/SimplnxCore/test/PartitionGeometryTest.cpp index 062a39e119..569da4fc67 100644 --- a/src/Plugins/SimplnxCore/test/PartitionGeometryTest.cpp +++ b/src/Plugins/SimplnxCore/test/PartitionGeometryTest.cpp @@ -4,16 +4,37 @@ */ #include +#include +#include #include +#include #include +#include #include #include - +#include +#include +#include +#include + +#include "simplnx/Common/Aliases.hpp" +#include "simplnx/Common/Array.hpp" +#include "simplnx/Common/Result.hpp" +#include "simplnx/Common/Types.hpp" #include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataGroup.hpp" +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/DataStructure.hpp" +#include "simplnx/DataStructure/Geometry/IGridGeometry.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/DataStructure/Geometry/RectGridGeom.hpp" +#include "simplnx/DataStructure/Geometry/VertexGeom.hpp" +#include "simplnx/DataStructure/NeighborList.hpp" +#include "simplnx/DataStructure/StringArray.hpp" +#include "simplnx/Filter/Arguments.hpp" +#include "simplnx/Filter/IFilter.hpp" #include "simplnx/Parameters/ArrayCreationParameter.hpp" #include "simplnx/Parameters/BoolParameter.hpp" #include "simplnx/Parameters/ChoicesParameter.hpp" @@ -213,6 +234,152 @@ using FileSentinelType = nx::core::UnitTest::TestFileSentinel; using SharedFileSentinelType = std::shared_ptr; // One shared sentinel keeps the extracted geometry fixtures available across each parameterized loop. SharedFileSentinelType s_FileSentinel; + +const DataPath k_SelectedTupleGeometryPath({"Selected", "Geometries", "Geometry"}); +const DataPath k_SelectedTupleMatrixPath({"Selected", "Arrays", "CellData"}); +const DataPath k_SelectedVertexTupleMatrixPath({"Selected", "Arrays", "VertexData"}); +const DataPath k_ExistingTuplePartitionGridPath({"ExistingTuplePartitionGrid"}); + +/** + * @brief Creates an eight-cell grid and separately stored selected cell data. + * @param dataStructure Receives the fixture and linked selected paths. + * @param rectilinear Chooses a rectilinear grid instead of an image geometry. + * @param tupleCount Number of tuples in the selected AttributeMatrix. + */ +void createCellTupleValidationInput(DataStructure& dataStructure, bool rectilinear, usize tupleCount) +{ + auto* storedGeometryGroupPtr = DataGroup::Create(dataStructure, "StoredGeometry"); + REQUIRE(storedGeometryGroupPtr != nullptr); + IGridGeometry* geometryPtr = nullptr; + if(rectilinear) + { + auto* rectGridPtr = RectGridGeom::Create(dataStructure, "Geometry", storedGeometryGroupPtr->getId()); + REQUIRE(rectGridPtr != nullptr); + rectGridPtr->setDimensions({2, 2, 2}); + auto* xBoundsPtr = UnitTest::CreateTestDataArray(dataStructure, "XBounds", {3}, {1}, rectGridPtr->getId()); + auto* yBoundsPtr = UnitTest::CreateTestDataArray(dataStructure, "YBounds", {3}, {1}, rectGridPtr->getId()); + auto* zBoundsPtr = UnitTest::CreateTestDataArray(dataStructure, "ZBounds", {3}, {1}, rectGridPtr->getId()); + REQUIRE(xBoundsPtr != nullptr); + REQUIRE(yBoundsPtr != nullptr); + REQUIRE(zBoundsPtr != nullptr); + for(usize index = 0; index < 3; index++) + { + (*xBoundsPtr)[index] = static_cast(index); + (*yBoundsPtr)[index] = static_cast(index); + (*zBoundsPtr)[index] = static_cast(index); + } + rectGridPtr->setXBoundsId(xBoundsPtr->getId()); + rectGridPtr->setYBoundsId(yBoundsPtr->getId()); + rectGridPtr->setZBoundsId(zBoundsPtr->getId()); + geometryPtr = rectGridPtr; + } + else + { + auto* imagePtr = ImageGeom::Create(dataStructure, "Geometry", storedGeometryGroupPtr->getId()); + REQUIRE(imagePtr != nullptr); + imagePtr->setDimensions({2, 2, 2}); + imagePtr->setSpacing({1.0F, 1.0F, 1.0F}); + imagePtr->setOrigin({0.0F, 0.0F, 0.0F}); + geometryPtr = imagePtr; + } + auto* geometryCellDataPtr = AttributeMatrix::Create(dataStructure, "GeometryCellData", {2, 2, 2}, geometryPtr->getId()); + REQUIRE(geometryCellDataPtr != nullptr); + geometryPtr->setCellData(*geometryCellDataPtr); + auto* storedDataGroupPtr = DataGroup::Create(dataStructure, "StoredData"); + REQUIRE(storedDataGroupPtr != nullptr); + // {4, 2} has eight tuples, with neither shape nor ownership matching the grid. + const ShapeType tupleShape = tupleCount == 8 ? ShapeType{4, 2} : ShapeType{tupleCount}; + auto* selectedMatrixPtr = AttributeMatrix::Create(dataStructure, "CellData", tupleShape, storedDataGroupPtr->getId()); + REQUIRE(selectedMatrixPtr != nullptr); + auto* selectedPtr = DataGroup::Create(dataStructure, "Selected"); + REQUIRE(selectedPtr != nullptr); + auto* selectedGeometriesPtr = DataGroup::Create(dataStructure, "Geometries", selectedPtr->getId()); + auto* selectedArraysPtr = DataGroup::Create(dataStructure, "Arrays", selectedPtr->getId()); + REQUIRE(selectedGeometriesPtr != nullptr); + REQUIRE(selectedArraysPtr != nullptr); + REQUIRE(dataStructure.setAdditionalParent(geometryPtr->getId(), selectedGeometriesPtr->getId())); + REQUIRE(dataStructure.setAdditionalParent(selectedMatrixPtr->getId(), selectedArraysPtr->getId())); +} + +/** + * @brief Configures one of the three modes that creates a partition grid. + * @param mode Basic, advanced, or bounding-box mode index. + * @return Arguments using the linked selected geometry and matrix paths. + */ +Arguments createCellTupleValidationArguments(ChoicesParameter::ValueType mode) +{ + Arguments args = createBasicPartitionGeometryArguments(k_SelectedTupleGeometryPath, k_SelectedTupleMatrixPath, "PartitionIds", {2, 2, 2}, {}); + args.insertOrAssign(PartitionGeometryFilter::k_PartitioningMode_Key, mode); + args.insertOrAssign(PartitionGeometryFilter::k_CellLength_Key, std::vector{1.0F, 1.0F, 1.0F}); + args.insertOrAssign(PartitionGeometryFilter::k_MinGridCoord_Key, std::vector{0.0F, 0.0F, 0.0F}); + args.insertOrAssign(PartitionGeometryFilter::k_MaxGridCoord_Key, std::vector{2.0F, 2.0F, 2.0F}); + return args; +} + +/** + * @brief Creates eight cube corners and separately stored selected vertex data. + * @param dataStructure Receives the geometry, linked selections, and an existing partition grid. + * @param tupleCount Number of tuples in the selected AttributeMatrix. + */ +void createVertexTupleValidationInput(DataStructure& dataStructure, usize tupleCount) +{ + auto* storedGeometryGroupPtr = DataGroup::Create(dataStructure, "StoredGeometry"); + REQUIRE(storedGeometryGroupPtr != nullptr); + auto* geometryPtr = VertexGeom::Create(dataStructure, "Geometry", storedGeometryGroupPtr->getId()); + REQUIRE(geometryPtr != nullptr); + const DataPath coordinatesPath({"StoredGeometry", "Geometry", "Coordinates"}); + auto vertexStorePtr = DataStoreUtilities::CreateDataStore(dataStructure, coordinatesPath, {8}, {3}); + REQUIRE(vertexStorePtr != nullptr); + auto* coordinatesPtr = Float32Array::Create(dataStructure, coordinatesPath.getTargetName(), vertexStorePtr, geometryPtr->getId()); + REQUIRE(coordinatesPtr != nullptr); + constexpr std::array cubeCorners = {0.0F, 0.0F, 0.0F, 1.0F, 0.0F, 0.0F, 0.0F, 1.0F, 0.0F, 1.0F, 1.0F, 0.0F, 0.0F, 0.0F, 1.0F, 1.0F, 0.0F, 1.0F, 0.0F, 1.0F, 1.0F, 1.0F, 1.0F, 1.0F}; + const auto writeResult = vertexStorePtr->copyFromBuffer(0, nonstd::span(cubeCorners.data(), cubeCorners.size())); + SIMPLNX_RESULT_REQUIRE_VALID(writeResult); + geometryPtr->setVertices(*coordinatesPtr); + auto* geometryVertexDataPtr = AttributeMatrix::Create(dataStructure, "GeometryVertexData", {8}, geometryPtr->getId()); + REQUIRE(geometryVertexDataPtr != nullptr); + geometryPtr->setVertexAttributeMatrix(*geometryVertexDataPtr); + + auto* storedDataGroupPtr = DataGroup::Create(dataStructure, "StoredData"); + REQUIRE(storedDataGroupPtr != nullptr); + // The selected matrix can have a different tuple shape and parent than the vertex list. + const ShapeType tupleShape = tupleCount == 8 ? ShapeType{4, 2} : ShapeType{tupleCount}; + auto* selectedMatrixPtr = AttributeMatrix::Create(dataStructure, "VertexData", tupleShape, storedDataGroupPtr->getId()); + REQUIRE(selectedMatrixPtr != nullptr); + auto* selectedPtr = DataGroup::Create(dataStructure, "Selected"); + REQUIRE(selectedPtr != nullptr); + auto* selectedGeometriesPtr = DataGroup::Create(dataStructure, "Geometries", selectedPtr->getId()); + auto* selectedArraysPtr = DataGroup::Create(dataStructure, "Arrays", selectedPtr->getId()); + REQUIRE(selectedGeometriesPtr != nullptr); + REQUIRE(selectedArraysPtr != nullptr); + REQUIRE(dataStructure.setAdditionalParent(geometryPtr->getId(), selectedGeometriesPtr->getId())); + REQUIRE(dataStructure.setAdditionalParent(selectedMatrixPtr->getId(), selectedArraysPtr->getId())); + REQUIRE(dataStructure.getDataAs(k_SelectedTupleGeometryPath) == geometryPtr); + REQUIRE(dataStructure.getDataAs(k_SelectedVertexTupleMatrixPath) == selectedMatrixPtr); + REQUIRE(geometryPtr->getNumberOfVertices() == 8); + REQUIRE(selectedMatrixPtr->getNumberOfTuples() == tupleCount); + + auto* existingGridPtr = ImageGeom::Create(dataStructure, k_ExistingTuplePartitionGridPath.getTargetName()); + REQUIRE(existingGridPtr != nullptr); + existingGridPtr->setDimensions({2, 2, 2}); + existingGridPtr->setOrigin({0.0F, 0.0F, 0.0F}); + existingGridPtr->setSpacing({1.0F, 1.0F, 1.0F}); +} + +/** + * @brief Configures valid partition settings for the selected vertex fixture. + * @param mode Basic, advanced, bounding-box, or existing-grid mode index. + * @return Arguments using the linked selected geometry and vertex matrix paths. + */ +Arguments createVertexTupleValidationArguments(ChoicesParameter::ValueType mode) +{ + Arguments args = createCellTupleValidationArguments(mode); + args.insertOrAssign(PartitionGeometryFilter::k_InputGeometryCellAttributeMatrixPath_Key, k_SelectedVertexTupleMatrixPath); + args.insertOrAssign(PartitionGeometryFilter::k_ExistingPartitionGridPath_Key, k_ExistingTuplePartitionGridPath); + args.insertOrAssign(PartitionGeometryFilter::k_PartitionGridGeometry_Key, DataPath({"CreatedTuplePartitionGrid"})); + args.insertOrAssign(PartitionGeometryFilter::k_FeatureAttrMatrixName_Key, std::string("PartitionData")); + return args; +} } // namespace TEST_CASE("SimplnxCore::PartitionGeometryFilter: Basic RectGrid bounds", "[Plugins][PartitionGeometryFilter]") @@ -286,6 +453,292 @@ TEST_CASE("SimplnxCore::PartitionGeometryFilter: Basic RectGrid bounds", "[Plugi UnitTest::CheckArraysInheritTupleDims(dataStructure); } +TEST_CASE("SimplnxCore::PartitionGeometryFilter: Selected cell tuple counts", "[Plugins][PartitionGeometryFilter]") +{ + UnitTest::LoadPlugins(); + const bool rectilinear = GENERATE(false, true); + const auto mode = GENERATE(ChoicesParameter::ValueType{0}, ChoicesParameter::ValueType{1}, ChoicesParameter::ValueType{2}); + const usize tupleCount = GENERATE(usize{7}, usize{8}, usize{9}); + CAPTURE(rectilinear, mode, tupleCount); + DataStructure dataStructure; + createCellTupleValidationInput(dataStructure, rectilinear, tupleCount); + const PartitionGeometryFilter filter; + const auto preflightResult = filter.preflight(dataStructure, createCellTupleValidationArguments(mode)); + if(tupleCount == 8) + { + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + const auto& error = preflightResult.outputActions.errors()[0]; + CHECK(error.code == (rectilinear ? -3010 : -3009)); + CHECK(error.message.find(k_SelectedTupleGeometryPath.toString()) != std::string::npos); + CHECK(error.message.find(k_SelectedTupleMatrixPath.toString()) != std::string::npos); + CHECK(error.message.find(fmt::format("has {} tuples", tupleCount)) != std::string::npos); + CHECK(error.message.find("has 8 cells") != std::string::npos); + CHECK(error.message.find("one tuple per geometry cell") != std::string::npos); + } + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::PartitionGeometryFilter: Partition counts precede image tuple validation", "[Plugins][PartitionGeometryFilter]") +{ + UnitTest::LoadPlugins(); + const auto mode = GENERATE(ChoicesParameter::ValueType{0}, ChoicesParameter::ValueType{1}, ChoicesParameter::ValueType{2}); + const usize axis = GENERATE(usize{0}, usize{1}, usize{2}); + CAPTURE(mode, axis); + DataStructure dataStructure; + createCellTupleValidationInput(dataStructure, false, 7); + Arguments args = createCellTupleValidationArguments(mode); + std::vector partitionCounts = {2, 2, 2}; + partitionCounts[axis] = 0; + args.insertOrAssign(PartitionGeometryFilter::k_NumberOfCellsPerAxis_Key, partitionCounts); + // A partition-count error also precedes invalid advanced lengths/bounds. + args.insertOrAssign(PartitionGeometryFilter::k_CellLength_Key, std::vector{-1.0F, -1.0F, -1.0F}); + args.insertOrAssign(PartitionGeometryFilter::k_MinGridCoord_Key, std::vector{3.0F, 3.0F, 3.0F}); + const auto preflightResult = PartitionGeometryFilter().preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + CHECK(preflightResult.outputActions.errors()[0].code == -3012 - static_cast(axis)); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::PartitionGeometryFilter: Selected vertex tuple counts", "[Plugins][PartitionGeometryFilter]") +{ + UnitTest::LoadPlugins(); + const auto mode = GENERATE(ChoicesParameter::ValueType{0}, ChoicesParameter::ValueType{1}, ChoicesParameter::ValueType{2}, ChoicesParameter::ValueType{3}); + const usize tupleCount = GENERATE(usize{7}, usize{8}, usize{9}); + CAPTURE(mode, tupleCount); + DataStructure dataStructure; + createVertexTupleValidationInput(dataStructure, tupleCount); + const auto initialSize = dataStructure.getSize(); + const PartitionGeometryFilter filter; + IFilter::PreflightResult preflightResult; + REQUIRE_NOTHROW(preflightResult = filter.preflight(dataStructure, createVertexTupleValidationArguments(mode))); + if(tupleCount == 8) + { + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + const auto& error = preflightResult.outputActions.errors()[0]; + CHECK(error.code == -3043); + CHECK(error.code != -3014); + CHECK(error.message.find(k_SelectedTupleGeometryPath.toString()) != std::string::npos); + CHECK(error.message.find(k_SelectedVertexTupleMatrixPath.toString()) != std::string::npos); + CHECK(error.message.find(fmt::format("has {} tuples", tupleCount)) != std::string::npos); + CHECK(error.message.find("has 8 vertices") != std::string::npos); + CHECK(error.message.find("one tuple per geometry vertex") != std::string::npos); + CHECK(error.message.find(filter.humanName() + ":") == std::string::npos); + } + CHECK(dataStructure.getSize() == initialSize); + CHECK_FALSE(dataStructure.containsData(k_SelectedVertexTupleMatrixPath.createChildPath("PartitionIds"))); + CHECK_FALSE(dataStructure.containsData(k_SelectedVertexTupleMatrixPath.getParent().createChildPath("PartitionData"))); + CHECK_FALSE(dataStructure.containsData(DataPath({"CreatedTuplePartitionGrid"}))); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::PartitionGeometryFilter: Rejects unsupported active vertex mask kinds", "[Plugins][PartitionGeometryFilter]") +{ + UnitTest::LoadPlugins(); + const auto mode = GENERATE(ChoicesParameter::ValueType{0}, ChoicesParameter::ValueType{1}, ChoicesParameter::ValueType{2}, ChoicesParameter::ValueType{3}); + const bool useStringArray = GENERATE(false, true); + CAPTURE(mode, useStringArray); + DataStructure dataStructure; + createVertexTupleValidationInput(dataStructure, 8); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_SelectedVertexTupleMatrixPath)); + const auto& parent = dataStructure.getDataRefAs(k_SelectedVertexTupleMatrixPath); + const DataPath maskPath = k_SelectedVertexTupleMatrixPath.createChildPath("RejectedMask"); + if(useStringArray) + { + REQUIRE(StringArray::CreateWithValues(dataStructure, maskPath.getTargetName(), parent.getShape(), std::vector(8, "true"), parent.getId()) != nullptr); + } + else + { + auto* listsPtr = NeighborList::Create(dataStructure, maskPath.getTargetName(), parent.getShape(), parent.getId()); + REQUIRE(listsPtr != nullptr); + for(usize index = 0; index < parent.getNumberOfTuples(); index++) + { + listsPtr->setList(static_cast(index), std::vector{1}); + } + REQUIRE(listsPtr->getTupleShape() == parent.getShape()); + } + auto args = createVertexTupleValidationArguments(mode); + args.insertOrAssign(PartitionGeometryFilter::k_UseVertexMask_Key, true); + args.insertOrAssign(PartitionGeometryFilter::k_VertexMaskPath_Key, maskPath); + const auto initialPaths = dataStructure.getAllDataPaths(); + const auto initialNextId = dataStructure.getNextId(); + IFilter::PreflightResult preflightResult; + REQUIRE_NOTHROW(preflightResult = PartitionGeometryFilter().preflight(dataStructure, args)); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + const auto& error = preflightResult.outputActions.errors().front(); + CHECK(error.code == -3044); + CHECK(error.message.find(maskPath.toString()) != std::string::npos); + CHECK(error.message.find(useStringArray ? "StringArray" : "NeighborList") != std::string::npos); + CHECK(error.message.find("scalar Boolean DataArray") != std::string::npos); + CHECK(error.message.find("Select a scalar Boolean DataArray") != std::string::npos); + CHECK(dataStructure.getAllDataPaths() == initialPaths); + CHECK(dataStructure.getNextId() == initialNextId); + CHECK_FALSE(dataStructure.containsData(k_SelectedVertexTupleMatrixPath.createChildPath("PartitionIds"))); + CHECK_FALSE(dataStructure.containsData(k_SelectedVertexTupleMatrixPath.getParent().createChildPath("PartitionData"))); + CHECK_FALSE(dataStructure.containsData(DataPath({"CreatedTuplePartitionGrid"}))); + if(useStringArray) + { + REQUIRE_NOTHROW(dataStructure.getDataRefAs(maskPath)); + CHECK(dataStructure.getDataRefAs(maskPath)[0] == "true"); + } + else + { + REQUIRE_NOTHROW(dataStructure.getDataRefAs>(maskPath)); + CHECK(dataStructure.getDataRefAs>(maskPath).getList(0) == std::vector{1}); + } + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::PartitionGeometryFilter: Inactive mask selections and active Boolean arrays pass preflight", "[Plugins][PartitionGeometryFilter]") +{ + UnitTest::LoadPlugins(); + const usize maskKind = GENERATE(usize{0}, usize{1}, usize{2}, usize{3}, usize{4}); + CAPTURE(maskKind); + DataStructure dataStructure; + createVertexTupleValidationInput(dataStructure, 8); + const DataPath maskPath({"OptionalMask"}); + if(maskKind == 0) + { + REQUIRE(StringArray::CreateWithValues(dataStructure, maskPath.getTargetName(), {1}, {"unused"}) != nullptr); + } + else if(maskKind == 1) + { + REQUIRE(NeighborList::Create(dataStructure, maskPath.getTargetName(), {1}) != nullptr); + } + else if(maskKind >= 3) + { + auto maskStorePtr = DataStoreUtilities::CreateDataStore(dataStructure, maskPath, {8}, {1}); + auto* maskPtr = BoolArray::Create(dataStructure, maskPath.getTargetName(), maskStorePtr); + REQUIRE(maskPtr != nullptr); + maskPtr->fill(true); + } + auto args = createVertexTupleValidationArguments(0); + args.insertOrAssign(PartitionGeometryFilter::k_UseVertexMask_Key, maskKind == 4); + args.insertOrAssign(PartitionGeometryFilter::k_VertexMaskPath_Key, maskPath); + const auto initialPaths = dataStructure.getAllDataPaths(); + IFilter::PreflightResult preflightResult; + REQUIRE_NOTHROW(preflightResult = PartitionGeometryFilter().preflight(dataStructure, args)); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + CHECK(dataStructure.getAllDataPaths() == initialPaths); + CHECK_FALSE(dataStructure.containsData(k_SelectedVertexTupleMatrixPath.createChildPath("PartitionIds"))); + CHECK_FALSE(dataStructure.containsData(DataPath({"CreatedTuplePartitionGrid"}))); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::PartitionGeometryFilter: Geometry errors precede vertex mask kind errors", "[Plugins][PartitionGeometryFilter]") +{ + UnitTest::LoadPlugins(); + const usize geometryKind = GENERATE(usize{0}, usize{1}, usize{2}); + const bool tupleMismatch = GENERATE(false, true); + const bool useStringArray = GENERATE(false, true); + CAPTURE(geometryKind, tupleMismatch, useStringArray); + DataStructure dataStructure; + const usize tupleCount = tupleMismatch ? 7 : 8; + Arguments args; + if(geometryKind == 0) + { + createVertexTupleValidationInput(dataStructure, tupleCount); + args = createVertexTupleValidationArguments(0); + } + else + { + createCellTupleValidationInput(dataStructure, geometryKind == 2, tupleCount); + args = createCellTupleValidationArguments(0); + } + const DataPath maskPath({"RejectedMask"}); + if(useStringArray) + { + REQUIRE(StringArray::CreateWithValues(dataStructure, maskPath.getTargetName(), {8}, std::vector(8, "true")) != nullptr); + } + else + { + REQUIRE(NeighborList::Create(dataStructure, maskPath.getTargetName(), {8}) != nullptr); + } + args.insertOrAssign(PartitionGeometryFilter::k_UseVertexMask_Key, true); + args.insertOrAssign(PartitionGeometryFilter::k_VertexMaskPath_Key, maskPath); + const auto initialPaths = dataStructure.getAllDataPaths(); + IFilter::PreflightResult preflightResult; + REQUIRE_NOTHROW(preflightResult = PartitionGeometryFilter().preflight(dataStructure, args)); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + constexpr std::array tupleErrorCodes = {-3043, -3009, -3010}; + int32 expectedCode = -3019; + if(tupleMismatch) + { + expectedCode = tupleErrorCodes.at(geometryKind); + } + else if(geometryKind == 0) + { + expectedCode = -3044; + } + CHECK(preflightResult.outputActions.errors().front().code == expectedCode); + CHECK(dataStructure.getAllDataPaths() == initialPaths); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::PartitionGeometryFilter: Vertex tuple and partition Z count errors are distinct", "[Plugins][PartitionGeometryFilter]") +{ + UnitTest::LoadPlugins(); + DataStructure vertexDataStructure; + createVertexTupleValidationInput(vertexDataStructure, 7); + const auto vertexPreflight = PartitionGeometryFilter().preflight(vertexDataStructure, createVertexTupleValidationArguments(0)); + SIMPLNX_RESULT_REQUIRE_INVALID(vertexPreflight.outputActions); + REQUIRE(vertexPreflight.outputActions.errors().size() == 1); + CHECK(vertexPreflight.outputActions.errors()[0].code == -3043); + + DataStructure imageDataStructure; + createCellTupleValidationInput(imageDataStructure, false, 8); + Arguments imageArgs = createCellTupleValidationArguments(0); + imageArgs.insertOrAssign(PartitionGeometryFilter::k_NumberOfCellsPerAxis_Key, std::vector{2, 2, 0}); + const auto imagePreflight = PartitionGeometryFilter().preflight(imageDataStructure, imageArgs); + SIMPLNX_RESULT_REQUIRE_INVALID(imagePreflight.outputActions); + REQUIRE(imagePreflight.outputActions.errors().size() == 1); + CHECK(imagePreflight.outputActions.errors()[0].code == -3014); + CHECK(imagePreflight.outputActions.errors()[0].message.find("Z dimension (0)") != std::string::npos); + CHECK(vertexPreflight.outputActions.errors()[0].code != imagePreflight.outputActions.errors()[0].code); + UnitTest::CheckArraysInheritTupleDims(vertexDataStructure); + UnitTest::CheckArraysInheritTupleDims(imageDataStructure); +} + +TEST_CASE("SimplnxCore::PartitionGeometryFilter: Mode errors precede image tuple validation", "[Plugins][PartitionGeometryFilter]") +{ + UnitTest::LoadPlugins(); + const auto mode = GENERATE(ChoicesParameter::ValueType{1}, ChoicesParameter::ValueType{2}); + const usize axis = GENERATE(usize{0}, usize{1}, usize{2}); + CAPTURE(mode, axis); + DataStructure dataStructure; + createCellTupleValidationInput(dataStructure, false, 7); + Arguments args = createCellTupleValidationArguments(mode); + std::vector invalidValues = {1.0F, 1.0F, 1.0F}; + if(mode == static_cast(PartitionGeometryFilter::PartitioningMode::Advanced)) + { + invalidValues[axis] = -1.0F; + args.insertOrAssign(PartitionGeometryFilter::k_CellLength_Key, invalidValues); + } + else + { + invalidValues[axis] = 3.0F; + args.insertOrAssign(PartitionGeometryFilter::k_MinGridCoord_Key, invalidValues); + } + const auto preflightResult = PartitionGeometryFilter().preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + const int32 firstAxisError = mode == static_cast(PartitionGeometryFilter::PartitioningMode::Advanced) ? -3003 : -3006; + CHECK(preflightResult.outputActions.errors()[0].code == firstAxisError - static_cast(axis)); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + TEST_CASE("SimplnxCore::PartitionGeometryFilter: Basic", "[Plugins][PartitionGeometryFilter]") { const auto scenario = GENERATE(from_range(UnitTest::SelectAlgorithmTestScenariosForInMemoryStores())); @@ -774,3 +1227,47 @@ TEST_CASE("SimplnxCore::PartitionGeometryFilter: Invalid filter execution") UnitTest::CheckArraysInheritTupleDims(dataStructure); } + +TEST_CASE("SimplnxCore::PartitionGeometryFilter: Geometry cell tuple count", "[Plugins][PartitionGeometryFilter][TupleCount]") +{ + using FilterType = PartitionGeometryFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{12}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + auto* partitionGridPtr = ImageGeom::Create(dataStructure, "Existing Grid"); + REQUIRE(partitionGridPtr != nullptr); + partitionGridPtr->setDimensions({1, 1, 1}); + partitionGridPtr->setSpacing({2.0F, 3.0F, 2.0F}); + + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + // The selected matrix is intentionally empty and outside the geometry's Cell Data. + REQUIRE(AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}) != nullptr); + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_InputGeometryToPartition_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_InputGeometryCellAttributeMatrixPath_Key, std::make_any(DataPath({"Other Data"}))); + args.insertOrAssign(FilterType::k_PartitioningMode_Key, std::make_any(3)); + args.insertOrAssign(FilterType::k_ExistingPartitionGridPath_Key, std::make_any(DataPath({"Existing Grid"}))); + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -3045); + REQUIRE(errorRef.message.find("The selected data 'Other Data'") != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", tuples)) != std::string::npos); + REQUIRE(errorRef.message.find("selected grid geometry 'Image' has 12 cells") != std::string::npos); + REQUIRE(errorRef.message.find("Select data with one tuple per geometry cell.") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/ReplaceElementAttributesWithNeighborValuesTest.cpp b/src/Plugins/SimplnxCore/test/ReplaceElementAttributesWithNeighborValuesTest.cpp index 815e41eaac..be85218104 100644 --- a/src/Plugins/SimplnxCore/test/ReplaceElementAttributesWithNeighborValuesTest.cpp +++ b/src/Plugins/SimplnxCore/test/ReplaceElementAttributesWithNeighborValuesTest.cpp @@ -449,3 +449,49 @@ TEST_CASE("SimplnxCore::ReplaceElementAttributesWithNeighborValuesFilter: SIMPL } } } + +TEST_CASE("SimplnxCore::ReplaceElementAttributesWithNeighborValuesFilter: Geometry cell tuple count", "[SimplnxCore][ReplaceElementAttributesWithNeighborValuesFilter][TupleCount]") +{ + UnitTest::LoadPlugins(); + using FilterType = ReplaceElementAttributesWithNeighborValuesFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{12}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + + // A matching input in another Attribute Matrix must remain valid. + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 1), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_ComparisonDataPath, std::make_any(inputPath)); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -15001); + const DataPath expectedPath = inputPath; + const usize expectedTuples = tuples; + REQUIRE(errorRef.message.find(expectedPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", expectedTuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTest.cpp b/src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTest.cpp index d0351c7388..e7c3ac9653 100644 --- a/src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTest.cpp +++ b/src/Plugins/SimplnxCore/test/RequireMinNumNeighborsTest.cpp @@ -3,13 +3,19 @@ #include "SimplnxCore/Filters/RequireMinNumNeighborsFilter.hpp" #include "SimplnxCore/SimplnxCore_test_dirs.hpp" -#include - +#include "simplnx/Common/StringLiteral.hpp" +#include "simplnx/Common/Types.hpp" #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataArray.hpp" #include "simplnx/DataStructure/DataGroup.hpp" +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/DataStructure.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/DataStructure/NeighborList.hpp" +#include "simplnx/DataStructure/StringArray.hpp" +#include "simplnx/Filter/Arguments.hpp" +#include "simplnx/Filter/IFilter.hpp" #include "simplnx/Parameters/MultiArraySelectionParameter.hpp" #include "simplnx/Pipeline/Pipeline.hpp" #include "simplnx/Pipeline/PipelineFilter.hpp" @@ -17,11 +23,17 @@ #include "simplnx/Utilities/DataStoreUtilities.hpp" #include "simplnx/Utilities/Parsing/HDF5/IO/FileIO.hpp" +#include +#include + +#include #include #include #include #include +#include #include +#include #include namespace fs = std::filesystem; @@ -278,6 +290,22 @@ void PopulateDataStructure(DataStructure& dataStructure) } } } // namespace DiscriminatingFixture + +/** + * @brief Selects the analytical fixture inputs for array-kind validation. + * @return Arguments with all three numeric inputs active. + */ +Arguments createArrayKindValidationArguments() +{ + Arguments args = RequireMinNumNeighborsFilter().getDefaultArguments(); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_SelectedImageGeometryPath_Key, DataPath({"ImageGeometry"})); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_FeatureIdsPath_Key, DataPath({"ImageGeometry", "CellData", "FeatureIds"})); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_NumNeighborsPath_Key, DataPath({"ImageGeometry", "FeatureData", "NumNeighbors"})); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_FeaturePhasesPath_Key, DataPath({"ImageGeometry", "FeatureData", "Phases"})); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_ApplyToSinglePhase_Key, true); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_PhaseNumber_Key, uint64{1}); + return args; +} } // namespace // The tests use two analytical oracles. @@ -639,32 +667,58 @@ TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - tuple co REQUIRE(preflightResult.outputActions.errors()[0].code == -252); } -TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - FeatureIds tuple count mismatch (-55571)", "[SimplnxCore][RequireMinNumNeighborsFilter][preflight]") +TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter: Selected cell tuple counts", "[SimplnxCore][RequireMinNumNeighborsFilter][preflight]") { UnitTest::LoadPlugins(); + const usize tupleCount = GENERATE(usize{3}, usize{4}, usize{5}); + CAPTURE(tupleCount); DataStructure dataStructure; - auto* imageGeom = ImageGeom::Create(dataStructure, "ImageGeometry"); - imageGeom->setDimensions({5, 1, 1}); - auto* cellAM = AttributeMatrix::Create(dataStructure, "CellData", {1, 1, 4}, imageGeom->getId()); - imageGeom->setCellData(*cellAM); - auto* featureAM = AttributeMatrix::Create(dataStructure, "FeatureData", {4}, imageGeom->getId()); - UnitTest::CreateTestDataArray(dataStructure, "FeatureIds", {1, 1, 4}, {1}, cellAM->getId()); - UnitTest::CreateTestDataArray(dataStructure, "NumNeighbors", {4}, {1}, featureAM->getId()); + auto* selectedGroupPtr = DataGroup::Create(dataStructure, "Selected"); + REQUIRE(selectedGroupPtr != nullptr); + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "ImageGeometry", selectedGroupPtr->getId()); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 2, 1}); + // Equal counts are valid in the geometry's Cell Attribute Matrix even when + // its tuple shape differs from the geometry's {1, 2, 2} shape. + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "CellData", {tupleCount}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + auto* featureAMPtr = AttributeMatrix::Create(dataStructure, "FeatureData", {4}, imageGeomPtr->getId()); + REQUIRE(featureAMPtr != nullptr); + UnitTest::CreateTestDataArray(dataStructure, "FeatureIds", {tupleCount}, {1}, cellAMPtr->getId()); + UnitTest::CreateTestDataArray(dataStructure, "NumNeighbors", {4}, {1}, featureAMPtr->getId()); + const DataPath geometryPath({"Selected", "ImageGeometry"}); + const DataPath featureIdsPath({"Selected", "ImageGeometry", "CellData", "FeatureIds"}); RequireMinNumNeighborsFilter filter; Arguments args; args.insertOrAssign(RequireMinNumNeighborsFilter::k_MinNumNeighbors_Key, std::make_any(0)); args.insertOrAssign(RequireMinNumNeighborsFilter::k_ApplyToSinglePhase_Key, std::make_any(false)); args.insertOrAssign(RequireMinNumNeighborsFilter::k_PhaseNumber_Key, std::make_any(0)); - args.insertOrAssign(RequireMinNumNeighborsFilter::k_SelectedImageGeometryPath_Key, std::make_any(DataPath({"ImageGeometry"}))); - args.insertOrAssign(RequireMinNumNeighborsFilter::k_FeatureIdsPath_Key, std::make_any(DataPath({"ImageGeometry", "CellData", "FeatureIds"}))); - args.insertOrAssign(RequireMinNumNeighborsFilter::k_NumNeighborsPath_Key, std::make_any(DataPath({"ImageGeometry", "FeatureData", "NumNeighbors"}))); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_SelectedImageGeometryPath_Key, std::make_any(geometryPath)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_FeatureIdsPath_Key, std::make_any(featureIdsPath)); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_NumNeighborsPath_Key, std::make_any(geometryPath.createChildPath("FeatureData").createChildPath("NumNeighbors"))); args.insertOrAssign(RequireMinNumNeighborsFilter::k_IgnoredVoxelArrays_Key, std::make_any>(std::vector{})); const auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); - REQUIRE(preflightResult.outputActions.errors()[0].code == -55571); + if(tupleCount == 4) + { + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + const auto& error = preflightResult.outputActions.errors()[0]; + CHECK(error.code == -59571); + CHECK(error.message.find(featureIdsPath.toString()) != std::string::npos); + CHECK(error.message.find(geometryPath.toString()) != std::string::npos); + CHECK(error.message.find(fmt::format("has {} tuples", tupleCount)) != std::string::npos); + CHECK(error.message.find("has 4 cells") != std::string::npos); + CHECK(error.message.find("one tuple per geometry cell") != std::string::npos); + } + UnitTest::CheckArraysInheritTupleDims(dataStructure); } TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - FeatureIds outside Cell Attribute Matrix (-55575)", "[SimplnxCore][RequireMinNumNeighborsFilter][preflight]") @@ -696,6 +750,136 @@ TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - FeatureI REQUIRE(preflightResult.outputActions.errors()[0].code == -55575); } +TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter: Rejects unsupported array kinds", "[SimplnxCore][RequireMinNumNeighborsFilter][preflight]") +{ + UnitTest::LoadPlugins(); + const usize inputIndex = GENERATE(usize{0}, usize{1}, usize{2}); + const bool useStringArray = GENERATE(false, true); + CAPTURE(inputIndex, useStringArray); + const std::array inputKeys = {RequireMinNumNeighborsFilter::k_FeatureIdsPath_Key, RequireMinNumNeighborsFilter::k_NumNeighborsPath_Key, + RequireMinNumNeighborsFilter::k_FeaturePhasesPath_Key}; + constexpr std::array expectedCodes = {-59574, -59575, -59576}; + DataStructure dataStructure; + DiscriminatingFixture::PopulateDataStructure(dataStructure); + Arguments args = createArrayKindValidationArguments(); + const auto parentPath = args.value(inputKeys.at(inputIndex)).getParent(); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(parentPath)); + const auto& parent = dataStructure.getDataRefAs(parentPath); + const DataPath rejectedPath = parentPath.createChildPath("RejectedInput"); + if(useStringArray) + { + REQUIRE(StringArray::CreateWithValues(dataStructure, rejectedPath.getTargetName(), parent.getShape(), std::vector(parent.getNumberOfTuples(), "17"), parent.getId()) != nullptr); + } + else + { + auto* listsPtr = NeighborList::Create(dataStructure, rejectedPath.getTargetName(), parent.getShape(), parent.getId()); + REQUIRE(listsPtr != nullptr); + for(usize index = 0; index < parent.getNumberOfTuples(); index++) + { + listsPtr->setList(static_cast(index), std::vector{17}); + } + REQUIRE(listsPtr->getTupleShape() == parent.getShape()); + } + args.insertOrAssign(inputKeys.at(inputIndex), rejectedPath); + const auto initialPaths = dataStructure.getAllDataPaths(); + const auto initialNextId = dataStructure.getNextId(); + const RequireMinNumNeighborsFilter filter; + IFilter::PreflightResult preflightResult; + REQUIRE_NOTHROW(preflightResult = filter.preflight(dataStructure, args)); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + const auto& error = preflightResult.outputActions.errors().front(); + CHECK(error.code == expectedCodes.at(inputIndex)); + CHECK(error.message.find(rejectedPath.toString()) != std::string::npos); + CHECK(error.message.find(useStringArray ? "StringArray" : "NeighborList") != std::string::npos); + CHECK(error.message.find("scalar int32 DataArray") != std::string::npos); + CHECK(error.message.find("Select a scalar int32 DataArray") != std::string::npos); + CHECK(dataStructure.getAllDataPaths() == initialPaths); + CHECK(dataStructure.getNextId() == initialNextId); + if(useStringArray) + { + REQUIRE_NOTHROW(dataStructure.getDataRefAs(rejectedPath)); + CHECK(dataStructure.getDataRefAs(rejectedPath)[0] == "17"); + } + else + { + REQUIRE_NOTHROW(dataStructure.getDataRefAs>(rejectedPath)); + CHECK(dataStructure.getDataRefAs>(rejectedPath).getList(0) == std::vector{17}); + } + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({"ImageGeometry", "CellData", "CopiedScalar"}))); + CHECK(dataStructure.getDataRefAs(DataPath({"ImageGeometry", "CellData", "CopiedScalar"}))[0] == 10000); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter: Inactive phases skip array-kind validation", "[SimplnxCore][RequireMinNumNeighborsFilter][preflight]") +{ + UnitTest::LoadPlugins(); + const usize inactiveKind = GENERATE(usize{0}, usize{1}, usize{2}); + CAPTURE(inactiveKind); + DataStructure dataStructure; + DiscriminatingFixture::PopulateDataStructure(dataStructure); + const DataPath inactivePath({"InactivePhases"}); + if(inactiveKind == 0) + { + REQUIRE(StringArray::CreateWithValues(dataStructure, inactivePath.getTargetName(), {1}, {"unused"}) != nullptr); + } + else if(inactiveKind == 1) + { + REQUIRE(NeighborList::Create(dataStructure, inactivePath.getTargetName(), {1}) != nullptr); + } + Arguments args = createArrayKindValidationArguments(); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_ApplyToSinglePhase_Key, false); + args.insertOrAssign(RequireMinNumNeighborsFilter::k_FeaturePhasesPath_Key, inactivePath); + const auto initialPaths = dataStructure.getAllDataPaths(); + IFilter::PreflightResult preflightResult; + REQUIRE_NOTHROW(preflightResult = RequireMinNumNeighborsFilter().preflight(dataStructure, args)); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + CHECK(dataStructure.getAllDataPaths() == initialPaths); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter: Tuple errors precede feature array-kind errors", "[SimplnxCore][RequireMinNumNeighborsFilter][preflight]") +{ + UnitTest::LoadPlugins(); + const bool replaceNeighbors = GENERATE(false, true); + const bool useStringArray = GENERATE(false, true); + const bool cellMismatch = GENERATE(false, true); + CAPTURE(replaceNeighbors, useStringArray, cellMismatch); + DataStructure dataStructure; + DiscriminatingFixture::PopulateDataStructure(dataStructure); + const auto& featureAM = dataStructure.getDataRefAs(DataPath({"ImageGeometry", "FeatureData"})); + const DataPath rejectedPath({"ImageGeometry", "FeatureData", "RejectedFeatureInput"}); + if(useStringArray) + { + REQUIRE(StringArray::CreateWithValues(dataStructure, rejectedPath.getTargetName(), featureAM.getShape(), std::vector(featureAM.getNumberOfTuples(), "17"), featureAM.getId()) != + nullptr); + } + else + { + REQUIRE(NeighborList::Create(dataStructure, rejectedPath.getTargetName(), featureAM.getShape(), featureAM.getId()) != nullptr); + } + // Keep the other selected feature array in a separate AM so its tuple count can differ. + const auto& imageGeom = dataStructure.getDataRefAs(DataPath({"ImageGeometry"})); + auto* mismatchAM = AttributeMatrix::Create(dataStructure, "MismatchData", {5}, imageGeom.getId()); + REQUIRE(mismatchAM != nullptr); + const DataPath mismatchPath({"ImageGeometry", "MismatchData", "OtherFeatureInput"}); + UnitTest::CreateTestDataArray(dataStructure, mismatchPath.getTargetName(), {5}, {1}, mismatchAM->getId()); + if(cellMismatch) + { + REQUIRE_NOTHROW(dataStructure.getDataRefAs(DataPath({"ImageGeometry"}))); + dataStructure.getDataRefAs(DataPath({"ImageGeometry"})).setDimensions({7, 6, 6}); + } + Arguments args = createArrayKindValidationArguments(); + args.insertOrAssign(replaceNeighbors ? RequireMinNumNeighborsFilter::k_NumNeighborsPath_Key : RequireMinNumNeighborsFilter::k_FeaturePhasesPath_Key, rejectedPath); + args.insertOrAssign(replaceNeighbors ? RequireMinNumNeighborsFilter::k_FeaturePhasesPath_Key : RequireMinNumNeighborsFilter::k_NumNeighborsPath_Key, mismatchPath); + IFilter::PreflightResult preflightResult; + REQUIRE_NOTHROW(preflightResult = RequireMinNumNeighborsFilter().preflight(dataStructure, args)); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + CHECK(preflightResult.outputActions.errors().front().code == (cellMismatch ? -59571 : -252)); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + TEST_CASE("SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - feature ID out of range (-55567)", "[SimplnxCore][RequireMinNumNeighborsFilter][execute]") { UnitTest::LoadPlugins(); diff --git a/src/Plugins/SimplnxCore/test/ScalarSegmentFeaturesTest.cpp b/src/Plugins/SimplnxCore/test/ScalarSegmentFeaturesTest.cpp index 457d015612..4b1e46488d 100644 --- a/src/Plugins/SimplnxCore/test/ScalarSegmentFeaturesTest.cpp +++ b/src/Plugins/SimplnxCore/test/ScalarSegmentFeaturesTest.cpp @@ -543,3 +543,59 @@ TEST_CASE("SimplnxCore::ScalarSegmentFeatures: Execute Error - All Cells Masked SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); REQUIRE(executeResult.result.errors()[0].code == -87000); } + +TEST_CASE("SimplnxCore::ScalarSegmentFeatures: Geometry cell tuple count", "[SimplnxCore][ScalarSegmentFeatures][TupleCount]") +{ + UnitTest::LoadPlugins(); + using FilterType = ScalarSegmentFeaturesFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{12}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + // A disabled mask must not be checked; an enabled mask must match the geometry. + const int maskMode = GENERATE(0, 1, 2); + CAPTURE(maskMode); + const usize maskTuples = maskMode == 1 ? tuples : 5; + + // A matching input in another Attribute Matrix must remain valid. + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 1), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + auto* maskAMPtr = AttributeMatrix::Create(dataStructure, "Mask Data", ShapeType{maskTuples}); + REQUIRE(maskAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Mask", std::make_shared>(ShapeType{maskTuples}, ShapeType{1}, 1), maskAMPtr->getId()) != nullptr); + const DataPath maskPath({"Mask Data", "Mask"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_GridGeomPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_InputArrayPathKey, std::make_any(inputPath)); + args.insertOrAssign(FilterType::k_UseMask_Key, std::make_any(maskMode != 0)); + args.insertOrAssign(FilterType::k_MaskArrayPath_Key, std::make_any(maskPath)); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12 && maskMode != 2) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -654); + const DataPath expectedPath = tuples == 12 ? maskPath : inputPath; + const usize expectedTuples = tuples == 12 ? maskTuples : tuples; + REQUIRE(errorRef.message.find(expectedPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", expectedTuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/WriteAbaqusHexahedronTest.cpp b/src/Plugins/SimplnxCore/test/WriteAbaqusHexahedronTest.cpp index 8b3ddf1e20..1edcf6aa16 100644 --- a/src/Plugins/SimplnxCore/test/WriteAbaqusHexahedronTest.cpp +++ b/src/Plugins/SimplnxCore/test/WriteAbaqusHexahedronTest.cpp @@ -648,3 +648,51 @@ TEST_CASE("SimplnxCore::WriteAbaqusHexahedronFilter: Output Target Is A Director } UnitTest::CheckArraysInheritTupleDims(dataStructure); } + +TEST_CASE("SimplnxCore::WriteAbaqusHexahedronFilter: Geometry cell tuple count", "[SimplnxCore][WriteAbaqusHexahedronFilter][TupleCount]") +{ + UnitTest::LoadPlugins(); + using FilterType = WriteAbaqusHexahedronFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{12}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + + // A matching input in another Attribute Matrix must remain valid. + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 1), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_ImageGeometryPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_FeatureIdsArrayPath_Key, std::make_any(inputPath)); + args.insertOrAssign(FilterType::k_OutputPath_Key, std::make_any(fs::path(unit_test::k_BinaryTestOutputDir.view()) / "TupleCountAbaqus")); + args.insertOrAssign(FilterType::k_FilePrefix_Key, std::make_any("TupleCount")); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -1124); + const DataPath expectedPath = inputPath; + const usize expectedTuples = tuples; + REQUIRE(errorRef.message.find(expectedPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", expectedTuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/WriteAvizoRectilinearCoordinateTest.cpp b/src/Plugins/SimplnxCore/test/WriteAvizoRectilinearCoordinateTest.cpp index 6726de9a19..24ca5c2d69 100644 --- a/src/Plugins/SimplnxCore/test/WriteAvizoRectilinearCoordinateTest.cpp +++ b/src/Plugins/SimplnxCore/test/WriteAvizoRectilinearCoordinateTest.cpp @@ -2,6 +2,9 @@ #include #include "simplnx/Core/Application.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Parameters/ArraySelectionParameter.hpp" #include "simplnx/Parameters/BoolParameter.hpp" #include "simplnx/Parameters/FileSystemPathParameter.hpp" @@ -115,3 +118,50 @@ TEST_CASE("SimplnxCore::WriteAvizoRectilinearCoordinateFilter: SIMPL Backwards C } } } + +TEST_CASE("SimplnxCore::WriteAvizoRectilinearCoordinateFilter: Geometry cell tuple count", "[SimplnxCore][WriteAvizoRectilinearCoordinateFilter][TupleCount]") +{ + UnitTest::LoadPlugins(); + using FilterType = WriteAvizoRectilinearCoordinateFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{12}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + + // A matching input in another Attribute Matrix must remain valid. + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 1), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_GeometryPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_FeatureIdsArrayPath_Key, std::make_any(inputPath)); + args.insertOrAssign(FilterType::k_OutputFile_Key, std::make_any(fs::path(unit_test::k_BinaryTestOutputDir.view()) / "TupleCountWriteAvizoRectilinearCoordinate.txt")); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -5834); + const DataPath expectedPath = inputPath; + const usize expectedTuples = tuples; + REQUIRE(errorRef.message.find(expectedPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", expectedTuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/WriteAvizoUniformCoordinateTest.cpp b/src/Plugins/SimplnxCore/test/WriteAvizoUniformCoordinateTest.cpp index 0cf09a4be5..d2bed71c6f 100644 --- a/src/Plugins/SimplnxCore/test/WriteAvizoUniformCoordinateTest.cpp +++ b/src/Plugins/SimplnxCore/test/WriteAvizoUniformCoordinateTest.cpp @@ -2,6 +2,9 @@ #include #include "simplnx/Core/Application.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Parameters/ArraySelectionParameter.hpp" #include "simplnx/Parameters/BoolParameter.hpp" #include "simplnx/Parameters/FileSystemPathParameter.hpp" @@ -116,3 +119,50 @@ TEST_CASE("SimplnxCore::WriteAvizoUniformCoordinateFilter: SIMPL Backwards Compa } } } + +TEST_CASE("SimplnxCore::WriteAvizoUniformCoordinateFilter: Geometry cell tuple count", "[SimplnxCore][WriteAvizoUniformCoordinateFilter][TupleCount]") +{ + UnitTest::LoadPlugins(); + using FilterType = WriteAvizoUniformCoordinateFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{12}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + + // A matching input in another Attribute Matrix must remain valid. + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 1), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_GeometryPath_Key, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_FeatureIdsArrayPath_Key, std::make_any(inputPath)); + args.insertOrAssign(FilterType::k_OutputFile_Key, std::make_any(fs::path(unit_test::k_BinaryTestOutputDir.view()) / "TupleCountWriteAvizoUniformCoordinate.am")); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -5835); + const DataPath expectedPath = inputPath; + const usize expectedTuples = tuples; + REQUIRE(errorRef.message.find(expectedPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", expectedTuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/WriteLosAlamosFFTTest.cpp b/src/Plugins/SimplnxCore/test/WriteLosAlamosFFTTest.cpp index e8f0c43a94..691ad45296 100644 --- a/src/Plugins/SimplnxCore/test/WriteLosAlamosFFTTest.cpp +++ b/src/Plugins/SimplnxCore/test/WriteLosAlamosFFTTest.cpp @@ -220,3 +220,63 @@ TEST_CASE("SimplnxCore::WriteLosAlamosFFTFilter: SIMPL Backwards Compatibility", } } } + +TEST_CASE("SimplnxCore::WriteLosAlamosFFTFilter: Geometry cell tuple count", "[SimplnxCore][WriteLosAlamosFFTFilter][TupleCount]") +{ + UnitTest::LoadPlugins(); + using FilterType = WriteLosAlamosFFTFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{12}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + // Equally sized siblings expose the missing geometry check; separate short inputs cover every parameter. + const auto counts = GENERATE((std::array{0, 0, 0}), (std::array{11, 11, 11}), (std::array{12, 12, 12}), (std::array{13, 13, 13}), + (std::array{11, 12, 12}), (std::array{12, 11, 12}), (std::array{12, 12, 11})); + const usize tuples = counts[0]; + CAPTURE(counts); + + // A matching input in another Attribute Matrix must remain valid. + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 1), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + auto* eulerAMPtr = AttributeMatrix::Create(dataStructure, "Euler Data", ShapeType{counts[1]}); + auto* phaseAMPtr = AttributeMatrix::Create(dataStructure, "Phase Data", ShapeType{counts[2]}); + REQUIRE(eulerAMPtr != nullptr); + REQUIRE(phaseAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "EulerAngles", std::make_shared>(ShapeType{counts[1]}, ShapeType{3}, 0), eulerAMPtr->getId()) != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Phases", std::make_shared>(ShapeType{counts[2]}, ShapeType{1}, 1), phaseAMPtr->getId()) != nullptr); + const DataPath eulerPath({"Euler Data", "EulerAngles"}); + const DataPath phasePath({"Phase Data", "Phases"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_ImageGeomPath, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_FeatureIdsArrayPath_Key, std::make_any(inputPath)); + args.insertOrAssign(FilterType::k_CellEulerAnglesArrayPath_Key, std::make_any(eulerPath)); + args.insertOrAssign(FilterType::k_CellPhasesArrayPath_Key, std::make_any(phasePath)); + args.insertOrAssign(FilterType::k_OutputFile_Key, std::make_any(fs::path(unit_test::k_BinaryTestOutputDir.view()) / "TupleCountWriteLosAlamosFFT.txt")); + + const auto result = filter.preflight(dataStructure, args); + if(counts[0] == 12 && counts[1] == 12 && counts[2] == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -73461); + const DataPath expectedPath = counts[0] != 12 ? inputPath : (counts[1] != 12 ? eulerPath : phasePath); + const usize expectedTuples = counts[0] != 12 ? counts[0] : (counts[1] != 12 ? counts[1] : counts[2]); + REQUIRE(errorRef.message.find(expectedPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", expectedTuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/test/WriteOnScaleTableFileTest.cpp b/src/Plugins/SimplnxCore/test/WriteOnScaleTableFileTest.cpp index 909ba867df..f599aaed93 100644 --- a/src/Plugins/SimplnxCore/test/WriteOnScaleTableFileTest.cpp +++ b/src/Plugins/SimplnxCore/test/WriteOnScaleTableFileTest.cpp @@ -3,12 +3,20 @@ #include "SimplnxCore/Filters/WriteOnScaleTableFileFilter.hpp" #include "SimplnxCore/SimplnxCore_test_dirs.hpp" +#include "simplnx/Common/Types.hpp" #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/AttributeMatrix.hpp" #include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataGroup.hpp" +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/DataStructure.hpp" +#include "simplnx/DataStructure/Geometry/IGridGeometry.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/DataStructure/Geometry/RectGridGeom.hpp" +#include "simplnx/DataStructure/NeighborList.hpp" #include "simplnx/DataStructure/StringArray.hpp" +#include "simplnx/Filter/Arguments.hpp" +#include "simplnx/Filter/IFilter.hpp" #include "simplnx/Parameters/ChoicesParameter.hpp" #include "simplnx/Parameters/DynamicTableParameter.hpp" #include "simplnx/Parameters/FileSystemPathParameter.hpp" @@ -20,20 +28,25 @@ #include "simplnx/Utilities/DataStoreUtilities.hpp" #include +#include #include #include +#include +#include #include #include #include #include #include +#include #include #include #include #include #include +#include #include namespace fs = std::filesystem; @@ -46,7 +59,6 @@ const DataPath k_CellDataPath = k_GeometryPath.createChildPath("Cell Data"); const DataPath k_FeatureIdsPath = k_CellDataPath.createChildPath("FeatureIds"); const DataPath k_EnsembleDataPath = k_GeometryPath.createChildPath("Cell Ensemble Data"); const DataPath k_PhaseNamesPath = k_EnsembleDataPath.createChildPath("PhaseNames"); -const DataPath k_WrongFeatureIdsPath({"Wrong FeatureIds"}); template DataStructure CreateImageDataStructure(const SizeVec3& dimensions, const std::vector& featureIds, const std::vector& phaseNames, const FloatVec3& spacing = {0.001F, 0.002F, 0.003F}, @@ -596,19 +608,144 @@ TEST_CASE("SimplnxCore::WriteOnScaleTableFileFilter: RectGrid Needs Reorder Fail UnitTest::CheckArraysInheritTupleDims(dataStructure); } -TEST_CASE("SimplnxCore::WriteOnScaleTableFileFilter: Tuple Count Mismatch", "[SimplnxCore][WriteOnScaleTableFileFilter]") +TEST_CASE("SimplnxCore::WriteOnScaleTableFileFilter: Selected cell tuple counts", "[SimplnxCore][WriteOnScaleTableFileFilter]") { UnitTest::LoadPlugins(); + const bool rectilinear = GENERATE(false, true); + const usize tupleCount = GENERATE(usize{3}, usize{4}, usize{5}); + CAPTURE(rectilinear, tupleCount); + + DataStructure dataStructure; + if(rectilinear) + { + const std::array, 3> bounds = {{{0.0F, 1.0F, 2.0F}, {0.0F, 1.0F, 2.0F}, {0.0F, 1.0F}}}; + dataStructure = CreateRectGridDataStructure({2, 2, 1}, bounds, {1, 2, 3, 1}, {"", "Phase1", "Phase2", "Phase3"}); + } + else + { + dataStructure = CreateImageDataStructure({2, 2, 1}, {1, 2, 3, 1}, {"", "Phase1", "Phase2", "Phase3"}); + } + auto* selectedGroupPtr = DataGroup::Create(dataStructure, "Selected"); + REQUIRE(selectedGroupPtr != nullptr); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_GeometryPath)); + REQUIRE(dataStructure.setAdditionalParent(dataStructure.getDataRefAs(k_GeometryPath).getId(), selectedGroupPtr->getId())); + const DataPath geometryPath({"Selected", "Geometry"}); + auto* storageGroupPtr = DataGroup::Create(dataStructure, "Storage"); + REQUIRE(storageGroupPtr != nullptr); + auto* selectedCellDataPtr = AttributeMatrix::Create(dataStructure, "Cell Data", {tupleCount}, storageGroupPtr->getId()); + REQUIRE(selectedCellDataPtr != nullptr); + const DataPath featureIdsPath({"Storage", "Cell Data", "FeatureIds"}); + auto featureIdsStorePtr = DataStoreUtilities::CreateDataStore(dataStructure, featureIdsPath, {tupleCount}, {1}); + REQUIRE(Int32Array::Create(dataStructure, "FeatureIds", featureIdsStorePtr, selectedCellDataPtr->getId()) != nullptr); - DataStructure dataStructure = CreateImageDataStructure({2, 2, 1}, {1, 2, 3, 1}, {"", "Phase1", "Phase2", "Phase3"}); - auto wrongFeatureIdsStore = DataStoreUtilities::CreateDataStore(dataStructure, k_WrongFeatureIdsPath, {3}, {1}); - REQUIRE(Int32Array::Create(dataStructure, k_WrongFeatureIdsPath.getTargetName(), wrongFeatureIdsStore) != nullptr); const WriteOnScaleTableFileFilter filter; Arguments args = CreateArguments(fs::path(unit_test::k_BinaryTestOutputDir.view()), "Tuple_Mismatch"); - args.insertOrAssign(WriteOnScaleTableFileFilter::k_FeatureIdsArrayPath_Key, std::make_any(k_WrongFeatureIdsPath)); - auto preflightResult = filter.preflight(dataStructure, args); - REQUIRE(preflightResult.outputActions.invalid()); + args.insertOrAssign(WriteOnScaleTableFileFilter::k_InputGeometryPath_Key, std::make_any(geometryPath)); + args.insertOrAssign(WriteOnScaleTableFileFilter::k_FeatureIdsArrayPath_Key, std::make_any(featureIdsPath)); + const auto preflightResult = filter.preflight(dataStructure, args); + if(tupleCount == 4) + { + // The separately stored flat tuple shape is a valid count-only match. + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + const auto& error = preflightResult.outputActions.errors()[0]; + CHECK(error.code == -12024); + CHECK(error.message.find(featureIdsPath.toString()) != std::string::npos); + CHECK(error.message.find(geometryPath.toString()) != std::string::npos); + CHECK(error.message.find(fmt::format("has {} tuples", tupleCount)) != std::string::npos); + CHECK(error.message.find("has 4 cells") != std::string::npos); + CHECK(error.message.find("one tuple per geometry cell") != std::string::npos); + } + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} +TEMPLATE_TEST_CASE("SimplnxCore::WriteOnScaleTableFileFilter: All integer Feature Ids pass preflight", "[SimplnxCore][WriteOnScaleTableFileFilter]", int8, int16, int32, int64, uint8, uint16, uint32, + uint64) +{ + UnitTest::LoadPlugins(); + DataStructure dataStructure = CreateImageDataStructure({2, 2, 1}, {1, 2, 3, 1}, {"", "Phase1", "Phase2", "Phase3"}); + const auto initialPaths = dataStructure.getAllDataPaths(); + const auto args = CreateArguments(fs::path(unit_test::k_BinaryTestOutputDir.view()), "Integer_Preflight"); + IFilter::PreflightResult preflightResult; + REQUIRE_NOTHROW(preflightResult = WriteOnScaleTableFileFilter().preflight(dataStructure, args)); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + CHECK(dataStructure.getAllDataPaths() == initialPaths); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_PhaseNamesPath)); + CHECK(dataStructure.getDataRefAs(k_PhaseNamesPath)[1] == "Phase1"); + UnitTest::CheckArraysInheritTupleDims(dataStructure); +} + +TEST_CASE("SimplnxCore::WriteOnScaleTableFileFilter: Rejects unsupported array kinds after output path errors", "[SimplnxCore][WriteOnScaleTableFileFilter]") +{ + UnitTest::LoadPlugins(); + const bool useStringArray = GENERATE(false, true); + const bool outputPathIsFile = GENERATE(false, true); + const bool rectilinear = GENERATE(false, true); + CAPTURE(useStringArray, outputPathIsFile, rectilinear); + DataStructure dataStructure; + if(rectilinear) + { + const std::array, 3> bounds = {{{0.0F, 1.0F, 2.0F}, {0.0F, 1.0F, 2.0F}, {0.0F, 1.0F}}}; + dataStructure = CreateRectGridDataStructure({2, 2, 1}, bounds, {1, 2, 3, 1}, {"", "Phase1", "Phase2", "Phase3"}); + } + else + { + dataStructure = CreateImageDataStructure({2, 2, 1}, {1, 2, 3, 1}, {"", "Phase1", "Phase2", "Phase3"}); + } + const DataPath rejectedPath({"RejectedFeatureIds"}); + if(useStringArray) + { + REQUIRE(StringArray::CreateWithValues(dataStructure, rejectedPath.getTargetName(), {4}, {"1", "2", "3", "1"}) != nullptr); + } + else + { + auto* listsPtr = NeighborList::Create(dataStructure, rejectedPath.getTargetName(), {4}); + REQUIRE(listsPtr != nullptr); + listsPtr->setLists({{1}, {2}, {3}, {1}}); + } + const auto uniqueSuffix = std::chrono::high_resolution_clock::now().time_since_epoch().count(); + const fs::path outputPath = fs::path(unit_test::k_BinaryTestOutputDir.view()) / fmt::format("WriteOnScaleTableFile_ArrayKind_{}", uniqueSuffix); + REQUIRE_FALSE(fs::exists(outputPath)); + if(outputPathIsFile) + { + fs::create_directories(outputPath.parent_path()); + std::ofstream output(outputPath); + REQUIRE(output.is_open()); + output << "Existing regular file"; + } + auto args = CreateArguments(outputPath, "Rejected_Input"); + args.insertOrAssign(WriteOnScaleTableFileFilter::k_FeatureIdsArrayPath_Key, rejectedPath); + const auto initialPaths = dataStructure.getAllDataPaths(); + const auto initialNextId = dataStructure.getNextId(); + IFilter::PreflightResult preflightResult; + REQUIRE_NOTHROW(preflightResult = WriteOnScaleTableFileFilter().preflight(dataStructure, args)); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + const auto& error = preflightResult.outputActions.errors().front(); + CHECK(error.code == (outputPathIsFile ? -12023 : -12042)); + if(outputPathIsFile) + { + CHECK(ReadFile(outputPath) == "Existing regular file"); + fs::remove(outputPath); + } + else + { + CHECK(error.message.find(rejectedPath.toString()) != std::string::npos); + CHECK(error.message.find(useStringArray ? "StringArray" : "NeighborList") != std::string::npos); + CHECK(error.message.find("scalar integer DataArray") != std::string::npos); + CHECK(error.message.find("Select a scalar integer DataArray") != std::string::npos); + CHECK_FALSE(fs::exists(outputPath)); + } + CHECK(dataStructure.getAllDataPaths() == initialPaths); + CHECK(dataStructure.getNextId() == initialNextId); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_FeatureIdsPath)); + CHECK(dataStructure.getDataRefAs(k_FeatureIdsPath)[0] == 1); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_PhaseNamesPath)); + CHECK(dataStructure.getDataRefAs(k_PhaseNamesPath)[1] == "Phase1"); UnitTest::CheckArraysInheritTupleDims(dataStructure); } diff --git a/src/Plugins/SimplnxCore/test/WriteSPParksSitesTest.cpp b/src/Plugins/SimplnxCore/test/WriteSPParksSitesTest.cpp index 99f9b0c51a..a7e18866ad 100644 --- a/src/Plugins/SimplnxCore/test/WriteSPParksSitesTest.cpp +++ b/src/Plugins/SimplnxCore/test/WriteSPParksSitesTest.cpp @@ -4,6 +4,9 @@ #include "SimplnxCore/Filters/WriteSPParksSitesFilter.hpp" #include "simplnx/Core/Application.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Parameters/ArraySelectionParameter.hpp" #include "simplnx/Parameters/ChoicesParameter.hpp" #include "simplnx/Parameters/FileSystemPathParameter.hpp" @@ -96,3 +99,50 @@ TEST_CASE("SimplnxCore::WriteSPParksSitesFilter: SIMPL Backwards Compatibility", } } } + +TEST_CASE("SimplnxCore::WriteSPParksSitesFilter: Geometry cell tuple count", "[SimplnxCore][WriteSPParksSitesFilter][TupleCount]") +{ + UnitTest::LoadPlugins(); + using FilterType = WriteSPParksSitesFilter; + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + auto* cellAMPtr = AttributeMatrix::Create(dataStructure, "Cell Data", ShapeType{12}, imageGeomPtr->getId()); + REQUIRE(cellAMPtr != nullptr); + imageGeomPtr->setCellData(*cellAMPtr); + + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + CAPTURE(tuples); + + // A matching input in another Attribute Matrix must remain valid. + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(DataArray::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{1}, 1), otherAMPtr->getId()) != nullptr); + const DataPath inputPath({"Other Data", "Values"}); + + FilterType filter; + Arguments args; + args.insertOrAssign(FilterType::k_ImageGeomPath, std::make_any(DataPath({"Image"}))); + args.insertOrAssign(FilterType::k_FeatureIdsArrayPath_Key, std::make_any(inputPath)); + args.insertOrAssign(FilterType::k_OutputFile_Key, std::make_any(fs::path(unit_test::k_BinaryTestOutputDir.view()) / "TupleCountWriteSPParksSites.txt")); + + const auto result = filter.preflight(dataStructure, args); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result.outputActions); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result.outputActions); + REQUIRE(result.outputActions.errors().size() == 1); + const auto& errorRef = result.outputActions.errors()[0]; + REQUIRE(errorRef.code == -77453); + const DataPath expectedPath = inputPath; + const usize expectedTuples = tuples; + REQUIRE(errorRef.message.find(expectedPath.toString()) != std::string::npos); + REQUIRE(errorRef.message.find(fmt::format("has {} tuples", expectedTuples)) != std::string::npos); + REQUIRE(errorRef.message.find("'Image' has 12 cells") != std::string::npos); + } +} diff --git a/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md b/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md index f806c824ab..d5e34b24fd 100644 --- a/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md +++ b/src/Plugins/SimplnxCore/vv/RequireMinNumNeighborsFilter.md @@ -181,7 +181,7 @@ The filter selects features to retain, marks rejected-feature voxels, iterativel | 16 | Retired | No explicit source tuple-index range error exists in the current slice-based copy implementation. Face-neighbor selection constrains source indexes to valid cells. | Not an active path. Boundary copying is covered by `SimplnxCore::RequireMinNumNeighborsFilter: Discriminating 6x6x6 Analytical Fixture`. | | 17 | Finalize | Inactive feature tuples are removed and remaining FeatureIds are remapped; NeighborLists are removed. | `SimplnxCore::RequireMinNumNeighborsFilter`, both analytical oracle tests, and their exact compacted `NumNeighbors`, `Phases`, and `FeatureIds` assertions. | | 18 | Finalize | Inactive-feature removal fails - propagate the error returned by `RemoveInactiveObjects`. | *Not directly tested. Defensive check.* | -| 19 | Preflight | FeatureIds tuple count differs from the selected Image Geometry cell count - return `-55571`. | `SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - FeatureIds tuple count mismatch (-55571)` | +| 19 | Preflight | FeatureIds tuple count differs from the selected Image Geometry cell count - return `-59571`. | `SimplnxCore::RequireMinNumNeighborsFilter: Selected cell tuple counts` | | 20 | Preflight | A Cell Arrays to Ignore path is not a direct child of the selected geometry's Cell Attribute Matrix - return `-55573`. Membership uses the parent object's ID, so another path to the same matrix is accepted. | `SimplnxCore::RequireMinNumNeighborsFilter: Strict Feature Ids` - `Ignored array in a second Attribute Matrix`; shared-path acceptance is covered by `SimplnxCore::RequireMinNumNeighborsFilter: Shared Cell Paths and Selected Feature Matrix`. | | 21 | Preflight | The parent of Number of Neighbors is not an Attribute Matrix directly under the selected geometry, or is its Cell Attribute Matrix - return `-55574`. | `SimplnxCore::RequireMinNumNeighborsFilter: Strict Feature Ids` - `Feature array outside the selected geometry`, `Feature array under a plain DataGroup`, and `Feature array in the Cell Attribute Matrix`. | @@ -196,6 +196,7 @@ The filter selects features to retain, marks rejected-feature voxels, iterativel | `SimplnxCore::RequireMinNumNeighborsFilter` | kept | Modified: loads `6_5_test_data_1_v2.tar.gz`, recomputes neighbors, derives the expected retained `NumElements` sequence from the pre-filter input and threshold, checks all 791 retained values, asserts the single `-5558` preflight warning, and verifies tuple dimensions. | | `SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - tuple count mismatch (-252)` | kept | Creates 5-tuple `NumNeighbors` and 4-tuple `Phases` arrays, then asserts invalid preflight and exact code `-252`. | | `SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - FeatureIds outside Cell Attribute Matrix (-55575)` | new-for-V&V | Places `FeatureIds` in a second Attribute Matrix with the same tuple count as the Cell Attribute Matrix, then asserts invalid preflight and exact code `-55575`. | +| `SimplnxCore::RequireMinNumNeighborsFilter: Selected cell tuple counts` | new-for-V&V | Creates a 4-cell geometry with 3, 4, or 5 `FeatureIds` tuples stored separately. Equal counts pass; mismatches assert invalid preflight, exact code `-59571`, selected paths, tuple count, and cell count. | | `SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - feature ID out of range (-55567)` | new-for-V&V | Supplies `FeatureIds [1,4,2,3]` for four feature tuples, asserts valid preflight, invalid execution, and exact code `-55567`; covers D2. | | `SimplnxCore::RequireMinNumNeighborsFilter: Execute - negative feature ID is reassigned` | new-for-V&V | Supplies `FeatureIds [-1,1,1,2]`, asserts successful execution, and checks all four final IDs equal `[1,1,1,2]`; covers D1. | | `SimplnxCore::RequireMinNumNeighborsFilter: Execute Error - unavailable phase (-5555)` | new-for-V&V | Selects absent phase 5, then asserts valid preflight, invalid execution, and exact code `-5555`. | @@ -206,7 +207,6 @@ The filter selects features to retain, marks rejected-feature voxels, iterativel | `SIMPL 6.4 (Filter_Name)` | kept | `DYNAMIC_SECTION` using `test/simpl_conversion/6_4/RequireMinNumNeighborsFilter.json`; verifies name-based 6.4 pipeline conversion. | | `SimplnxCore::RequireMinNumNeighborsFilter: Bad Phase Number` | retired | Removed from `#if 0`; it used stale parameter keys and could not compile. The active `unavailable phase (-5555)` test replaces its intended error coverage. | | `SimplnxCore::RequireMinNumNeighborsFilter: Phase Array` | retired | Removed from `#if 0`; it used stale parameter keys and could not compile. Current analytical variants cover the all-phase and single-phase selection behavior. | -| `SimplnxCore::RequireMinNumNeighborsFilter: Preflight Error - FeatureIds tuple count mismatch (-55571)` | new-for-V&V | Creates a 5-cell geometry with four `FeatureIds` tuples in its Cell Attribute Matrix, then asserts invalid preflight and exact code `-55571`. | | `SimplnxCore::RequireMinNumNeighborsFilter: Strict Feature Ids` | new-for-V&V | Checks valid Cell and Feature Attribute Matrices, shared Cell paths, and read-only phases in another matrix. Rejects misplaced Feature Ids (`-55575`), ignored arrays (`-55573`), and invalid Feature Attribute Matrix locations (`-55574`); checks each error code and affected paths. | | `SimplnxCore::RequireMinNumNeighborsFilter: Shared Cell Paths and Selected Feature Matrix` | new-for-V&V | Alternates geometry and shared-parent paths for Feature Ids and ignored arrays. Checks geometry-based modification paths, reassigned IDs and cell values, unchanged ignored values, selected Feature Attribute Matrix compaction, unchanged external marker values, and inherited tuple dimensions. | diff --git a/src/simplnx/Utilities/DataArrayUtilities.cpp b/src/simplnx/Utilities/DataArrayUtilities.cpp index f871e73fa4..7594958cd2 100644 --- a/src/simplnx/Utilities/DataArrayUtilities.cpp +++ b/src/simplnx/Utilities/DataArrayUtilities.cpp @@ -155,6 +155,20 @@ Result<> ValidateCellTupleCount(const IGridGeometry& geometry, const DataPath& g return {}; } +Result<> ValidateCellTupleCount(const DataStructure& dataStructure, const DataPath& geometryPath, const std::vector& dataPaths, int32 errorCode) +{ + const auto& geometry = dataStructure.getDataRefAs(geometryPath); + for(const auto& dataPath : dataPaths) + { + const auto& array = dataStructure.getDataRefAs(dataPath); + if(auto result = ValidateCellTupleCount(geometry, geometryPath, array.getNumberOfTuples(), dataPath, errorCode); result.invalid()) + { + return result; + } + } + return {}; +} + bool IsChildOfAttributeMatrix(const DataStructure& dataStructure, const DataPath& arrayPath, const AttributeMatrix& attributeMatrix) { if(arrayPath.getLength() < 2) diff --git a/src/simplnx/Utilities/DataArrayUtilities.hpp b/src/simplnx/Utilities/DataArrayUtilities.hpp index 14fe101173..76dc5c16f9 100644 --- a/src/simplnx/Utilities/DataArrayUtilities.hpp +++ b/src/simplnx/Utilities/DataArrayUtilities.hpp @@ -28,7 +28,6 @@ #include #include #include -#include #include #if defined(_MSC_VER) @@ -63,9 +62,23 @@ class IGridGeometry; * @return Empty valid result if the counts match; otherwise, an error with both paths and counts. * * This check compares counts only. It does not read array values or require matching tuple shapes or a common parent. + * The error omits a filter-name prefix. The selected paths identify the geometry and data. */ SIMPLNX_EXPORT Result<> ValidateCellTupleCount(const IGridGeometry& geometry, const DataPath& geometryPath, usize tupleCount, const DataPath& dataPath, int32 errorCode); +/** + * @brief Checks that each array has one tuple per geometry cell. + * @param dataStructure Owns the geometry and arrays. + * @param geometryPath Identifies the geometry that supplies the cell count. + * @param dataPaths Identifies the arrays to check in order. + * @param errorCode Error code to return if the counts differ. + * @return Empty valid result if all counts match; otherwise, the first error with both paths and counts. + * @pre geometryPath identifies an IGridGeometry and dataPaths identify IArray objects. + * + * This check compares counts only. It does not read array values or require matching tuple shapes or a common parent. + */ +SIMPLNX_EXPORT Result<> ValidateCellTupleCount(const DataStructure& dataStructure, const DataPath& geometryPath, const std::vector& dataPaths, int32 errorCode); + /** * @brief Tests whether a path's parent identifies the specified Attribute Matrix. * @param dataStructure Resolves path identifiers. diff --git a/test/DataArrayTest.cpp b/test/DataArrayTest.cpp index 739fcbd1e3..bef779838e 100644 --- a/test/DataArrayTest.cpp +++ b/test/DataArrayTest.cpp @@ -94,6 +94,57 @@ TEST_CASE("DataArrayUtilities::ValidateCellTupleCount", "[simplnx][DataArrayUtil UnitTest::CheckArraysInheritTupleDims(dataStructure); } +TEST_CASE("DataArrayUtilities::ValidateCellTupleCount arrays", "[simplnx][DataArrayUtilities][TupleCount]") +{ + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + const DataPath geometryPath({"Image"}); + const usize tuples = GENERATE(usize{0}, usize{11}, usize{12}, usize{13}); + auto* otherAMPtr = AttributeMatrix::Create(dataStructure, "Other Data", ShapeType{tuples}); + REQUIRE(otherAMPtr != nullptr); + REQUIRE(Int32Array::Create(dataStructure, "Values", std::make_shared>(ShapeType{tuples}, ShapeType{2}, 0), otherAMPtr->getId()) != nullptr); + auto* stringsPtr = StringArray::Create(dataStructure, "Strings"); + REQUIRE(stringsPtr != nullptr); + const auto resizeResult = stringsPtr->resizeTuples({tuples}); + SIMPLNX_RESULT_REQUIRE_VALID(resizeResult); + REQUIRE(NeighborList::Create(dataStructure, "Neighbors", ShapeType{tuples}) != nullptr); + const DataPath path = GENERATE(DataPath({"Other Data", "Values"}), DataPath({"Strings"}), DataPath({"Neighbors"})); + REQUIRE(Int32Array::Create(dataStructure, "Matching", std::make_shared>(ShapeType{12}, ShapeType{1}, 0)) != nullptr); + const auto result = ValidateCellTupleCount(dataStructure, geometryPath, {DataPath({"Matching"}), path}, -98765); + if(tuples == 12) + { + SIMPLNX_RESULT_REQUIRE_VALID(result); + } + else + { + SIMPLNX_RESULT_REQUIRE_INVALID(result); + REQUIRE(result.errors().size() == 1); + REQUIRE(result.errors()[0].code == -98765); + REQUIRE(result.errors()[0].message == + fmt::format("The selected data '{}' has {} tuples, but the selected grid geometry 'Image' has 12 cells. Select data with one tuple per geometry cell.", path.toString(), tuples)); + } + const auto emptyResult = ValidateCellTupleCount(dataStructure, geometryPath, {}, -98765); + SIMPLNX_RESULT_REQUIRE_VALID(emptyResult); +} + +TEST_CASE("DataArrayUtilities::ValidateCellTupleCount arrays first mismatch", "[simplnx][DataArrayUtilities][TupleCount]") +{ + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, "Image"); + REQUIRE(imageGeomPtr != nullptr); + imageGeomPtr->setDimensions({2, 3, 2}); + REQUIRE(Int32Array::Create(dataStructure, "Matching", std::make_shared>(ShapeType{12}, ShapeType{1}, 0)) != nullptr); + REQUIRE(Int32Array::Create(dataStructure, "Short", std::make_shared>(ShapeType{11}, ShapeType{1}, 0)) != nullptr); + REQUIRE(Int32Array::Create(dataStructure, "Long", std::make_shared>(ShapeType{13}, ShapeType{1}, 0)) != nullptr); + const auto result = ValidateCellTupleCount(dataStructure, DataPath({"Image"}), {DataPath({"Matching"}), DataPath({"Short"}), DataPath({"Long"})}, -98765); + SIMPLNX_RESULT_REQUIRE_INVALID(result); + REQUIRE(result.errors().size() == 1); + REQUIRE(result.errors()[0].code == -98765); + REQUIRE(result.errors()[0].message == "The selected data 'Short' has 11 tuples, but the selected grid geometry 'Image' has 12 cells. Select data with one tuple per geometry cell."); +} + TEST_CASE("DataArrayUtilities::IsChildOfAttributeMatrix", "[simplnx][DataArrayUtilities]") { DataStructure dataStructure;