diff --git a/docs/ComputeGroupingDensityFilter.md b/docs/ComputeGroupingDensityFilter.md index b1b49ec..c8788be 100644 --- a/docs/ComputeGroupingDensityFilter.md +++ b/docs/ComputeGroupingDensityFilter.md @@ -1,22 +1,82 @@ # Compute Grouping Densities -**THIS FILTER IS UNTESTED, UNVERIFIED AND UNVALIDATED. IT IS AN EXPERIMENTAL FILTER THAT IS UNDERGOING LONG TERM DEVELOPMENT -AND TESTING. USE AT YOUR OWN RISK** - ## Group (Subgroup) Statistics (Reconstruction) ## Description -This **Filter** calculates the grouping densities for specific **Parent Features**. This filter is intended to be used for hierarchical reconstructions (i.e., reconstructions involving more than one segmentation; thus, the **Feature**-**Parent Feature** relationship). The **Filter** iterates through all **Features** that belong to each **Parent Feature,** querying each of the **Feature** **Neighbors** to determine if it was checked during grouping. A list of **Checked Features** is kept for each **Parent Feature**. Then, each **Parent Volume** is divided by the corresponding total volume of **Checked Features** to give the **Grouping Densities**. +This **Filter** computes a **Grouping Density** value for each **Parent Feature** in a hierarchical reconstruction. Hierarchical reconstructions involve more than one level of segmentation, creating a **Feature** to **Parent Feature** relationship (e.g., grains grouped into reconstructed parent grains). + +The **Grouping Density** quantifies how spatially concentrated the child **Features** of a **Parent Feature** are relative to the surrounding microstructure. A higher density value indicates the **Parent Feature's** volume is large relative to the region its child **Features** and their neighbors occupy, suggesting a tightly packed grouping. A lower density value indicates the child **Features** are spread out among many neighbors, suggesting a more dispersed grouping. + +### How It Works + +For each **Parent Feature**, the filter: + +1. Identifies all child **Features** that belong to the **Parent Feature** (via the **Parent IDs** array). +2. Collects all contiguous neighbors of those child **Features** (and optionally non-contiguous neighbors). +3. Accumulates the volumes of all collected ("checked") **Features** into a total checked volume. Each **Feature** is only counted once, even if it is a neighbor of multiple child **Features**. +4. Computes the **Grouping Density** as: + +``` +Grouping Density = Parent Volume / Total Checked Volume +``` + +If a **Parent Feature** has no child **Features** (total checked volume is zero), the **Grouping Density** is set to **-1.0** to indicate an invalid or empty parent. + +### Optional Parameters + +#### Use Non-Contiguous Neighbors + +When enabled, the filter also queries the **Non-Contiguous Neighbor List** for each child **Feature** in addition to the standard **Contiguous Neighbor List**. This expands the set of checked **Features** to include neighbors that are nearby but do not share a direct face/edge/vertex with the child **Feature**. Enable this option if non-contiguous neighbors were used during the original grouping step. Typically the filter "Compute Feature NeighborHoods" is used to generate the Non-contiguous Neighbors lists. That filter's parameter for the "Multiples of Average Diameter can have a large effect on the final Grouping Density value that is computed. + +#### Find Checked Features + +When enabled, the filter produces an additional output array (**Checked Features**) at the **Feature** level. For each **Feature** that was checked during the density computation, this array records which **Parent Feature** checked it. Since a **Feature** may be checked by multiple **Parent Features** (as a neighbor of children belonging to different parents), the assignment goes to the **Parent Feature** with the **largest Parent Volume**. This provides a way to see which parent had the strongest influence over each region of the microstructure. + -If non-contiguous neighbors were used in addition to standard neighbors for grouping, then the *Use Non-Contiguous Neighbors* Parameter may be used. +### Worked Example -Since many **Checked Features** are checked by more than one **Feature** during grouping, a premium is placed on the **Parent Feature** querying the **Checked Feature** having the largest **Parent Volume.** For **Checked Features** to be written, the *Find Checked Features* Parameter may be used. +Consider a 20x5 2D **Image Geometry** with unit spacing (1.0 x 1.0 x 1.0), containing 5 **Features** arranged as vertical bands and 2 **Parent Features**: + +![](Images/ComputeGroupingDensity_FeatureIds.png) + +![](Images/ComputeGroupingDensity_ParentIds.png) + +- Features 1, 2, 3 belong to Parent 1 (Parent Volume = 45, i.e. 10 + 20 + 15 cells) +- Features 4, 5 belong to Parent 2 (Parent Volume = 55, i.e. 25 + 30 cells) + +| Feature | Cells | Volume | Parent | Contiguous Neighbors | +|---------|-------|--------|--------|----------------------| +| 1 | 10 | 10.0 | 1 | {2} | +| 2 | 20 | 20.0 | 1 | {1, 3} | +| 3 | 15 | 15.0 | 1 | {2, 4} | +| 4 | 25 | 25.0 | 2 | {3, 5} | +| 5 | 30 | 30.0 | 2 | {4} | + +**Parent 1:** Child features {1, 2, 3} plus their contiguous neighbors include feature 4 (neighbor of feature 3). Total checked volume = 10 + 20 + 15 + 25 = 70. Density = 45 / 70 = **0.6429**. + +**Parent 2:** Child features {4, 5} plus their contiguous neighbors include feature 3 (neighbor of feature 4). Total checked volume = 25 + 30 + 15 = 70. Density = 55 / 70 = **0.7857**. + +Note that both densities are less than 1.0 because each parent's children have neighbors belonging to the *other* parent, expanding the total checked volume beyond the parent's own volume. + +### Interpreting Results + +| Density Value | Interpretation | +|---------------|----------------| +| > 1.0 | Parent volume exceeds the total checked region; the grouping is compact and dense | +| = 1.0 | Parent volume equals the total checked region | +| 0.0 < d < 1.0 | Parent volume is smaller than the total checked region; the grouping is dispersed | +| -1.0 | No child features found for this parent (invalid/empty parent) | % Auto generated parameter table will be inserted here + +### Algorithm Flowchart + +![ComputeGroupingDensity Algorithm Flowchart](Images/ComputeGroupingDensity_Algorithm.png) + ## References ## Example Pipelines diff --git a/docs/Images/ComputeGroupingDensity_Algorithm.png b/docs/Images/ComputeGroupingDensity_Algorithm.png new file mode 100644 index 0000000..5c41a96 Binary files /dev/null and b/docs/Images/ComputeGroupingDensity_Algorithm.png differ diff --git a/docs/Images/ComputeGroupingDensity_FeatureIds.png b/docs/Images/ComputeGroupingDensity_FeatureIds.png new file mode 100644 index 0000000..b04f707 Binary files /dev/null and b/docs/Images/ComputeGroupingDensity_FeatureIds.png differ diff --git a/docs/Images/ComputeGroupingDensity_ParentIds.png b/docs/Images/ComputeGroupingDensity_ParentIds.png new file mode 100644 index 0000000..a35a08f Binary files /dev/null and b/docs/Images/ComputeGroupingDensity_ParentIds.png differ diff --git a/src/SimplnxReview/Filters/Algorithms/ComputeGroupingDensity.cpp b/src/SimplnxReview/Filters/Algorithms/ComputeGroupingDensity.cpp index 31e20a2..3082220 100644 --- a/src/SimplnxReview/Filters/Algorithms/ComputeGroupingDensity.cpp +++ b/src/SimplnxReview/Filters/Algorithms/ComputeGroupingDensity.cpp @@ -4,6 +4,8 @@ #include "simplnx/DataStructure/NeighborList.hpp" #include "simplnx/Utilities/MessageHelper.hpp" +#include + using namespace nx::core; namespace @@ -11,21 +13,21 @@ namespace template struct FindDensitySpecializations { - static inline constexpr bool UsingNonContiguousNeighbors = UseNonContiguousNeighbors; - static inline constexpr bool FindingCheckedFeatures = FindCheckedFeatures; + static constexpr bool UsingNonContiguousNeighbors = UseNonContiguousNeighbors; + static constexpr bool FindingCheckedFeatures = FindCheckedFeatures; }; template > class FindDensityGrouping { public: - FindDensityGrouping(const std::atomic_bool& shouldCancel, const IFilter::MessageHandler& mesgHandler, const Int32Array& parentIds, const Float32Array& parentVolumes, const Float32Array& volumes, - const Int32NeighborList& contiguousNL, Float32Array& groupingDensities, Int32NeighborList& nonContiguousNL, Int32Array& checkedFeatures) + FindDensityGrouping(const std::atomic_bool& shouldCancel, const IFilter::MessageHandler& mesgHandler, const Int32Array& parentIds, const Float32Array& parentVolumes, + const Float32Array& featureVolumes, const Int32NeighborList& contiguousNL, Float32Array& groupingDensities, const Int32NeighborList& nonContiguousNL, Int32Array& checkedFeatures) : m_ShouldCancel(shouldCancel) , m_MessageHandler(mesgHandler) , m_ParentIds(parentIds) , m_ParentVolumes(parentVolumes) - , m_Volumes(volumes) + , m_FeatureVolumes(featureVolumes) , m_ContiguousNL(contiguousNL) , m_GroupingDensities(groupingDensities) , m_NonContiguousNL(nonContiguousNL) @@ -41,27 +43,23 @@ class FindDensityGrouping Result<> operator()() { - const auto& parentIds = m_ParentIds.getDataStoreRef(); - const auto& parentVolumes = m_ParentVolumes.getDataStoreRef(); - const auto& volumes = m_Volumes.getDataStoreRef(); - - auto& checkedFeatures = m_CheckedFeatures.getDataStoreRef(); - auto& groupingDensities = m_GroupingDensities.getDataStoreRef(); + // This is feature data, from 2 different Feature Attribute Matrix + const auto& featureParentIdsRef = m_ParentIds.getDataStoreRef(); + const auto& parentVolumesRef = m_ParentVolumes.getDataStoreRef(); + const auto& featureVolumesRef = m_FeatureVolumes.getDataStoreRef(); - usize numFeatures = volumes.getNumberOfTuples(); - usize numParents = parentVolumes.getNumberOfTuples(); + // These are output **Feature** level data arrays + auto& outCheckedFeaturesRef = m_CheckedFeatures.getDataStoreRef(); + auto& outGroupingDensitiesRef = m_GroupingDensities.getDataStoreRef(); - int kMax = 1; - if constexpr(FindDensitySpecializations::UsingNonContiguousNeighbors) - { - kMax = 2; - } + usize numFeatures = featureVolumesRef.getNumberOfTuples(); + usize numParents = parentVolumesRef.getNumberOfTuples(); - int32 numNeighbors, numNeighborhoods, numCurNeighborList, neigh; - float32 totalCheckVolume, curParentVolume; - std::set totalCheckList = {}; + float32 totalFeatureCheckVolume = 0.0f; + float32 curParentVolume = 0.0f; + std::unordered_set totalFeatureCheckList = {}; - std::vector checkedFeatureVolumes(1, 0.0f); + std::vector checkedFeatureVolumes = {0.0f}; if constexpr(FindDensitySpecializations::FindingCheckedFeatures) { // Default value-initialized to zeroes: https://en.cppreference.com/w/cpp/named_req/DefaultInsertable @@ -70,104 +68,100 @@ class FindDensityGrouping MessageHelper messageHelper(m_MessageHandler); ThrottledMessenger throttledMessenger = messageHelper.createThrottledMessenger(); - for(usize parentIdx = 1; parentIdx < numParents; parentIdx++) + // Start the Parent Outer Loop + for(usize currentParentId = 1; currentParentId < numParents; currentParentId++) { - throttledMessenger.sendThrottledMessage([&]() { return fmt::format("[{}%]", CalculatePercentComplete(parentIdx, numParents)); }); + throttledMessenger.sendThrottledMessage([&]() { return fmt::format("{}/{} {}%", currentParentId, numParents, CalculatePercentComplete(currentParentId, numParents)); }); if(m_ShouldCancel) { return {}; } - for(usize j = 1; j < numFeatures; j++) + + // Loop on each feature. + for(usize currentFeatureId = 1; currentFeatureId < numFeatures; currentFeatureId++) { - if(parentIds[j] == parentIdx) + // If the currentParentId is the same as the parentIds[currentFeatureId] and we have not added it to the `totalCheckList` + // then increment the volumes + if(featureParentIdsRef[currentFeatureId] == currentParentId) { - if(totalCheckList.find(j) == totalCheckList.end()) - // if(std::find(totalCheckList.begin(), totalCheckList.end(), j) == totalCheckList.end()) + if(!totalFeatureCheckList.contains(static_cast(currentFeatureId))) { - totalCheckVolume += m_Volumes[j]; - totalCheckList.insert(static_cast(j)); + totalFeatureCheckVolume += m_FeatureVolumes[currentFeatureId]; // Increment the checked volume by aggregating volumes from each feature that made up the parent feature + totalFeatureCheckList.insert(static_cast(currentFeatureId)); // This is to the list of checked features + if constexpr(FindDensitySpecializations::FindingCheckedFeatures) { - if(parentVolumes[parentIdx] > checkedFeatureVolumes[j]) + if(parentVolumesRef[currentParentId] > checkedFeatureVolumes[currentFeatureId]) { - checkedFeatureVolumes[j] = parentVolumes[parentIdx]; - checkedFeatures[j] = static_cast(parentIdx); + checkedFeatureVolumes[currentFeatureId] = parentVolumesRef[currentParentId]; + outCheckedFeaturesRef[currentFeatureId] = static_cast(currentParentId); } } } - numNeighbors = m_ContiguousNL.getListSize(static_cast(j)); + processNeighborListData(m_ContiguousNL, currentFeatureId, currentParentId, totalFeatureCheckList, totalFeatureCheckVolume, parentVolumesRef, checkedFeatureVolumes, outCheckedFeaturesRef); if constexpr(FindDensitySpecializations::UsingNonContiguousNeighbors) { - numNeighborhoods = static_cast(m_NonContiguousNL[j].size()); - } - for(int k = 0; k < kMax; k++) - { - if(k == 0) - { - numCurNeighborList = numNeighbors; - } - if constexpr(FindDensitySpecializations::UsingNonContiguousNeighbors) - { - if(k == 1) - { - numCurNeighborList = numNeighborhoods; - } - } - for(int32_t l = 0; l < numCurNeighborList; l++) - { - if(k == 0) - { - bool ok = false; - neigh = m_ContiguousNL.getValue(static_cast(j), l, ok); - } - else if(k == 1) - { - neigh = m_NonContiguousNL[j][l]; - } - if(totalCheckList.find(neigh) == totalCheckList.end()) - // if(std::find(totalCheckList.begin(), totalCheckList.end(), neigh) == totalCheckList.end()) - { - totalCheckVolume += m_Volumes[neigh]; - totalCheckList.insert(neigh); - if constexpr(FindDensitySpecializations::FindingCheckedFeatures) - { - if(parentVolumes[parentIdx] > checkedFeatureVolumes[neigh]) - { - checkedFeatureVolumes[neigh] = parentVolumes[parentIdx]; - checkedFeatures[neigh] = static_cast(parentIdx); - } - } - } - } + processNeighborListData(m_NonContiguousNL, currentFeatureId, currentParentId, totalFeatureCheckList, totalFeatureCheckVolume, parentVolumesRef, checkedFeatureVolumes, + outCheckedFeaturesRef); } } - } - curParentVolume = parentVolumes[parentIdx]; - if(totalCheckVolume == 0.0f) + } // END OF FEATURE ID LOOP + + curParentVolume = parentVolumesRef[currentParentId]; + if(totalFeatureCheckVolume == 0.0f) { - groupingDensities[parentIdx] = -1.0f; + outGroupingDensitiesRef[currentParentId] = -1.0f; } else { - groupingDensities[parentIdx] = (curParentVolume / totalCheckVolume); + outGroupingDensitiesRef[currentParentId] = (curParentVolume / totalFeatureCheckVolume); } - totalCheckList.clear(); - totalCheckVolume = 0.0f; - } + totalFeatureCheckList.clear(); + totalFeatureCheckVolume = 0.0f; + } // END OF PARENT ID LOOP return {}; } + void processNeighborListData(const NeighborList& neighborList, usize currentFeatureId, usize currentParentId, std::unordered_set& totalFeatureCheckList, + float32& totalFeatureCheckVolume, const AbstractDataStore& parentVolumesRef, std::vector& checkedFeatureVolumes, + AbstractDataStore& outCheckedFeaturesRef) + { + auto featureNeighbors = neighborList.at(currentFeatureId); + auto numNeighbors = static_cast(featureNeighbors.size()); + + for(int32 neighborIdx = 0; neighborIdx < numNeighbors; neighborIdx++) + { + auto neighborId = featureNeighbors.at(neighborIdx); + + // If the current neighbor is NOT in the check list... + if(!totalFeatureCheckList.contains(neighborId)) + { + // update the volumes and the check list + totalFeatureCheckVolume += m_FeatureVolumes[neighborId]; // Increment the total volume for this neighbor + totalFeatureCheckList.insert(neighborId); + if constexpr(FindDensitySpecializations::FindingCheckedFeatures) + { + if(parentVolumesRef[currentParentId] > checkedFeatureVolumes[neighborId]) + { + checkedFeatureVolumes[neighborId] = parentVolumesRef[currentParentId]; + outCheckedFeaturesRef[neighborId] = static_cast(currentParentId); + } + } + } + } + } + private: const std::atomic_bool& m_ShouldCancel; const IFilter::MessageHandler& m_MessageHandler; const Int32Array& m_ParentIds; const Float32Array& m_ParentVolumes; - const Float32Array& m_Volumes; + const Float32Array& m_FeatureVolumes; const Int32NeighborList& m_ContiguousNL; Float32Array& m_GroupingDensities; - Int32NeighborList& m_NonContiguousNL; + const Int32NeighborList& m_NonContiguousNL; Int32Array& m_CheckedFeatures; }; } // namespace @@ -191,34 +185,35 @@ const std::atomic_bool& ComputeGroupingDensity::getCancel() // ----------------------------------------------------------------------------- Result<> ComputeGroupingDensity::operator()() { - auto& parentIds = m_DataStructure.getDataRefAs(m_InputValues->ParentIdsPath); - auto& parentVolumes = m_DataStructure.getDataRefAs(m_InputValues->ParentVolumesPath); - auto& volumes = m_DataStructure.getDataRefAs(m_InputValues->VolumesPath); - auto& contiguousNL = m_DataStructure.getDataRefAs>(m_InputValues->ContiguousNLPath); + const auto& parentIds = m_DataStructure.getDataRefAs(m_InputValues->ParentIdsPath); + const auto& parentVolumes = m_DataStructure.getDataRefAs(m_InputValues->ParentVolumesPath); + const auto& featureVolumes = m_DataStructure.getDataRefAs(m_InputValues->VolumesPath); + const auto& contiguousNL = m_DataStructure.getDataRefAs>(m_InputValues->ContiguousNLPath); auto& groupingDensities = m_DataStructure.getDataRefAs(m_InputValues->GroupingDensitiesPath); // These may or may not be empty depending on the parameters - auto& nonContiguousNL = m_DataStructure.getDataRefAs>(m_InputValues->NonContiguousNLPath); + // The filter created some temporary hidden data array and neighbor list that may or may not + // get used for this. This setup does ensure the next 2 lines will actually return something. + const auto& nonContiguousNL = m_DataStructure.getDataRefAs>(m_InputValues->NonContiguousNLPath); auto& checkedFeatures = m_DataStructure.getDataRefAs(m_InputValues->CheckedFeaturesPath); if(m_InputValues->UseNonContiguousNeighbors) { if(m_InputValues->FindCheckedFeatures) { - return ::FindDensityGrouping>(getCancel(), m_MessageHandler, parentIds, parentVolumes, volumes, contiguousNL, groupingDensities, nonContiguousNL, + return ::FindDensityGrouping>(getCancel(), m_MessageHandler, parentIds, parentVolumes, featureVolumes, contiguousNL, groupingDensities, nonContiguousNL, checkedFeatures)(); } - return ::FindDensityGrouping>(getCancel(), m_MessageHandler, parentIds, parentVolumes, volumes, contiguousNL, groupingDensities, nonContiguousNL, + return ::FindDensityGrouping>(getCancel(), m_MessageHandler, parentIds, parentVolumes, featureVolumes, contiguousNL, groupingDensities, nonContiguousNL, checkedFeatures)(); } - else if(m_InputValues->FindCheckedFeatures) + + if(m_InputValues->FindCheckedFeatures) { - return ::FindDensityGrouping>(getCancel(), m_MessageHandler, parentIds, parentVolumes, volumes, contiguousNL, groupingDensities, nonContiguousNL, + return ::FindDensityGrouping>(getCancel(), m_MessageHandler, parentIds, parentVolumes, featureVolumes, contiguousNL, groupingDensities, nonContiguousNL, checkedFeatures)(); } - else - { - return ::FindDensityGrouping>(getCancel(), m_MessageHandler, parentIds, parentVolumes, volumes, contiguousNL, groupingDensities, nonContiguousNL, - checkedFeatures)(); - } + + return ::FindDensityGrouping>(getCancel(), m_MessageHandler, parentIds, parentVolumes, featureVolumes, contiguousNL, groupingDensities, nonContiguousNL, + checkedFeatures)(); } diff --git a/src/SimplnxReview/Filters/Algorithms/ComputeGroupingDensity.hpp b/src/SimplnxReview/Filters/Algorithms/ComputeGroupingDensity.hpp index 3b2dc60..ca7a99c 100644 --- a/src/SimplnxReview/Filters/Algorithms/ComputeGroupingDensity.hpp +++ b/src/SimplnxReview/Filters/Algorithms/ComputeGroupingDensity.hpp @@ -24,7 +24,7 @@ struct SIMPLNXREVIEW_EXPORT ComputeGroupingDensityInputValues /** * @class ComputeGroupingDensity - * @brief This filter determines the average C-axis location of each Feature. + * @brief Computes grouping densities for parent features in hierarchical reconstructions. */ class SIMPLNXREVIEW_EXPORT ComputeGroupingDensity diff --git a/src/SimplnxReview/Filters/Algorithms/GroupMicroTextureRegions.cpp b/src/SimplnxReview/Filters/Algorithms/GroupMicroTextureRegions.cpp index 317baed..64ac13a 100644 --- a/src/SimplnxReview/Filters/Algorithms/GroupMicroTextureRegions.cpp +++ b/src/SimplnxReview/Filters/Algorithms/GroupMicroTextureRegions.cpp @@ -59,10 +59,10 @@ Result<> GroupMicroTextureRegions::execute() if(m_InputValues->UseNonContiguousNeighbors) { nonContigNeighListPtr = m_DataStructure.getDataAs>(m_InputValues->NonContiguousNeighborListArrayPath); - } - if(nullptr == nonContigNeighListPtr) - { - return MakeErrorResult(-99345, "There was an error getting the Non-contiguous neighborlist from the DataStructure"); + if(nullptr == nonContigNeighListPtr) + { + return MakeErrorResult(-99345, "There was an error getting the Non-contiguous neighborlist from the DataStructure"); + } } std::vector groupList; diff --git a/src/SimplnxReview/Filters/ComputeGroupingDensityFilter.cpp b/src/SimplnxReview/Filters/ComputeGroupingDensityFilter.cpp index a2f79ff..f86de81 100644 --- a/src/SimplnxReview/Filters/ComputeGroupingDensityFilter.cpp +++ b/src/SimplnxReview/Filters/ComputeGroupingDensityFilter.cpp @@ -18,7 +18,7 @@ using namespace nx::core; namespace { const DataPath k_ThrowawayCheckedFeatures = DataPath({"HiddenTempCheckedFeatures"}); -const DataPath k_ThrowawayNonContiguous = DataPath({"HiddenContiguousNL"}); +const DataPath k_ThrowawayNonContiguous = DataPath({"HiddenNonContiguousNL"}); } // namespace namespace nx::core @@ -50,7 +50,7 @@ std::string ComputeGroupingDensityFilter::humanName() const //------------------------------------------------------------------------------ std::vector ComputeGroupingDensityFilter::defaultTags() const { - return {className(), "Statistics", "Reconstruction"}; + return {className(), "Statistics", "Reconstruction", "Microtexture"}; } //------------------------------------------------------------------------------ @@ -66,23 +66,25 @@ Parameters ComputeGroupingDensityFilter::parameters() const params.insert(std::make_unique(k_NonContiguousNeighborListArrayPath_Key, "Non-Contiguous Neighborhoods", "List of non-contiguous neighbors for each Feature.", DataPath{}, NeighborListSelectionParameter::AllowedTypes{DataType::int32})); - params.insertSeparator(Parameters::Separator{"Input Cell Data"}); - params.insert(std::make_unique(k_ParentIdsPath_Key, "Parent Ids", "Input Cell level ParentIds", DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::int32}, - ArraySelectionParameter::AllowedComponentShapes{{1}})); - params.insertSeparator(Parameters::Separator{"Input Feature Data"}); - params.insert(std::make_unique(k_VolumesArrayPath_Key, "Volumes", "The Feature Volumes Data Array", DataPath{}, + params.insert(std::make_unique(k_ParentIdsPath_Key, "Feature Parent Ids", "Input Feature level ParentIds", DataPath{}, + ArraySelectionParameter::AllowedTypes{DataType::int32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); + + params.insert(std::make_unique(k_FeatureVolumesArrayPath_Key, "Feature Volumes", "The Feature Volumes Data Array", DataPath{}, ArraySelectionParameter::AllowedTypes{nx::core::DataType::float32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); params.insert(std::make_unique(k_ContiguousNeighborListArrayPath_Key, "Contiguous Neighbor List", "List of contiguous neighbors for each Feature.", DataPath{}, NeighborListSelectionParameter::AllowedTypes{DataType::int32})); - params.insert(std::make_unique(k_ParentVolumesPath_Key, "Parent Volumes", "Input feature level parent volume data array", DataPath{}, + params.insertSeparator(Parameters::Separator{"Input Parent Feature Data"}); + params.insert(std::make_unique(k_ParentVolumesPath_Key, "Parent Volumes", "Input Parent feature level volumes data array", DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::float32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); params.insertSeparator(Parameters::Separator{"Output Feature Data"}); params.insert(std::make_unique(k_CheckedFeaturesName_Key, "Checked Features Name", "Output feature level data array to hold 'Checked Features' values", "Checked Features")); + + params.insertSeparator(Parameters::Separator{"Output Parent Feature Data"}); params.insert( std::make_unique(k_GroupingDensitiesName_Key, "Grouping Densities Name", "Output feature level data array to hold 'Grouping Densities' values", "Grouping Densities")); @@ -110,41 +112,54 @@ IFilter::PreflightResult ComputeGroupingDensityFilter::preflightImpl(const DataS const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { auto pParentIdsPath = filterArgs.value(k_ParentIdsPath_Key); - auto pParentVolumesPath = filterArgs.value(k_ParentVolumesPath_Key); + auto pFeatureVolumesPath = filterArgs.value(k_FeatureVolumesArrayPath_Key); auto pContiguousNLPath = filterArgs.value(k_ContiguousNeighborListArrayPath_Key); - auto pVolumesPath = filterArgs.value(k_VolumesArrayPath_Key); - auto pGroupingDensitiesName = filterArgs.value(k_GroupingDensitiesName_Key); + + auto pParentVolumesPath = filterArgs.value(k_ParentVolumesPath_Key); auto pUseNonContiguousNeighbors = filterArgs.value(k_UseNonContiguousNeighbors_Key); auto pNonContiguousNLPath = filterArgs.value(k_NonContiguousNeighborListArrayPath_Key); + auto pFindCheckedFeatures = filterArgs.value(k_FindCheckedFeatures_Key); auto pCheckedFeaturesName = filterArgs.value(k_CheckedFeaturesName_Key); + auto pGroupingDensitiesName = filterArgs.value(k_GroupingDensitiesName_Key); Result resultOutputActions; std::vector preflightUpdatedValues; - auto* pParentAM = dataStructure.getDataAs(pParentVolumesPath.getParent()); - if(pParentAM == nullptr) + // Selection parameters auto-validate existence, so use references directly + const auto& parentIds = dataStructure.getDataRefAs(pParentIdsPath); + const auto& featureVolumes = dataStructure.getDataRefAs(pFeatureVolumesPath); + const auto& contiguousNL = dataStructure.getDataRefAs(pContiguousNLPath); + + // Make sure all these arrays and neighbor lists all come from the same attribute matrix or at least have the same number of tuples + if(parentIds.getNumberOfTuples() != featureVolumes.getNumberOfTuples() || parentIds.getNumberOfTuples() != contiguousNL.getNumberOfTuples()) { - return MakePreflightErrorResult(-15670, fmt::format("Parent Volumes [{}] must be stored in an Attribute Matrix.", pParentVolumesPath.toString())); + return MakePreflightErrorResult(-15671, fmt::format("All Input Feature level data arrays and neighbor lists MUST have the same number of tuples.\n{}: {}\n{}: {}\n{}: {}", + pParentIdsPath.toString(), parentIds.getNumberOfTuples(), pFeatureVolumesPath.toString(), featureVolumes.getNumberOfTuples(), + pContiguousNLPath.toString(), contiguousNL.getNumberOfTuples())); } + if(pUseNonContiguousNeighbors) { - DataPath groupingDataPath = pParentVolumesPath.replaceName(pGroupingDensitiesName); - auto createArrayAction = std::make_unique(nx::core::DataType::float32, pParentAM->getShape(), std::vector{1}, groupingDataPath); - resultOutputActions.value().appendAction(std::move(createArrayAction)); + const auto& nonContiguousNL = dataStructure.getDataRefAs(pNonContiguousNLPath); + if(parentIds.getNumberOfTuples() != nonContiguousNL.getNumberOfTuples()) + { + return MakePreflightErrorResult(-15672, fmt::format("All Input Feature level data arrays and neighbor lists MUST have the same number of tuples.\n{}: {}\n{}: {}", pParentIdsPath.toString(), + parentIds.getNumberOfTuples(), pNonContiguousNLPath.toString(), nonContiguousNL.getNumberOfTuples())); + } } - auto* pFeatureAM = dataStructure.getDataAs(pVolumesPath.getParent()); + auto* pFeatureAM = dataStructure.getDataAs(pFeatureVolumesPath.getParent()); if(pFeatureAM == nullptr) { - return MakePreflightErrorResult(-15671, fmt::format("Feature Volumes [{}] must be stored in an Attribute Matrix.", pVolumesPath.toString())); + return MakePreflightErrorResult(-15673, fmt::format("Feature Volumes [{}] must be stored in an Attribute Matrix.", pFeatureVolumesPath.toString())); } if(pFindCheckedFeatures) { - DataPath checkedFeaturesPath = pVolumesPath.replaceName(pCheckedFeaturesName); { - auto createArrayAction = std::make_unique(nx::core::DataType::int32, pFeatureAM->getShape(), std::vector{1}, checkedFeaturesPath); + DataPath checkedFeaturesPath = pFeatureVolumesPath.replaceName(pCheckedFeaturesName); + auto createArrayAction = std::make_unique(nx::core::DataType::int32, pFeatureAM->getShape(), ShapeType{1}, checkedFeaturesPath); resultOutputActions.value().appendAction(std::move(createArrayAction)); } } @@ -172,8 +187,17 @@ IFilter::PreflightResult ComputeGroupingDensityFilter::preflightImpl(const DataS } } - preflightUpdatedValues.push_back({"WARNING: This filter is experimental in nature and has not had any testing, validation or verification. Use at your own risk"}); - resultOutputActions.warnings().push_back({-65432, "WARNING: This filter is experimental in nature and has not had any testing, validation or verification. Use at your own risk"}); + auto* pParentAM = dataStructure.getDataAs(pParentVolumesPath.getParent()); + if(pParentAM == nullptr) + { + return MakePreflightErrorResult(-15670, fmt::format("Parent Volumes [{}] must be stored in an Attribute Matrix.", pParentVolumesPath.toString())); + } + + { + DataPath groupingDataPath = pParentVolumesPath.replaceName(pGroupingDensitiesName); + auto createArrayAction = std::make_unique(nx::core::DataType::float32, pParentAM->getShape(), std::vector{1}, groupingDataPath); + resultOutputActions.value().appendAction(std::move(createArrayAction)); + } return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; } @@ -187,7 +211,7 @@ Result<> ComputeGroupingDensityFilter::executeImpl(DataStructure& dataStructure, inputValues.ParentIdsPath = filterArgs.value(k_ParentIdsPath_Key); inputValues.ParentVolumesPath = filterArgs.value(k_ParentVolumesPath_Key); inputValues.ContiguousNLPath = filterArgs.value(k_ContiguousNeighborListArrayPath_Key); - inputValues.VolumesPath = filterArgs.value(k_VolumesArrayPath_Key); + inputValues.VolumesPath = filterArgs.value(k_FeatureVolumesArrayPath_Key); inputValues.GroupingDensitiesPath = inputValues.ParentVolumesPath.replaceName(filterArgs.value(k_GroupingDensitiesName_Key)); inputValues.UseNonContiguousNeighbors = filterArgs.value(k_UseNonContiguousNeighbors_Key); @@ -245,7 +269,7 @@ Result ComputeGroupingDensityFilter::FromSIMPLJson(const nlohmann::js results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_ParentIdsArrayPathKey, k_ParentIdsPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_ParentVolumesArrayPathKey, k_ParentVolumesPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_UseNonContiguousNeighborsKey, k_UseNonContiguousNeighbors_Key)); - results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_VolumesArrayPathKey, k_VolumesArrayPath_Key)); + results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_VolumesArrayPathKey, k_FeatureVolumesArrayPath_Key)); Result<> conversionResult = MergeResults(std::move(results)); diff --git a/src/SimplnxReview/Filters/ComputeGroupingDensityFilter.hpp b/src/SimplnxReview/Filters/ComputeGroupingDensityFilter.hpp index 0614178..5c9abb9 100644 --- a/src/SimplnxReview/Filters/ComputeGroupingDensityFilter.hpp +++ b/src/SimplnxReview/Filters/ComputeGroupingDensityFilter.hpp @@ -9,7 +9,7 @@ namespace nx::core { /** * @class ComputeGroupingDensityFilter - * @brief This filter determines the average C-axis location of each Feature + * @brief Computes grouping densities for parent features in hierarchical reconstructions */ class SIMPLNXREVIEW_EXPORT ComputeGroupingDensityFilter : public IFilter { @@ -24,7 +24,7 @@ class SIMPLNXREVIEW_EXPORT ComputeGroupingDensityFilter : public IFilter ComputeGroupingDensityFilter& operator=(ComputeGroupingDensityFilter&&) noexcept = delete; // Parameter Keys - static constexpr StringLiteral k_VolumesArrayPath_Key = "volumes_path"; + static constexpr StringLiteral k_FeatureVolumesArrayPath_Key = "volumes_path"; static constexpr StringLiteral k_ContiguousNeighborListArrayPath_Key = "contiguous_neighbor_list_path"; static constexpr StringLiteral k_UseNonContiguousNeighbors_Key = "use_non_contiguous_neighbors"; static constexpr StringLiteral k_NonContiguousNeighborListArrayPath_Key = "non_contiguous_neighbor_list_path"; diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 7811af1..ea3a9e0 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -6,9 +6,10 @@ # ------------------------------------------------------------------------------ # Define the list of unit test source files set(${PLUGIN_NAME}UnitTest_SRCS - MergeColoniesTest.cpp + ComputeGroupingDensityTest.cpp ComputeLocalAverageCAxisMisalignmentsTest.cpp ComputeMicroTextureRegionsTest.cpp + MergeColoniesTest.cpp ) set(DISABLED_TESTS @@ -58,7 +59,7 @@ if(EXISTS "${DREAM3D_DATA_DIR}" AND SIMPLNX_DOWNLOAD_TEST_FILES) file(MAKE_DIRECTORY "${DREAM3D_DATA_DIR}/TestFiles/") endif() -# download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME 12_IN625_GBCD.tar.gz SHA512 f696a8af181505947e6fecfdb1a11fda6c762bba5e85fea8d484b1af00bf18643e1d930d48f092ee238d1c19c9ce7c4fb5a8092d17774bda867961a1400e9cea) + download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME compute_grouping_densities.tar.gz SHA512 96066196d6aa5f87cc7b717f959848c2f3025b7129589abe1eded2a8d725c539a89b0a6290a388a56b5a401e0bd3041698fbd8e8cf37a1f18fdd937debd21531) endif() diff --git a/test/ComputeGroupingDensityTest.cpp b/test/ComputeGroupingDensityTest.cpp new file mode 100644 index 0000000..b9d0caa --- /dev/null +++ b/test/ComputeGroupingDensityTest.cpp @@ -0,0 +1,431 @@ +#include + +#include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/DataStructure/NeighborList.hpp" +#include "simplnx/Parameters/ArraySelectionParameter.hpp" +#include "simplnx/Parameters/BoolParameter.hpp" +#include "simplnx/Parameters/DataObjectNameParameter.hpp" +#include "simplnx/Parameters/NeighborListSelectionParameter.hpp" +#include "simplnx/UnitTest/UnitTestCommon.hpp" + +#include "SimplnxReview/Filters/ComputeGroupingDensityFilter.hpp" +#include "SimplnxReview/SimplnxReview_test_dirs.hpp" + +using namespace nx::core; + +namespace +{ +// DataStructure path constants +const std::string k_ImageGeomName = "ImageGeom"; +const std::string k_FeatureAMName = "CellFeatureData"; +const std::string k_ParentAMName = "ParentFeatureData"; +const std::string k_VolumesName = "Volumes"; +const std::string k_ParentIdsName = "ParentIds"; +const std::string k_ContiguousNLName = "ContiguousNeighborList"; +const std::string k_NonContiguousNLName = "NonContiguousNeighborList"; +const std::string k_ParentVolumesName = "Volumes"; +const std::string k_ComputedGroupingDensitiesName = "Computed GroupingDensities"; +const std::string k_CheckedFeaturesName = "CheckedFeatures"; + +const DataPath k_VolumesPath = DataPath({k_ImageGeomName, k_FeatureAMName, k_VolumesName}); +const DataPath k_ParentIdsPath = DataPath({k_ImageGeomName, k_FeatureAMName, k_ParentIdsName}); +const DataPath k_ContiguousNLPath = DataPath({k_ImageGeomName, k_FeatureAMName, k_ContiguousNLName}); +const DataPath k_NonContiguousNLPath = DataPath({k_ImageGeomName, k_FeatureAMName, k_NonContiguousNLName}); +const DataPath k_ParentVolumesPath = DataPath({k_ImageGeomName, k_ParentAMName, k_ParentVolumesName}); +const DataPath k_GroupingDensitiesPath = DataPath({k_ImageGeomName, k_ParentAMName, k_ComputedGroupingDensitiesName}); +const DataPath k_CheckedFeaturesPath = DataPath({k_ImageGeomName, k_FeatureAMName, k_CheckedFeaturesName}); + +// Test data dimensions matching the 20x5 2D Image Geometry: +// 6 features (index 0 = placeholder, features 1-5) +// 3 parents (index 0 = placeholder, parents 1-2) +// Features 1,2,3 -> Parent 1 (volume = 10+20+15 = 45) +// Features 4,5 -> Parent 2 (volume = 25+30 = 55) +constexpr usize k_NumFeatures = 6; +constexpr usize k_NumParents = 3; + +/** + * @brief Builds a DataStructure with all input data needed for the ComputeGroupingDensity filter. + * Optionally includes a non-contiguous neighbor list. + * + * Data matches the 20x5 2D Image Geometry worked example: + * Feature Volumes: [0, 10, 20, 15, 25, 30] + * Parent IDs: [0, 1, 1, 1, 2, 2] + * Parent Volumes: [0, 45, 55] + * Contiguous Neighbors: chain 1-2-3-4-5 + */ +DataStructure createTestDataStructure(bool includeNonContiguousNL) +{ + DataStructure dataStructure; + + // Create ImageGeom (just a container for the AMs) + auto* imageGeom = ImageGeom::Create(dataStructure, k_ImageGeomName); + imageGeom->setDimensions({1, 1, 1}); + + // Feature-level AttributeMatrix (6 tuples: indices 0-5) + auto* featureAM = AttributeMatrix::Create(dataStructure, k_FeatureAMName, {k_NumFeatures}, imageGeom->getId()); + + // Parent-level AttributeMatrix (3 tuples: indices 0-2) + auto* parentAM = AttributeMatrix::Create(dataStructure, k_ParentAMName, {k_NumParents}, imageGeom->getId()); + + // --- Feature-level arrays --- + + // Feature Volumes: [0, 10, 20, 15, 25, 30] + auto* featureVolumes = UnitTest::CreateTestDataArray(dataStructure, k_VolumesName, {k_NumFeatures}, {1}, featureAM->getId()); + auto& featureVolumesRef = featureVolumes->getDataStoreRef(); + featureVolumesRef[0] = 0.0f; + featureVolumesRef[1] = 10.0f; + featureVolumesRef[2] = 20.0f; + featureVolumesRef[3] = 15.0f; + featureVolumesRef[4] = 25.0f; + featureVolumesRef[5] = 30.0f; + + // Parent IDs: [0, 1, 1, 1, 2, 2] + auto* parentIds = UnitTest::CreateTestDataArray(dataStructure, k_ParentIdsName, {k_NumFeatures}, {1}, featureAM->getId()); + auto& parentIdsRef = parentIds->getDataStoreRef(); + parentIdsRef[0] = 0; + parentIdsRef[1] = 1; + parentIdsRef[2] = 1; + parentIdsRef[3] = 1; + parentIdsRef[4] = 2; + parentIdsRef[5] = 2; + + // Contiguous Neighbor List (chain: 1-2-3-4-5) + // Feature 0: {} + // Feature 1: {2} + // Feature 2: {1, 3} + // Feature 3: {2, 4} + // Feature 4: {3, 5} + // Feature 5: {4} + auto* contiguousNL = NeighborList::Create(dataStructure, k_ContiguousNLName, ShapeType{k_NumFeatures}, featureAM->getId()); + contiguousNL->setList(0, std::make_shared>(std::vector{})); + contiguousNL->setList(1, std::make_shared>(std::vector{2})); + contiguousNL->setList(2, std::make_shared>(std::vector{1, 3})); + contiguousNL->setList(3, std::make_shared>(std::vector{2, 4})); + contiguousNL->setList(4, std::make_shared>(std::vector{3, 5})); + contiguousNL->setList(5, std::make_shared>(std::vector{4})); + + // Non-Contiguous Neighbor List (optional) + // Feature 0: {} + // Feature 1: {4} + // Feature 2: {5} + // Feature 3: {} + // Feature 4: {1} + // Feature 5: {2} + if(includeNonContiguousNL) + { + auto* nonContiguousNL = NeighborList::Create(dataStructure, k_NonContiguousNLName, ShapeType{k_NumFeatures}, featureAM->getId()); + nonContiguousNL->setList(0, std::make_shared>(std::vector{})); + nonContiguousNL->setList(1, std::make_shared>(std::vector{4})); + nonContiguousNL->setList(2, std::make_shared>(std::vector{5})); + nonContiguousNL->setList(3, std::make_shared>(std::vector{})); + nonContiguousNL->setList(4, std::make_shared>(std::vector{1})); + nonContiguousNL->setList(5, std::make_shared>(std::vector{2})); + } + + // --- Parent-level arrays --- + + // Parent Volumes: [0, 45, 55] (sum of child feature cell volumes) + auto* parentVolumes = UnitTest::CreateTestDataArray(dataStructure, k_ParentVolumesName, {k_NumParents}, {1}, parentAM->getId()); + auto& parentVolumesRef = parentVolumes->getDataStoreRef(); + parentVolumesRef[0] = 0.0f; + parentVolumesRef[1] = 45.0f; + parentVolumesRef[2] = 55.0f; + + return dataStructure; +} + +/** + * @brief Creates the filter Arguments for the given boolean option combination. + */ +Arguments createFilterArgs(bool useNonContiguous, bool findCheckedFeatures) +{ + Arguments args; + args.insertOrAssign(ComputeGroupingDensityFilter::k_FeatureVolumesArrayPath_Key, std::make_any(k_VolumesPath)); + args.insertOrAssign(ComputeGroupingDensityFilter::k_ContiguousNeighborListArrayPath_Key, std::make_any(k_ContiguousNLPath)); + args.insertOrAssign(ComputeGroupingDensityFilter::k_UseNonContiguousNeighbors_Key, std::make_any(useNonContiguous)); + args.insertOrAssign(ComputeGroupingDensityFilter::k_NonContiguousNeighborListArrayPath_Key, std::make_any(useNonContiguous ? k_NonContiguousNLPath : DataPath{})); + args.insertOrAssign(ComputeGroupingDensityFilter::k_ParentIdsPath_Key, std::make_any(k_ParentIdsPath)); + args.insertOrAssign(ComputeGroupingDensityFilter::k_ParentVolumesPath_Key, std::make_any(k_ParentVolumesPath)); + args.insertOrAssign(ComputeGroupingDensityFilter::k_FindCheckedFeatures_Key, std::make_any(findCheckedFeatures)); + args.insertOrAssign(ComputeGroupingDensityFilter::k_CheckedFeaturesName_Key, std::make_any(k_CheckedFeaturesName)); + args.insertOrAssign(ComputeGroupingDensityFilter::k_GroupingDensitiesName_Key, std::make_any(k_ComputedGroupingDensitiesName)); + return args; +} +} // namespace + +// ============================================================================= +// Exemplar-Based Test - Compare against DREAM3D-NX pipeline output +// ============================================================================= + +TEST_CASE("SimplnxReview::ComputeGroupingDensityFilter: Basic Density (contiguous, no checked features)", "[SimplnxReview][ComputeGroupingDensityFilter]") +{ + + const std::string k_GroupingDensitiesName = "GroupingDensities (false, false)"; + const DataPath k_ExemplarGroupingDensitiesPath = DataPath({k_ImageGeomName, k_ParentAMName, k_GroupingDensitiesName}); + + UnitTest::LoadPlugins(); + + const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "compute_grouping_densities.tar.gz", "compute_grouping_densities"); + + // Read Exemplar DREAM3D File Filter + auto exemplarFilePath = fs::path(fmt::format("{}/compute_grouping_densities/compute_grouping_densities.dream3d", unit_test::k_TestFilesDir)); + DataStructure dataStructure = UnitTest::LoadDataStructure(exemplarFilePath); + + ComputeGroupingDensityFilter filter; + Arguments args; + args.insertOrAssign(ComputeGroupingDensityFilter::k_FeatureVolumesArrayPath_Key, std::make_any(k_VolumesPath)); + args.insertOrAssign(ComputeGroupingDensityFilter::k_ContiguousNeighborListArrayPath_Key, std::make_any(k_ContiguousNLPath)); + args.insertOrAssign(ComputeGroupingDensityFilter::k_UseNonContiguousNeighbors_Key, std::make_any(false)); + args.insertOrAssign(ComputeGroupingDensityFilter::k_NonContiguousNeighborListArrayPath_Key, std::make_any(DataPath{})); + args.insertOrAssign(ComputeGroupingDensityFilter::k_ParentIdsPath_Key, std::make_any(k_ParentIdsPath)); + args.insertOrAssign(ComputeGroupingDensityFilter::k_ParentVolumesPath_Key, std::make_any(k_ParentVolumesPath)); + args.insertOrAssign(ComputeGroupingDensityFilter::k_FindCheckedFeatures_Key, std::make_any(false)); + args.insertOrAssign(ComputeGroupingDensityFilter::k_CheckedFeaturesName_Key, std::make_any(k_CheckedFeaturesName)); + args.insertOrAssign(ComputeGroupingDensityFilter::k_GroupingDensitiesName_Key, std::make_any(k_ComputedGroupingDensitiesName)); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) + + auto executeResult = filter.execute(dataStructure, args, nullptr, IFilter::MessageHandler{[](const IFilter::Message& message) { fmt::print("{}\n", message.message); }}); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + // Compare computed densities against the exemplar from the DREAM3D-NX pipeline + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_GroupingDensitiesPath)); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_ExemplarGroupingDensitiesPath)); + + const auto& computedDensities = dataStructure.getDataRefAs(k_GroupingDensitiesPath); + const auto& exemplarDensities = dataStructure.getDataRefAs(k_ExemplarGroupingDensitiesPath); + + REQUIRE(computedDensities.getNumberOfTuples() == exemplarDensities.getNumberOfTuples()); + for(usize i = 0; i < computedDensities.getNumberOfTuples(); i++) + { + REQUIRE(computedDensities[i] == Approx(exemplarDensities[i]).epsilon(0.0001f)); + } + + // Verify against hand-calculated values: + // Parent Volumes: [0, 45, 55] + // Parent 1: children {1,2,3}, neighbors add feature 4 + // totalCheckVolume = 10 + 20 + 15 + 25 = 70 + // density = 45 / 70 = 0.642857 + // Parent 2: children {4,5}, neighbors add feature 3 + // totalCheckVolume = 25 + 30 + 15 = 70 + // density = 55 / 70 = 0.785714 + REQUIRE(computedDensities[1] == Approx(45.0f / 70.0f).epsilon(0.0001f)); + REQUIRE(computedDensities[2] == Approx(55.0f / 70.0f).epsilon(0.0001f)); +} + +// ============================================================================= +// Execution Tests - Exercise all 4 template specializations +// ============================================================================= + +TEST_CASE("SimplnxReview::ComputeGroupingDensityFilter: Contiguous Only, No Checked Features", "[SimplnxReview][ComputeGroupingDensityFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure = createTestDataStructure(false); + ComputeGroupingDensityFilter filter; + Arguments args = createFilterArgs(false, false); + + auto executeResult = filter.execute(dataStructure, args, nullptr, IFilter::MessageHandler{[](const IFilter::Message& message) { fmt::print("{}\n", message.message); }}); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + // Parent 1: children {1,2,3}, contiguous neighbors add feature 4 + // totalCheckVolume = 10 + 20 + 15 + 25 = 70 + // density = 45 / 70 = 0.642857 + // Parent 2: children {4,5}, contiguous neighbors add feature 3 + // totalCheckVolume = 25 + 30 + 15 = 70 + // density = 55 / 70 = 0.785714 + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_GroupingDensitiesPath)); + const auto& groupingDensities = dataStructure.getDataRefAs(k_GroupingDensitiesPath); + + REQUIRE(groupingDensities[1] == Approx(45.0f / 70.0f).epsilon(0.0001f)); + REQUIRE(groupingDensities[2] == Approx(55.0f / 70.0f).epsilon(0.0001f)); +} + +TEST_CASE("SimplnxReview::ComputeGroupingDensityFilter: With Non-Contiguous Neighbors", "[SimplnxReview][ComputeGroupingDensityFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure = createTestDataStructure(true); + ComputeGroupingDensityFilter filter; + Arguments args = createFilterArgs(true, false); + + auto executeResult = filter.execute(dataStructure, args, nullptr, IFilter::MessageHandler{[](const IFilter::Message& message) { fmt::print("{}\n", message.message); }}); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + // With non-contiguous neighbors, all 5 features get checked for each parent + // Parent 1: totalCheckVolume = 10+20+15+25+30 = 100, density = 45/100 = 0.45 + // Parent 2: totalCheckVolume = 25+30+15+10+20 = 100, density = 55/100 = 0.55 + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_GroupingDensitiesPath)); + const auto& groupingDensities = dataStructure.getDataRefAs(k_GroupingDensitiesPath); + + REQUIRE(groupingDensities[1] == Approx(45.0f / 100.0f).epsilon(0.0001f)); + REQUIRE(groupingDensities[2] == Approx(55.0f / 100.0f).epsilon(0.0001f)); +} + +TEST_CASE("SimplnxReview::ComputeGroupingDensityFilter: With Checked Features", "[SimplnxReview][ComputeGroupingDensityFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure = createTestDataStructure(false); + ComputeGroupingDensityFilter filter; + Arguments args = createFilterArgs(false, true); + + auto executeResult = filter.execute(dataStructure, args, nullptr, IFilter::MessageHandler{[](const IFilter::Message& message) { fmt::print("{}\n", message.message); }}); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + // Densities same as contiguous-only case + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_GroupingDensitiesPath)); + const auto& groupingDensities = dataStructure.getDataRefAs(k_GroupingDensitiesPath); + + REQUIRE(groupingDensities[1] == Approx(45.0f / 70.0f).epsilon(0.0001f)); + REQUIRE(groupingDensities[2] == Approx(55.0f / 70.0f).epsilon(0.0001f)); + + // Checked features: each feature is assigned to the parent with the largest volume that checked it + // Parent 1 (vol=45) processes first and checks features {1,2,3,4} + // Parent 2 (vol=55) processes second and checks features {3,4,5} + // Feature 3: checked by Parent 1 (45) then Parent 2 (55 > 45) -> overridden to Parent 2 + // Feature 4: checked by Parent 1 (45) then Parent 2 (55 > 45) -> overridden to Parent 2 + // Feature 5: only checked by Parent 2 + // Expected: [0, 1, 1, 2, 2, 2] + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_CheckedFeaturesPath)); + const auto& checkedFeatures = dataStructure.getDataRefAs(k_CheckedFeaturesPath); + + REQUIRE(checkedFeatures[0] == 0); + REQUIRE(checkedFeatures[1] == 1); + REQUIRE(checkedFeatures[2] == 1); + REQUIRE(checkedFeatures[3] == 2); + REQUIRE(checkedFeatures[4] == 2); + REQUIRE(checkedFeatures[5] == 2); +} + +TEST_CASE("SimplnxReview::ComputeGroupingDensityFilter: Both Options Enabled", "[SimplnxReview][ComputeGroupingDensityFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure = createTestDataStructure(true); + ComputeGroupingDensityFilter filter; + Arguments args = createFilterArgs(true, true); + + auto executeResult = filter.execute(dataStructure, args, nullptr, IFilter::MessageHandler{[](const IFilter::Message& message) { fmt::print("{}\n", message.message); }}); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + // With non-contiguous neighbors, all features get checked by both parents + // Parent 1: density = 45/100 = 0.45 + // Parent 2: density = 55/100 = 0.55 + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_GroupingDensitiesPath)); + const auto& groupingDensities = dataStructure.getDataRefAs(k_GroupingDensitiesPath); + + REQUIRE(groupingDensities[1] == Approx(45.0f / 100.0f).epsilon(0.0001f)); + REQUIRE(groupingDensities[2] == Approx(55.0f / 100.0f).epsilon(0.0001f)); + + // Parent 1 (vol=45) checks ALL features {1,2,3,4,5} via non-contiguous links + // Parent 2 (vol=55) also checks ALL features, and 55 > 45 so all get overridden + // Expected: [0, 2, 2, 2, 2, 2] + REQUIRE_NOTHROW(dataStructure.getDataRefAs(k_CheckedFeaturesPath)); + const auto& checkedFeatures = dataStructure.getDataRefAs(k_CheckedFeaturesPath); + + REQUIRE(checkedFeatures[0] == 0); + REQUIRE(checkedFeatures[1] == 2); + REQUIRE(checkedFeatures[2] == 2); + REQUIRE(checkedFeatures[3] == 2); + REQUIRE(checkedFeatures[4] == 2); + REQUIRE(checkedFeatures[5] == 2); +} + +// ============================================================================= +// Preflight Error Tests +// ============================================================================= + +TEST_CASE("SimplnxReview::ComputeGroupingDensityFilter: Preflight Error - Feature tuple count mismatch", "[SimplnxReview][ComputeGroupingDensityFilter]") +{ + UnitTest::LoadPlugins(); + + // Build a DataStructure where ParentIds has a different tuple count than Volumes + DataStructure dataStructure; + auto* imageGeom = ImageGeom::Create(dataStructure, k_ImageGeomName); + imageGeom->setDimensions({1, 1, 1}); + + auto* featureAM = AttributeMatrix::Create(dataStructure, k_FeatureAMName, {k_NumFeatures}, imageGeom->getId()); + auto* parentAM = AttributeMatrix::Create(dataStructure, k_ParentAMName, {k_NumParents}, imageGeom->getId()); + + // Volumes with 6 tuples + UnitTest::CreateTestDataArray(dataStructure, k_VolumesName, {k_NumFeatures}, {1}, featureAM->getId()); + // Contiguous NL with 6 tuples + NeighborList::Create(dataStructure, k_ContiguousNLName, ShapeType{k_NumFeatures}, featureAM->getId()); + // Parent Volumes + UnitTest::CreateTestDataArray(dataStructure, k_ParentVolumesName, {k_NumParents}, {1}, parentAM->getId()); + + // ParentIds in a DIFFERENT AM with a different tuple count (mismatch!) + auto* mismatchAM = AttributeMatrix::Create(dataStructure, "MismatchAM", {10}, imageGeom->getId()); + UnitTest::CreateTestDataArray(dataStructure, k_ParentIdsName, {10}, {1}, mismatchAM->getId()); + + DataPath mismatchParentIdsPath = DataPath({k_ImageGeomName, "MismatchAM", k_ParentIdsName}); + + ComputeGroupingDensityFilter filter; + Arguments args = createFilterArgs(false, false); + args.insertOrAssign(ComputeGroupingDensityFilter::k_ParentIdsPath_Key, std::make_any(mismatchParentIdsPath)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); +} + +TEST_CASE("SimplnxReview::ComputeGroupingDensityFilter: Preflight Error - Volumes not in AttributeMatrix", "[SimplnxReview][ComputeGroupingDensityFilter]") +{ + UnitTest::LoadPlugins(); + + // Build a DataStructure where Volumes is NOT inside an AttributeMatrix + DataStructure dataStructure; + auto* imageGeom = ImageGeom::Create(dataStructure, k_ImageGeomName); + imageGeom->setDimensions({1, 1, 1}); + + // Create volumes directly under the ImageGeom (not in an AM) + UnitTest::CreateTestDataArray(dataStructure, k_VolumesName, {k_NumFeatures}, {1}, imageGeom->getId()); + UnitTest::CreateTestDataArray(dataStructure, k_ParentIdsName, {k_NumFeatures}, {1}, imageGeom->getId()); + NeighborList::Create(dataStructure, k_ContiguousNLName, ShapeType{k_NumFeatures}, imageGeom->getId()); + + auto* parentAM = AttributeMatrix::Create(dataStructure, k_ParentAMName, {k_NumParents}, imageGeom->getId()); + UnitTest::CreateTestDataArray(dataStructure, k_ParentVolumesName, {k_NumParents}, {1}, parentAM->getId()); + + DataPath volumesNoAMPath = DataPath({k_ImageGeomName, k_VolumesName}); + DataPath parentIdsNoAMPath = DataPath({k_ImageGeomName, k_ParentIdsName}); + DataPath contiguousNLNoAMPath = DataPath({k_ImageGeomName, k_ContiguousNLName}); + + ComputeGroupingDensityFilter filter; + Arguments args = createFilterArgs(false, false); + args.insertOrAssign(ComputeGroupingDensityFilter::k_FeatureVolumesArrayPath_Key, std::make_any(volumesNoAMPath)); + args.insertOrAssign(ComputeGroupingDensityFilter::k_ParentIdsPath_Key, std::make_any(parentIdsNoAMPath)); + args.insertOrAssign(ComputeGroupingDensityFilter::k_ContiguousNeighborListArrayPath_Key, std::make_any(contiguousNLNoAMPath)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); +} + +TEST_CASE("SimplnxReview::ComputeGroupingDensityFilter: Preflight Error - Parent Volumes not in AttributeMatrix", "[SimplnxReview][ComputeGroupingDensityFilter]") +{ + UnitTest::LoadPlugins(); + + DataStructure dataStructure; + auto* imageGeom = ImageGeom::Create(dataStructure, k_ImageGeomName); + imageGeom->setDimensions({1, 1, 1}); + + auto* featureAM = AttributeMatrix::Create(dataStructure, k_FeatureAMName, {k_NumFeatures}, imageGeom->getId()); + UnitTest::CreateTestDataArray(dataStructure, k_VolumesName, {k_NumFeatures}, {1}, featureAM->getId()); + UnitTest::CreateTestDataArray(dataStructure, k_ParentIdsName, {k_NumFeatures}, {1}, featureAM->getId()); + NeighborList::Create(dataStructure, k_ContiguousNLName, ShapeType{k_NumFeatures}, featureAM->getId()); + + // Parent Volumes directly under ImageGeom (not in AM) + UnitTest::CreateTestDataArray(dataStructure, k_ParentVolumesName, {k_NumParents}, {1}, imageGeom->getId()); + + DataPath parentVolumesNoAMPath = DataPath({k_ImageGeomName, k_ParentVolumesName}); + + ComputeGroupingDensityFilter filter; + Arguments args = createFilterArgs(false, false); + args.insertOrAssign(ComputeGroupingDensityFilter::k_ParentVolumesPath_Key, std::make_any(parentVolumesNoAMPath)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); +}