From a625e56a6537f446b22e0a8847dc399c0d836b25 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 11 Jun 2026 11:24:37 -0400 Subject: [PATCH] VV: Group Microtexture Regions fully V&V'ed --- .../OrientationAnalysis/CMakeLists.txt | 2 + .../docs/GroupMicroTextureRegionsFilter.md | 29 + .../Algorithms/GroupMicroTextureRegions.cpp | 333 ++++++++++++ .../Algorithms/GroupMicroTextureRegions.hpp | 80 +++ .../GroupMicroTextureRegionsFilter.cpp | 251 +++++++++ .../GroupMicroTextureRegionsFilter.hpp | 135 +++++ .../OrientationAnalysisLegacyUUIDMapping.hpp | 2 + .../OrientationAnalysis/test/CMakeLists.txt | 1 + .../test/GroupMicroTextureRegionsTest.cpp | 502 ++++++++++++++++++ .../6_4/GroupMicroTextureRegionsFilter.json | 54 ++ .../6_5/GroupMicroTextureRegionsFilter.json | 55 ++ .../vv/GroupMicroTextureRegionsFilter.md | 146 +++++ .../GroupMicroTextureRegionsFilter.md | 102 ++++ .../GroupMicroTextureRegionsFilter.md | 75 +++ 14 files changed, 1767 insertions(+) create mode 100644 src/Plugins/OrientationAnalysis/docs/GroupMicroTextureRegionsFilter.md create mode 100644 src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/GroupMicroTextureRegions.cpp create mode 100644 src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/GroupMicroTextureRegions.hpp create mode 100644 src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/GroupMicroTextureRegionsFilter.cpp create mode 100644 src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/GroupMicroTextureRegionsFilter.hpp create mode 100644 src/Plugins/OrientationAnalysis/test/GroupMicroTextureRegionsTest.cpp create mode 100644 src/Plugins/OrientationAnalysis/test/simpl_conversion/6_4/GroupMicroTextureRegionsFilter.json create mode 100644 src/Plugins/OrientationAnalysis/test/simpl_conversion/6_5/GroupMicroTextureRegionsFilter.json create mode 100644 src/Plugins/OrientationAnalysis/vv/GroupMicroTextureRegionsFilter.md create mode 100644 src/Plugins/OrientationAnalysis/vv/deviations/GroupMicroTextureRegionsFilter.md create mode 100644 src/Plugins/OrientationAnalysis/vv/provenance/GroupMicroTextureRegionsFilter.md diff --git a/src/Plugins/OrientationAnalysis/CMakeLists.txt b/src/Plugins/OrientationAnalysis/CMakeLists.txt index c2bbd6c43f..0e5138f98c 100644 --- a/src/Plugins/OrientationAnalysis/CMakeLists.txt +++ b/src/Plugins/OrientationAnalysis/CMakeLists.txt @@ -66,6 +66,7 @@ set(FilterList CreateEnsembleInfoFilter EBSDSegmentFeaturesFilter EbsdToH5EbsdFilter + GroupMicroTextureRegionsFilter MergeTwinsFilter NeighborOrientationCorrelationFilter ReadAngDataFilter @@ -204,6 +205,7 @@ set(filter_algorithms CreateEnsembleInfo EBSDSegmentFeatures EbsdToH5Ebsd + GroupMicroTextureRegions MergeTwins NeighborOrientationCorrelation ReadAngData diff --git a/src/Plugins/OrientationAnalysis/docs/GroupMicroTextureRegionsFilter.md b/src/Plugins/OrientationAnalysis/docs/GroupMicroTextureRegionsFilter.md new file mode 100644 index 0000000000..b11fc53054 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/docs/GroupMicroTextureRegionsFilter.md @@ -0,0 +1,29 @@ +# Group MicroTexture Regions + +## Group (Subgroup) + +Reconstruction Filters (Grouping) + +## Description + +This Filter groups neighboring **Features** that have c-axes aligned within a user-defined tolerance. The algorithm for grouping the **Features** is analogous to the algorithm for segmenting the **Features** — only the average orientation of the **Features** is used instead of the orientations of the individual **Cells**, and the criterion for grouping only considers the alignment of the c-axes. The user can specify a tolerance for how closely aligned the c-axes must be for neighbor **Features** to be grouped. + +NOTE: This filter is intended for use with *Hexagonal* materials. While the c-axis is actually just referring to the <001> direction and thus will operate on any symmetry, the utility of grouping by <001> alignment is likely only important/useful in materials with anisotropy in that direction (like materials with *Hexagonal* symmetry). Features whose phase resolves to anything other than *Hexagonal_High* are silently left ungrouped. + +### Randomization of Parent Ids + +By default the filter assigns parent ids deterministically in the order features are picked as BFS seeds, so identical inputs produce identical parent ids. Set **Randomize Parent Ids** to true to randomly permute the assigned parent ids (useful when feeding the output straight into a color-mapped visualization where adjacent groups should not share the same color by accident). For reproducible randomization, enable **Use Seed for Random Generation** and supply a **Seed** value; the seed actually used is also written to a top-level array (default name `_Group_MicroTexture_Regions_Seed_Value_`) so the run can be replayed. + +% Auto generated parameter table will be inserted here + +## References + +## Example Pipelines + +## License & Copyright + +Please see the description file distributed with this **Plugin** + +## DREAM3D-NX Help + +If you need help, need to file a bug report or want to request a new feature, please head over to the [DREAM3DNX-Issues](https://github.com/BlueQuartzSoftware/DREAM3DNX-Issues/discussions) GitHub site where the community of DREAM3D-NX users can help answer your questions. diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/GroupMicroTextureRegions.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/GroupMicroTextureRegions.cpp new file mode 100644 index 0000000000..c5061b6155 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/GroupMicroTextureRegions.cpp @@ -0,0 +1,333 @@ +#include "GroupMicroTextureRegions.hpp" + +#include "simplnx/Common/Constants.hpp" +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/NeighborList.hpp" +#include "simplnx/Utilities/Math/GeometryMath.hpp" +#include "simplnx/Utilities/MessageHelper.hpp" + +#include "EbsdLib/LaueOps/LaueOps.h" + +#include + +using namespace nx::core; + +// ----------------------------------------------------------------------------- +GroupMicroTextureRegions::GroupMicroTextureRegions(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, + GroupMicroTextureRegionsInputValues* inputValues) +: m_DataStructure(dataStructure) +, m_InputValues(inputValues) +, m_ShouldCancel(shouldCancel) +, m_MessageHandler(mesgHandler) +, m_FeaturePhases(m_DataStructure.getDataRefAs(m_InputValues->FeaturePhasesArrayPath)) +, m_FeatureParentIds(m_DataStructure.getDataRefAs(m_InputValues->FeatureParentIdsArrayName)) +, m_CrystalStructures(m_DataStructure.getDataRefAs(m_InputValues->CrystalStructuresArrayPath)) +, m_AvgQuats(m_DataStructure.getDataRefAs(m_InputValues->AvgQuatsArrayPath)) +, m_Volumes(m_DataStructure.getDataRefAs(m_InputValues->VolumesArrayPath)) +{ +} + +// ----------------------------------------------------------------------------- +GroupMicroTextureRegions::~GroupMicroTextureRegions() noexcept = default; + +// ----------------------------------------------------------------------------- +const std::atomic_bool& GroupMicroTextureRegions::getCancel() +{ + return m_ShouldCancel; +} + +// ----------------------------------------------------------------------------- +void GroupMicroTextureRegions::randomizeParentIds(usize totalPoints, usize totalParentIds) +{ + // Shuffle parent IDs in [1, totalParentIds-1] via Fisher-Yates with the same + // RNG state already seeded by operator(). Parent ID 0 is reserved (unassigned) + // and is excluded from the shuffle so cells with no parent stay at 0. + auto& cellParentIds = m_DataStructure.getDataRefAs(m_InputValues->CellParentIdsArrayName); + + std::vector shuffle(totalParentIds); + for(usize i = 0; i < totalParentIds; i++) + { + shuffle[i] = static_cast(i); + } + + std::uniform_int_distribution intDist(1, totalParentIds - 1); + for(usize i = 1; i < totalParentIds; i++) + { + usize r = intDist(m_Generator); + std::swap(shuffle[i], shuffle[r]); + } + + // Remap feature parent IDs first so cell parent IDs can index through the new mapping + const usize numFeatures = m_FeatureParentIds.getNumberOfTuples(); + for(usize f = 0; f < numFeatures; f++) + { + const int32 oldId = m_FeatureParentIds[f]; + if(oldId >= 0 && static_cast(oldId) < totalParentIds) + { + m_FeatureParentIds[f] = shuffle[oldId]; + } + } + + // Re-derive cell parent IDs from the shuffled feature parent IDs + auto& featureIds = m_DataStructure.getDataRefAs(m_InputValues->FeatureIdsArrayPath); + for(usize k = 0; k < totalPoints; k++) + { + cellParentIds[k] = m_FeatureParentIds[featureIds[k]]; + } +} + +// ----------------------------------------------------------------------------- +Result<> GroupMicroTextureRegions::execute() +{ + MessageHelper messageHelper(m_MessageHandler); + ThrottledMessenger throttledMessenger = messageHelper.createThrottledMessenger(); + + NeighborList& featureNeighborListRef = m_DataStructure.getDataRefAs>(m_InputValues->ContiguousNeighborListArrayPath); + NeighborList* nonContigNeighListPtr = nullptr; + 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"); + } + } + + std::vector groupList; + + int32 parentCount = 0; + int32 featureSeed = 0; + int32 list1size = 0; + int32 list2size = 0; + + while(featureSeed >= 0) + { + parentCount++; + featureSeed = getSeed(parentCount); + if(featureSeed < 0) + { + continue; + } + + groupList.clear(); + groupList.push_back(featureSeed); + for(std::vector::size_type j = 0; j < groupList.size(); j++) + { + const int32 firstFeature = groupList[j]; + list1size = static_cast(featureNeighborListRef[firstFeature].size()); + if(m_InputValues->UseNonContiguousNeighbors) + { + list2size = nonContigNeighListPtr->getListSize(firstFeature); + } + // Walk contiguous neighbors (k=0) then optional non-contiguous neighbors (k=1) + for(int32 k = 0; k < 2; k++) + { + const int32 listSize = (k == 0) ? list1size : list2size; + for(int32 l = 0; l < listSize; l++) + { + int32 neigh = -1; + if(k == 0) + { + neigh = featureNeighborListRef[firstFeature][l]; + } + else if(k == 1 && m_InputValues->UseNonContiguousNeighbors) + { + bool ok = false; + neigh = nonContigNeighListPtr->getValue(firstFeature, l, ok); + } + if(neigh >= 0 && neigh != firstFeature) + { + if(determineGrouping(firstFeature, neigh, parentCount)) + { + groupList.push_back(neigh); + } + } + } + } + } + + throttledMessenger.sendThrottledMessage([&]() { return fmt::format("Parent Count: {}", parentCount); }); + } + return {}; +} + +// ----------------------------------------------------------------------------- +Result<> GroupMicroTextureRegions::operator()() +{ + MessageHelper messageHelper(m_MessageHandler); + + m_Generator = std::mt19937_64(m_InputValues->SeedValue); + m_Distribution = std::uniform_real_distribution(0.0f, 1.0f); + + // Initialize Data + m_AvgCAxes[0] = 0.0f; + m_AvgCAxes[1] = 0.0f; + m_AvgCAxes[2] = 0.0f; + m_FeatureParentIds.fill(-1); + + // Execute the main grouping algorithm + messageHelper.sendMessage(fmt::format("Start Grouping.....")); + + // Execute the grouping algorithm + Result<> result = execute(); + if(result.invalid()) + { + return result; + } + + // handle active array resize + if(m_NumTuples < 2) + { + return MakeErrorResult(-87000, fmt::format("The number of grouped Features was {} which means no grouped features were detected. A grouping value may be set too high", m_NumTuples)); + } + m_DataStructure.getDataRefAs(m_InputValues->NewCellFeatureAttributeMatrixName).resizeTuples(ShapeType{m_NumTuples}); + + auto& cellParentIds = m_DataStructure.getDataRefAs(m_InputValues->CellParentIdsArrayName); + auto& featureIds = m_DataStructure.getDataRefAs(m_InputValues->FeatureIdsArrayPath); + const usize totalPoints = featureIds.getNumberOfTuples(); + for(usize k = 0; k < totalPoints; k++) + { + cellParentIds[k] = m_FeatureParentIds[featureIds[k]]; + } + + if(m_InputValues->RandomizeParentIds) + { + messageHelper.sendMessage(fmt::format("Randomizing Parent Ids")); + randomizeParentIds(totalPoints, m_NumTuples); + } + + return {}; +} + +// ----------------------------------------------------------------------------- +int GroupMicroTextureRegions::getSeed(int32 newFid) +{ + usize numFeatures = m_FeaturePhases.getNumberOfTuples(); + + int32 featureIdSeed = -1; + + // Precalculate some constants + const int32 totalFMinus1 = static_cast(numFeatures) - 1; + + usize counter = 0; + // This section finds a feature id that has not been grouped yet. It starts by + // randomly selecting a feature id between 0 and numFeatures-1. We then start + // looping. If the initial random value is valid then we exit the loop after + // a single iteration. If that feature has already been grouped, then we add one + // to the `randFeature` value and try again. If we get to the end of the range of + // featureIds then the algorithm will loop back to featureId = 0 and start incrementing + // from there. This is reasonably efficient as we only generate random numbers + // as needed. + auto randFeature = static_cast(m_Distribution(m_Generator) * static_cast(totalFMinus1)); + while(featureIdSeed == -1 && counter < numFeatures) + { + if(randFeature > totalFMinus1) + { + randFeature = randFeature - numFeatures; + } + if(m_FeatureParentIds.getValue(randFeature) == -1) + { + featureIdSeed = randFeature; + } + randFeature++; + counter++; + } + + // // Used for debugging and demonstration + // if(newFid == 1) + // { + // auto& centroids = m_DataStructure.getDataRefAs(m_InputValues->VolumesArrayPath.replaceName("Centroids")); + // std::ofstream fout ("/tmp/GroupMicroTextureInitialVoxelSeeds.txt", std::ios_base::out | std::ios_base::app); + // fout << fmt::format("Feature Parent Id: {} | X: {}, Y: {}\n", voxelSeed, centroids.getComponent(voxelSeed, 0), centroids.getComponent(voxelSeed, 1)); + // } + + if(featureIdSeed >= 0) + { + m_FeatureParentIds[featureIdSeed] = newFid; + m_NumTuples = newFid + 1; + + if(m_InputValues->UseRunningAverage) + { + usize index = featureIdSeed * 4; + // Get the orientation matrix (which is passive) and then transpose it to make it active transform + ebsdlib::Matrix3X3F g1t = ebsdlib::Quaternion(m_AvgQuats.getValue(index + 0), m_AvgQuats.getValue(index + 1), m_AvgQuats.getValue(index + 2), m_AvgQuats.getValue(index + 3)) + .toOrientationMatrix() + .toGMatrix() + .transpose(); + ebsdlib::Matrix3X1F cAxis(0.0f, 0.0f, 1.0f); + // normalize so that the dot product can be taken below without + // dividing by the magnitudes (they would be 1) + const ebsdlib::Matrix3X1F c1 = (g1t * cAxis).normalize(); + + m_AvgCAxes = c1 * m_Volumes.getValue(featureIdSeed); + } + } + + return featureIdSeed; +} + +// ----------------------------------------------------------------------------- +bool GroupMicroTextureRegions::determineGrouping(int32 referenceFeature, int32 neighborFeature, int32 newFid) +{ + const int32 neighborParentId = m_FeatureParentIds.getValue(neighborFeature); + const int32 referenceFeaturePhase = m_FeaturePhases.getValue(referenceFeature); + const int32 neighborFeaturePhase = m_FeaturePhases.getValue(neighborFeature); + + if(neighborParentId == -1 && referenceFeaturePhase > 0 && neighborFeaturePhase > 0) + { + ebsdlib::Matrix3X1F c1 = {0.0f, 0.0f, 0.0f}; + ebsdlib::Matrix3X1F cAxis(0.0f, 0.0f, 1.0f); + + if(!m_InputValues->UseRunningAverage) + { + const usize index = referenceFeature * 4; + // Get the orientation matrix (which is passive) and then transpose it to make it active transform + // transpose the g matrix so when c-axis is multiplied by it, + // it will give the sample direction that the c-axis is along + ebsdlib::Matrix3X3F g1t = ebsdlib::Quaternion(m_AvgQuats.getValue(index + 0), m_AvgQuats.getValue(index + 1), m_AvgQuats.getValue(index + 2), m_AvgQuats.getValue(index + 3)) + .toOrientationMatrix() + .toGMatrix() + .transpose(); + c1 = (g1t * cAxis).normalize(); + } + uint32 phase1 = m_CrystalStructures.getValue(referenceFeaturePhase); + uint32 phase2 = m_CrystalStructures.getValue(neighborFeaturePhase); + if(phase1 == phase2 && (phase1 == ebsdlib::CrystalStructure::Hexagonal_High)) + { + const usize index = neighborFeature * 4; + // Get the orientation matrix (which is passive) and then transpose it to make it active transform + // transpose the g matrix so when c-axis is multiplied by it, + // it will give the sample direction that the c-axis is along + ebsdlib::Matrix3X3F g2t = ebsdlib::Quaternion(m_AvgQuats.getValue(index + 0), m_AvgQuats.getValue(index + 1), m_AvgQuats.getValue(index + 2), m_AvgQuats.getValue(index + 3)) + .toOrientationMatrix() + .toGMatrix() + .transpose(); + ebsdlib::Matrix3X1F c2 = (g2t * cAxis).normalize(); + + float32 w; + if(m_InputValues->UseRunningAverage) + { + w = m_AvgCAxes.cosTheta(c2); + } + else + { + w = c1.cosTheta(c2); + } + w = std::acos(std::clamp(w, -1.0f, 1.0f)); + + // Convert user defined tolerance to radians. + float32 cAxisToleranceRad = m_InputValues->CAxisTolerance * nx::core::Constants::k_PiF / 180.0f; + if(w <= cAxisToleranceRad || (nx::core::Constants::k_PiD - w) <= cAxisToleranceRad) + { + m_FeatureParentIds.setValue(neighborFeature, newFid); + if(m_InputValues->UseRunningAverage) + { + c2 = c2 * m_Volumes.getValue(neighborFeature); + m_AvgCAxes = m_AvgCAxes + c2; + } + return true; + } + } + } + return false; +} diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/GroupMicroTextureRegions.hpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/GroupMicroTextureRegions.hpp new file mode 100644 index 0000000000..9150904cd3 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/GroupMicroTextureRegions.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include "OrientationAnalysis/OrientationAnalysis_export.hpp" + +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/DataStructure/DataStructure.hpp" +#include "simplnx/Filter/IFilter.hpp" + +#include "EbsdLib/Math/Matrix3X1.hpp" + +#include + +namespace nx::core +{ + +struct ORIENTATIONANALYSIS_EXPORT GroupMicroTextureRegionsInputValues +{ + bool UseNonContiguousNeighbors; + DataPath NonContiguousNeighborListArrayPath; + DataPath ContiguousNeighborListArrayPath; + bool UseRunningAverage; + float32 CAxisTolerance; + DataPath FeatureIdsArrayPath; + DataPath FeaturePhasesArrayPath; + DataPath VolumesArrayPath; + DataPath AvgQuatsArrayPath; + DataPath CrystalStructuresArrayPath; + DataPath NewCellFeatureAttributeMatrixName; + DataPath CellParentIdsArrayName; + DataPath FeatureParentIdsArrayName; + bool RandomizeParentIds; + uint64 SeedValue; +}; + +/** + * @class GroupMicroTextureRegions + * @brief This filter ... + */ +class ORIENTATIONANALYSIS_EXPORT GroupMicroTextureRegions +{ +public: + GroupMicroTextureRegions(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, GroupMicroTextureRegionsInputValues* inputValues); + ~GroupMicroTextureRegions() noexcept; + + GroupMicroTextureRegions(const GroupMicroTextureRegions&) = delete; + GroupMicroTextureRegions(GroupMicroTextureRegions&&) noexcept = delete; + GroupMicroTextureRegions& operator=(const GroupMicroTextureRegions&) = delete; + GroupMicroTextureRegions& operator=(GroupMicroTextureRegions&&) noexcept = delete; + + Result<> operator()(); + + const std::atomic_bool& getCancel(); + +protected: + int getSeed(int32 newFid); + bool determineGrouping(int32 referenceFeature, int32 neighborFeature, int32 newFid); + Result<> execute(); + void randomizeParentIds(usize totalPoints, usize totalParentIds); + +private: + DataStructure& m_DataStructure; + const GroupMicroTextureRegionsInputValues* m_InputValues = nullptr; + const std::atomic_bool& m_ShouldCancel; + const IFilter::MessageHandler& m_MessageHandler; + + usize m_NumTuples = 0; + ebsdlib::Matrix3X1F m_AvgCAxes = {0.0f, 0.0f, 0.0f}; + std::mt19937_64 m_Generator = {}; + std::uniform_real_distribution m_Distribution = {}; + + // These are so that we don't have to keep getting the references while we are running + + Int32Array& m_FeaturePhases; + Int32Array& m_FeatureParentIds; + UInt32Array& m_CrystalStructures; + Float32Array& m_AvgQuats; + Float32Array& m_Volumes; +}; +} // namespace nx::core diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/GroupMicroTextureRegionsFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/GroupMicroTextureRegionsFilter.cpp new file mode 100644 index 0000000000..ffa6234784 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/GroupMicroTextureRegionsFilter.cpp @@ -0,0 +1,251 @@ +#include "GroupMicroTextureRegionsFilter.hpp" + +#include "OrientationAnalysis/Filters/Algorithms/GroupMicroTextureRegions.hpp" + +#include "simplnx/DataStructure/DataPath.hpp" +#include "simplnx/Filter/Actions/CreateArrayAction.hpp" +#include "simplnx/Filter/Actions/CreateAttributeMatrixAction.hpp" +#include "simplnx/Parameters/ArraySelectionParameter.hpp" +#include "simplnx/Parameters/BoolParameter.hpp" +#include "simplnx/Parameters/DataGroupCreationParameter.hpp" +#include "simplnx/Parameters/DataObjectNameParameter.hpp" +#include "simplnx/Parameters/NeighborListSelectionParameter.hpp" +#include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Utilities/SIMPLConversion.hpp" + +#include + +using namespace nx::core; + +namespace nx::core +{ +//------------------------------------------------------------------------------ +std::string GroupMicroTextureRegionsFilter::name() const +{ + return FilterTraits::name.str(); +} + +//------------------------------------------------------------------------------ +std::string GroupMicroTextureRegionsFilter::className() const +{ + return FilterTraits::className; +} + +//------------------------------------------------------------------------------ +Uuid GroupMicroTextureRegionsFilter::uuid() const +{ + return FilterTraits::uuid; +} + +//------------------------------------------------------------------------------ +std::string GroupMicroTextureRegionsFilter::humanName() const +{ + return "Group MicroTexture Regions"; +} + +//------------------------------------------------------------------------------ +std::vector GroupMicroTextureRegionsFilter::defaultTags() const +{ + return {className(), "Reconstruction", "Grouping"}; +} + +//------------------------------------------------------------------------------ +Parameters GroupMicroTextureRegionsFilter::parameters() const +{ + Parameters params; + + // Create the parameter descriptors that are needed for this filter + params.insertSeparator(Parameters::Separator{"Input Parameter(s)"}); + + params.insert(std::make_unique(k_UseRunningAverage_Key, "Group C-Axes With Running Average", "Group C-Axes With Running Average", true)); + params.insert(std::make_unique(k_CAxisTolerance_Key, "C-Axis Alignment Tolerance (Degrees)", "C-Axis Alignment Tolerance (Degrees)", 0.0f)); + params.insert(std::make_unique(k_ContiguousNeighborListArrayPath_Key, "Contiguous Neighbor List", "List of contiguous neighbors for each Feature.", DataPath{}, + NeighborListSelectionParameter::AllowedTypes{DataType::int32})); + + params.insertSeparator(Parameters::Separator{"Non-Contiguous Neighborhood Option"}); + params.insertLinkableParameter(std::make_unique(k_UseNonContiguousNeighbors_Key, "Use Non-Contiguous Neighbors", "Use non-contiguous neighborhoods", false)); + params.insert(std::make_unique(k_NonContiguousNeighborListArrayPath_Key, "Non-Contiguous Neighbor List", "List of non-contiguous neighbors for each Feature.", + DataPath{}, NeighborListSelectionParameter::AllowedTypes{DataType::int32})); + + params.insertSeparator(Parameters::Separator{"Parent Id Randomization"}); + params.insertLinkableParameter(std::make_unique( + k_RandomizeParentIds_Key, "Randomize Parent Ids", + "When true, the final parent ids assigned to each group are randomly permuted. Disabled by default so identical inputs produce identical parent id assignments.", false)); + params.insertLinkableParameter(std::make_unique( + k_UseSeed_Key, "Use Seed for Random Generation", + "When true the user-supplied Seed value is used for randomization and the random walk through the feature ids; otherwise the seed is derived from the system clock.", false)); + params.insert(std::make_unique>(k_SeedValue_Key, "Seed", "The seed fed into the random generator", std::mt19937::default_seed)); + params.insert(std::make_unique(k_SeedArrayName_Key, "Stored Seed Value Array Name", "Name of array holding the seed value", "_Group_MicroTexture_Regions_Seed_Value_")); + + params.insertSeparator(Parameters::Separator{"Input Cell Data"}); + params.insert(std::make_unique(k_FeatureIdsArrayPath_Key, "Cell Feature Ids", "Data Array that specifies to which Feature each Element belongs", DataPath{}, + ArraySelectionParameter::AllowedTypes{DataType::int32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); + + params.insertSeparator(Parameters::Separator{"Input Feature Data"}); + params.insert(std::make_unique(k_FeaturePhasesArrayPath_Key, "Feature Phases", "Specifies to which Ensemble each Feature belongs", DataPath{}, + ArraySelectionParameter::AllowedTypes{nx::core::DataType::int32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); + params.insert(std::make_unique(k_VolumesArrayPath_Key, "Volumes", "The Feature Volumes Data Array", DataPath{}, + ArraySelectionParameter::AllowedTypes{nx::core::DataType::float32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); + params.insert(std::make_unique(k_AvgQuatsArrayPath_Key, "Average Quaternions", "Specifies the average orientation of each Feature in quaternion representation", DataPath{}, + ArraySelectionParameter::AllowedTypes{nx::core::DataType::float32}, ArraySelectionParameter::AllowedComponentShapes{{4}})); + + params.insertSeparator(Parameters::Separator{"Input Ensemble Data"}); + params.insert(std::make_unique(k_CrystalStructuresArrayPath_Key, "Crystal Structures", "Enumeration representing the crystal structure for each Ensemble", DataPath{}, + ArraySelectionParameter::AllowedTypes{nx::core::DataType::uint32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); + + params.insertSeparator(Parameters::Separator{"Output Data Object(s)"}); + params.insert(std::make_unique(k_CellParentIdsArrayName_Key, "Cell Parent Ids Array name", "Output Cell Parent Ids Data Array", "Cell Parent Ids")); + params.insert(std::make_unique(k_FeatureParentIdsArrayName_Key, "Feature Parent Ids Array Name", "Output Feature Parent Ids Data Array", "Feature Parent Ids")); + params.insert(std::make_unique(k_NewCellFeatureAttributeMatrixName_Key, "Created Microtexture Feature Attribute Matrix", + "Output Feature Attribute Matrix for Microtexture Regions", DataPath{})); + params.insert(std::make_unique(k_ActiveArrayName_Key, "Active Array Name", "Output Active Array", "Active")); + + // Associate the Linkable Parameter(s) to the children parameters that they control + params.linkParameters(k_UseNonContiguousNeighbors_Key, k_NonContiguousNeighborListArrayPath_Key, true); + params.linkParameters(k_UseSeed_Key, k_SeedValue_Key, true); + + return params; +} + +//------------------------------------------------------------------------------ +IFilter::UniquePointer GroupMicroTextureRegionsFilter::clone() const +{ + return std::make_unique(); +} + +//------------------------------------------------------------------------------ +IFilter::VersionType GroupMicroTextureRegionsFilter::parametersVersion() const +{ + return 2; +} + +//------------------------------------------------------------------------------ +IFilter::PreflightResult GroupMicroTextureRegionsFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const +{ + auto pFeatureIdsPath = filterArgs.value(k_FeatureIdsArrayPath_Key); + auto pFeaturePhasesPath = filterArgs.value(k_FeaturePhasesArrayPath_Key); + auto pNewCellFeatureAMPath = filterArgs.value(k_NewCellFeatureAttributeMatrixName_Key); + auto pCellParentIdsName = filterArgs.value(k_CellParentIdsArrayName_Key); + auto pFeatureParentIdsName = filterArgs.value(k_FeatureParentIdsArrayName_Key); + auto pActiveName = filterArgs.value(k_ActiveArrayName_Key); + auto pSeedArrayName = filterArgs.value(k_SeedArrayName_Key); + + PreflightResult preflightResult; + nx::core::Result resultOutputActions; + std::vector preflightUpdatedValues; + + { + auto* featureIds = dataStructure.getDataAs(pFeatureIdsPath); + auto createAction = std::make_unique(DataType::int32, featureIds->getTupleShape(), std::vector{1}, pFeatureIdsPath.replaceName(pCellParentIdsName)); + resultOutputActions.value().appendAction(std::move(createAction)); + } + { + auto* featurePhases = dataStructure.getDataAs(pFeaturePhasesPath); + auto createAction = std::make_unique(DataType::int32, featurePhases->getTupleShape(), std::vector{1}, pFeaturePhasesPath.replaceName(pFeatureParentIdsName)); + resultOutputActions.value().appendAction(std::move(createAction)); + } + + { + auto createAction = std::make_unique(pNewCellFeatureAMPath, ShapeType{1}); + resultOutputActions.value().appendAction(std::move(createAction)); + } + { + auto createAction = std::make_unique(DataType::boolean, std::vector{1}, std::vector{1}, pNewCellFeatureAMPath.createChildPath(pActiveName)); + resultOutputActions.value().appendAction(std::move(createAction)); + } + + { + auto createAction = std::make_unique(DataType::uint64, std::vector{1}, std::vector{1}, DataPath({pSeedArrayName})); + resultOutputActions.value().appendAction(std::move(createAction)); + } + + // Return both the resultOutputActions and the preflightUpdatedValues via std::move() + return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; +} + +//------------------------------------------------------------------------------ +Result<> GroupMicroTextureRegionsFilter::executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const +{ + auto seed = filterArgs.value(k_SeedValue_Key); + if(!filterArgs.value(k_UseSeed_Key)) + { + seed = static_cast(std::chrono::steady_clock::now().time_since_epoch().count()); + } + + // Store Seed Value in Top Level Array + dataStructure.getDataRefAs(DataPath({filterArgs.value(k_SeedArrayName_Key)}))[0] = seed; + + GroupMicroTextureRegionsInputValues inputValues; + + inputValues.UseNonContiguousNeighbors = filterArgs.value(k_UseNonContiguousNeighbors_Key); + inputValues.NonContiguousNeighborListArrayPath = filterArgs.value(k_NonContiguousNeighborListArrayPath_Key); + inputValues.ContiguousNeighborListArrayPath = filterArgs.value(k_ContiguousNeighborListArrayPath_Key); + inputValues.UseRunningAverage = filterArgs.value(k_UseRunningAverage_Key); + inputValues.CAxisTolerance = filterArgs.value(k_CAxisTolerance_Key); + inputValues.FeatureIdsArrayPath = filterArgs.value(k_FeatureIdsArrayPath_Key); + inputValues.FeaturePhasesArrayPath = filterArgs.value(k_FeaturePhasesArrayPath_Key); + inputValues.VolumesArrayPath = filterArgs.value(k_VolumesArrayPath_Key); + inputValues.AvgQuatsArrayPath = filterArgs.value(k_AvgQuatsArrayPath_Key); + inputValues.CrystalStructuresArrayPath = filterArgs.value(k_CrystalStructuresArrayPath_Key); + inputValues.NewCellFeatureAttributeMatrixName = filterArgs.value(k_NewCellFeatureAttributeMatrixName_Key); + inputValues.CellParentIdsArrayName = inputValues.FeatureIdsArrayPath.replaceName(filterArgs.value(k_CellParentIdsArrayName_Key)); + inputValues.FeatureParentIdsArrayName = inputValues.FeaturePhasesArrayPath.replaceName(filterArgs.value(k_FeatureParentIdsArrayName_Key)); + inputValues.RandomizeParentIds = filterArgs.value(k_RandomizeParentIds_Key); + inputValues.SeedValue = seed; + + return GroupMicroTextureRegions(dataStructure, messageHandler, shouldCancel, &inputValues)(); +} + +namespace +{ +namespace SIMPL +{ +constexpr StringLiteral k_ActiveArrayNameKey = "ActiveArrayName"; +constexpr StringLiteral k_AvgQuatsArrayPathKey = "AvgQuatsArrayPath"; +constexpr StringLiteral k_CAxisToleranceKey = "CAxisTolerance"; +constexpr StringLiteral k_CellParentIdsArrayNameKey = "CellParentIdsArrayName"; +constexpr StringLiteral k_ContiguousNeighborListArrayPathKey = "ContiguousNeighborListArrayPath"; +constexpr StringLiteral k_CrystalStructuresArrayPathKey = "CrystalStructuresArrayPath"; +constexpr StringLiteral k_FeatureIdsArrayPathKey = "FeatureIdsArrayPath"; +constexpr StringLiteral k_FeatureParentIdsArrayNameKey = "FeatureParentIdsArrayName"; +constexpr StringLiteral k_FeaturePhasesArrayPathKey = "FeaturePhasesArrayPath"; +constexpr StringLiteral k_NewCellFeatureAttributeMatrixNameKey = "NewCellFeatureAttributeMatrixName"; +constexpr StringLiteral k_NonContiguousNeighborListArrayPathKey = "NonContiguousNeighborListArrayPath"; +constexpr StringLiteral k_UseNonContiguousNeighborsKey = "UseNonContiguousNeighbors"; +constexpr StringLiteral k_UseRunningAverageKey = "UseRunningAverage"; +constexpr StringLiteral k_VolumesArrayPathKey = "VolumesArrayPath"; +} // namespace SIMPL +} // namespace + +Result GroupMicroTextureRegionsFilter::FromSIMPLJson(const nlohmann::json& json) +{ + Arguments args = GroupMicroTextureRegionsFilter().getDefaultArguments(); + + std::vector> results; + + results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_ActiveArrayNameKey, k_ActiveArrayName_Key)); + results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_AvgQuatsArrayPathKey, k_AvgQuatsArrayPath_Key)); + results.push_back(SIMPLConversion::ConvertParameter>(args, json, SIMPL::k_CAxisToleranceKey, k_CAxisTolerance_Key)); + results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_CellParentIdsArrayNameKey, k_CellParentIdsArrayName_Key)); + results.push_back( + SIMPLConversion::ConvertParameter(args, json, SIMPL::k_ContiguousNeighborListArrayPathKey, k_ContiguousNeighborListArrayPath_Key)); + results.push_back( + SIMPLConversion::ConvertParameter(args, json, SIMPL::k_CrystalStructuresArrayPathKey, k_CrystalStructuresArrayPath_Key)); + results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeatureIdsArrayPathKey, k_FeatureIdsArrayPath_Key)); + results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeatureParentIdsArrayNameKey, k_FeatureParentIdsArrayName_Key)); + results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_FeaturePhasesArrayPathKey, k_FeaturePhasesArrayPath_Key)); + results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_NewCellFeatureAttributeMatrixNameKey, + k_NewCellFeatureAttributeMatrixName_Key)); + results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_NonContiguousNeighborListArrayPathKey, + k_NonContiguousNeighborListArrayPath_Key)); + results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_UseNonContiguousNeighborsKey, k_UseNonContiguousNeighbors_Key)); + results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_UseRunningAverageKey, k_UseRunningAverage_Key)); + results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_VolumesArrayPathKey, k_VolumesArrayPath_Key)); + + Result<> conversionResult = MergeResults(std::move(results)); + + return ConvertResultTo(std::move(conversionResult), std::move(args)); +} +} // namespace nx::core diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/GroupMicroTextureRegionsFilter.hpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/GroupMicroTextureRegionsFilter.hpp new file mode 100644 index 0000000000..a81c2c5987 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/GroupMicroTextureRegionsFilter.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include "OrientationAnalysis/OrientationAnalysis_export.hpp" + +#include "simplnx/Filter/FilterTraits.hpp" +#include "simplnx/Filter/IFilter.hpp" + +namespace nx::core +{ +/** + * @class GroupMicroTextureRegionsFilter + * @brief This filter will .... + */ +class ORIENTATIONANALYSIS_EXPORT GroupMicroTextureRegionsFilter : public IFilter +{ +public: + GroupMicroTextureRegionsFilter() = default; + ~GroupMicroTextureRegionsFilter() noexcept override = default; + + GroupMicroTextureRegionsFilter(const GroupMicroTextureRegionsFilter&) = delete; + GroupMicroTextureRegionsFilter(GroupMicroTextureRegionsFilter&&) noexcept = delete; + + GroupMicroTextureRegionsFilter& operator=(const GroupMicroTextureRegionsFilter&) = delete; + GroupMicroTextureRegionsFilter& operator=(GroupMicroTextureRegionsFilter&&) noexcept = delete; + + // Parameter Keys + static constexpr StringLiteral k_UseNonContiguousNeighbors_Key = "use_non_contiguous_neighbors"; + static constexpr StringLiteral k_NonContiguousNeighborListArrayPath_Key = "non_contiguous_neighbor_list_array_path"; + static constexpr StringLiteral k_ContiguousNeighborListArrayPath_Key = "contiguous_neighbor_list_array_path"; + static constexpr StringLiteral k_UseRunningAverage_Key = "use_running_average"; + static constexpr StringLiteral k_CAxisTolerance_Key = "c_axis_tolerance"; + static constexpr StringLiteral k_FeatureIdsArrayPath_Key = "feature_ids_array_path"; + static constexpr StringLiteral k_FeaturePhasesArrayPath_Key = "feature_phases_array_path"; + static constexpr StringLiteral k_VolumesArrayPath_Key = "volumes_array_path"; + static constexpr StringLiteral k_AvgQuatsArrayPath_Key = "avg_quats_array_path"; + static constexpr StringLiteral k_CrystalStructuresArrayPath_Key = "crystal_structures_array_path"; + static constexpr StringLiteral k_NewCellFeatureAttributeMatrixName_Key = "new_cell_feature_attribute_matrix_path"; + static constexpr StringLiteral k_CellParentIdsArrayName_Key = "cell_parent_ids_array_name"; + static constexpr StringLiteral k_FeatureParentIdsArrayName_Key = "feature_parent_ids_array_name"; + static constexpr StringLiteral k_ActiveArrayName_Key = "active_array_name"; + static constexpr StringLiteral k_RandomizeParentIds_Key = "randomize_parent_ids"; + static constexpr StringLiteral k_SeedArrayName_Key = "seed_array_name"; + static constexpr StringLiteral k_UseSeed_Key = "use_seed"; + static constexpr StringLiteral k_SeedValue_Key = "seed_value"; + + /** + * @brief Reads SIMPL json and converts it simplnx Arguments. + * @param json + * @return Result + */ + static Result FromSIMPLJson(const nlohmann::json& json); + + /** + * @brief Returns the name of the filter. + * @return + */ + std::string name() const override; + + /** + * @brief Returns the C++ classname of this filter. + * @return + */ + std::string className() const override; + + /** + * @brief Returns the uuid of the filter. + * @return + */ + Uuid uuid() const override; + + /** + * @brief Returns the human-readable name of the filter. + * @return + */ + std::string humanName() const override; + + /** + * @brief Returns the default tags for this filter. + * @return + */ + std::vector defaultTags() const override; + + /** + * @brief Returns the parameters of the filter (i.e. its inputs) + * @return + */ + Parameters parameters() const override; + + /** + * @brief Returns parameters version integer. + * Initial version should always be 1. + * Should be incremented everytime the parameters change. + * @return VersionType + */ + VersionType parametersVersion() const override; + + /** + * @brief Returns a copy of the filter. + * @return + */ + UniquePointer clone() const override; + +protected: + /** + * @brief Takes in a DataStructure and checks that the filter can be run on it with the given arguments. + * Returns any warnings/errors. Also returns the changes that would be applied to the DataStructure. + * Some parts of the actions may not be completely filled out if all the required information is not available at preflight time. + * @param dataStructure The input DataStructure instance + * @param filterArgs These are the input values for each parameter that is required for the filter + * @param messageHandler The MessageHandler object + * @param shouldCancel Atomic boolean value that can be checked to cancel the filter + * @param executionContext The ExecutionContext that can be used to determine the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + */ + PreflightResult preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, + const ExecutionContext& executionContext) const override; + + /** + * @brief Applies the filter's algorithm to the DataStructure with the given arguments. Returns any warnings/errors. + * On failure, there is no guarantee that the DataStructure is in a correct state. + * @param dataStructure The input DataStructure instance + * @param filterArgs These are the input values for each parameter that is required for the filter + * @param pipelineNode The node in the pipeline that is being executed + * @param messageHandler The MessageHandler object + * @param shouldCancel Atomic boolean value that can be checked to cancel the filter + * @param executionContext The ExecutionContext that can be used to determine the correct absolute path from a relative path + * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + */ + Result<> executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, + const ExecutionContext& executionContext) const override; +}; +} // namespace nx::core + +SIMPLNX_DEF_FILTER_TRAITS(nx::core, GroupMicroTextureRegionsFilter, "3f695987-81b1-47c3-8cff-b49cfa219be0"); +/* LEGACY UUID FOR THIS FILTER 5e18a9e2-e342-56ac-a54e-3bd0ca8b9c53 */ diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp index becdb78ce6..4953da6d1e 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp @@ -54,6 +54,7 @@ #include "OrientationAnalysis/Filters/ComputeFeatureFaceMisorientationFilter.hpp" #include "OrientationAnalysis/Filters/ComputeFZQuaternionsFilter.hpp" #include "OrientationAnalysis/Filters/ComputeGBCDPoleFigureFilter.hpp" +#include "OrientationAnalysis/Filters/GroupMicroTextureRegionsFilter.hpp" #include "OrientationAnalysis/Filters/ComputeIPFColorsFilter.hpp" #include "OrientationAnalysis/Filters/ComputeQuaternionConjugateFilter.hpp" #include "OrientationAnalysis/Filters/ReadH5EspritDataFilter.hpp" @@ -147,6 +148,7 @@ namespace nx::core {nx::core::Uuid::FromString("d67e9f28-2fe5-5188-b0f8-323a7e603de6").value(), {nx::core::FilterTraits::uuid, &ComputeGBCDMetricBasedFilter::FromSIMPLJson}}, // ComputeGBCDMetricBased {nx::core::Uuid::FromString("a4952f40-22dd-54ec-8c38-69c3fcd0e6f7").value(), {nx::core::FilterTraits::uuid, &WriteStatsGenOdfAngleFileFilter::FromSIMPLJson}}, // WriteStatsGenOdfAngleFile {nx::core::Uuid::FromString("27c724cc-8b69-5ebe-b90e-29d33858a032").value(), {nx::core::FilterTraits::uuid, &WriteINLFileFilter::FromSIMPLJson}}, // INLWriter + {nx::core::Uuid::FromString("5e18a9e2-e342-56ac-a54e-3bd0ca8b9c53").value(), {nx::core::FilterTraits::uuid, &GroupMicroTextureRegionsFilter::FromSIMPLJson}}, // GroupMicroTextureRegions // @@__MAP__UPDATE__TOKEN__DO__NOT__DELETE__@@ }; diff --git a/src/Plugins/OrientationAnalysis/test/CMakeLists.txt b/src/Plugins/OrientationAnalysis/test/CMakeLists.txt index 2979518aca..3d5bb836e0 100644 --- a/src/Plugins/OrientationAnalysis/test/CMakeLists.txt +++ b/src/Plugins/OrientationAnalysis/test/CMakeLists.txt @@ -41,6 +41,7 @@ set(${PLUGIN_NAME}UnitTest_SRCS CreateEnsembleInfoTest.cpp EBSDSegmentFeaturesFilterTest.cpp EbsdToH5EbsdTest.cpp + GroupMicroTextureRegionsTest.cpp MergeTwinsTest.cpp NeighborOrientationCorrelationTest.cpp ReadAngDataTest.cpp diff --git a/src/Plugins/OrientationAnalysis/test/GroupMicroTextureRegionsTest.cpp b/src/Plugins/OrientationAnalysis/test/GroupMicroTextureRegionsTest.cpp new file mode 100644 index 0000000000..ba564c2b94 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/test/GroupMicroTextureRegionsTest.cpp @@ -0,0 +1,502 @@ +#include "OrientationAnalysis/Filters/GroupMicroTextureRegionsFilter.hpp" +#include "OrientationAnalysis/OrientationAnalysis_test_dirs.hpp" + +#include + +#include "simplnx/Core/Application.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataArray.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" +#include "simplnx/DataStructure/NeighborList.hpp" +#include "simplnx/Parameters/ArraySelectionParameter.hpp" +#include "simplnx/Parameters/BoolParameter.hpp" +#include "simplnx/Parameters/NumberParameter.hpp" +#include "simplnx/Parameters/StringParameter.hpp" +#include "simplnx/Pipeline/Pipeline.hpp" +#include "simplnx/Pipeline/PipelineFilter.hpp" +#include "simplnx/UnitTest/UnitTestCommon.hpp" + +#include + +#include +#include +#include + +namespace fs = std::filesystem; +using namespace nx::core; +using namespace nx::core::UnitTest; + +namespace +{ +namespace AnalyticalFixtures +{ +const std::string k_GeomName = "ImageGeometry"; +const DataPath k_ImageGeomPath = DataPath({k_GeomName}); +const DataPath k_CellDataPath = k_ImageGeomPath.createChildPath("CellData"); +const DataPath k_FeatureDataPath = k_ImageGeomPath.createChildPath("CellFeatureData"); +const DataPath k_EnsembleDataPath = k_ImageGeomPath.createChildPath("CellEnsembleData"); + +const std::string k_FeatureIdsName = "FeatureIds"; +const std::string k_FeaturePhasesName = "FeaturePhases"; +const std::string k_VolumesName = "Volumes"; +const std::string k_AvgQuatsName = "AvgQuats"; +const std::string k_ContigNeighborListName = "ContiguousNeighborList"; +const std::string k_CrystalStructuresName = "CrystalStructures"; + +const std::string k_NewFeatureAMName = "MicroTextureFeatureData"; +const std::string k_CellParentIdsName = "CellParentIds"; +const std::string k_FeatureParentIdsName = "FeatureParentIds"; +const std::string k_ActiveName = "Active"; +const std::string k_SeedArrayName = "GroupMicroTextureRegions_Seed"; + +const DataPath k_FeatureIdsPath = k_CellDataPath.createChildPath(k_FeatureIdsName); +const DataPath k_FeaturePhasesPath = k_FeatureDataPath.createChildPath(k_FeaturePhasesName); +const DataPath k_VolumesPath = k_FeatureDataPath.createChildPath(k_VolumesName); +const DataPath k_AvgQuatsPath = k_FeatureDataPath.createChildPath(k_AvgQuatsName); +const DataPath k_ContigNeighborListPath = k_FeatureDataPath.createChildPath(k_ContigNeighborListName); +const DataPath k_CrystalStructuresPath = k_EnsembleDataPath.createChildPath(k_CrystalStructuresName); +const DataPath k_NewFeatureAMPath = k_ImageGeomPath.createChildPath(k_NewFeatureAMName); + +// Quaternion for a pure Bunge ZXZ Euler rotation (phi1=0, Phi=phiDeg, phi2=0). This is a pure +// rotation about the x-axis by phiDeg degrees, which tilts the crystal c-axis (originally along +// +z in crystal frame) by phiDeg degrees in the sample y-z plane. For two features with pure-Phi +// tilts of phiA and phiB degrees, the c-axis angular distance is |phiA - phiB| degrees, folded +// into [0, 90] via the (pi - w) symmetry in the algorithm. Storage convention: {x, y, z, w}. +std::array QuatFromPhiDeg(float32 phiDeg) +{ + const float32 halfAngleRad = (phiDeg * 0.5f) * 3.14159265358979323846f / 180.0f; + return {std::sin(halfAngleRad), 0.0f, 0.0f, std::cos(halfAngleRad)}; +} + +struct FixtureData +{ + DataStructure ds; + ImageGeom* geom = nullptr; + AttributeMatrix* cellAM = nullptr; + AttributeMatrix* featureAM = nullptr; + AttributeMatrix* ensembleAM = nullptr; + Int32Array* featureIds = nullptr; + Int32Array* featurePhases = nullptr; + Float32Array* volumes = nullptr; + Float32Array* avgQuats = nullptr; + NeighborList* neighborList = nullptr; + UInt32Array* crystalStructures = nullptr; +}; + +// Build a minimal {nX,1,1} ImageGeom with one cell per feature (FeatureIds[i] = i+1, so cell 0 +// belongs to feature 1, cell 1 to feature 2, etc.). Background feature 0 is allocated but no +// cell is assigned to it; it exists only so the FeaturePhases / NeighborList arrays have a 0-th +// tuple available. CrystalStructures[0] is a sentinel; CrystalStructures[1] is Hexagonal_High. +FixtureData CreateScaffold(usize numFeatures) +{ + FixtureData td; + const usize nX = numFeatures - 1; // one cell per real feature + const usize numCells = nX; + const usize numCrystalStructures = 2; + + td.geom = ImageGeom::Create(td.ds, k_GeomName); + td.geom->setSpacing({1.0f, 1.0f, 1.0f}); + td.geom->setOrigin({0.0f, 0.0f, 0.0f}); + td.geom->setDimensions({nX, 1, 1}); + + td.cellAM = AttributeMatrix::Create(td.ds, "CellData", ShapeType{1, 1, nX}, td.geom->getId()); + td.featureAM = AttributeMatrix::Create(td.ds, "CellFeatureData", ShapeType{numFeatures}, td.geom->getId()); + td.ensembleAM = AttributeMatrix::Create(td.ds, "CellEnsembleData", ShapeType{numCrystalStructures}, td.geom->getId()); + + td.featureIds = CreateTestDataArray(td.ds, k_FeatureIdsName, {1, 1, nX}, {1}, td.cellAM->getId()); + td.featurePhases = CreateTestDataArray(td.ds, k_FeaturePhasesName, {numFeatures}, {1}, td.featureAM->getId()); + td.volumes = CreateTestDataArray(td.ds, k_VolumesName, {numFeatures}, {1}, td.featureAM->getId()); + td.avgQuats = CreateTestDataArray(td.ds, k_AvgQuatsName, {numFeatures}, {4}, td.featureAM->getId()); + td.neighborList = NeighborList::Create(td.ds, k_ContigNeighborListName, ShapeType{numFeatures}, td.featureAM->getId()); + td.crystalStructures = CreateTestDataArray(td.ds, k_CrystalStructuresName, {numCrystalStructures}, {1}, td.ensembleAM->getId()); + + // One cell per real feature (cell k -> feature k+1). + for(usize k = 0; k < numCells; k++) + { + (*td.featureIds)[k] = static_cast(k + 1); + } + + // Feature 0 is the background; real features get phase 1 (Hex_High). + (*td.featurePhases)[0] = 0; + for(usize f = 1; f < numFeatures; f++) + { + (*td.featurePhases)[f] = 1; + } + + // Default volumes to 1.0 (uniform). Caller overrides if running-average behaviour matters. + for(usize f = 0; f < numFeatures; f++) + { + (*td.volumes)[f] = 1.0f; + } + + // Identity quaternion {x=0, y=0, z=0, w=1} everywhere by default — caller overrides per feature. + for(usize f = 0; f < numFeatures; f++) + { + (*td.avgQuats)[f * 4 + 0] = 0.0f; + (*td.avgQuats)[f * 4 + 1] = 0.0f; + (*td.avgQuats)[f * 4 + 2] = 0.0f; + (*td.avgQuats)[f * 4 + 3] = 1.0f; + } + + // Empty neighbor list per feature by default — caller overrides. + for(usize f = 0; f < numFeatures; f++) + { + td.neighborList->setList(static_cast(f), std::make_shared>(std::vector{})); + } + + (*td.crystalStructures)[0] = 999u; // sentinel + (*td.crystalStructures)[1] = static_cast(ebsdlib::CrystalStructure::Hexagonal_High); + + return td; +} + +void SetAvgQuat(FixtureData& td, usize featureIdx, const std::array& q) +{ + (*td.avgQuats)[featureIdx * 4 + 0] = q[0]; + (*td.avgQuats)[featureIdx * 4 + 1] = q[1]; + (*td.avgQuats)[featureIdx * 4 + 2] = q[2]; + (*td.avgQuats)[featureIdx * 4 + 3] = q[3]; +} + +void SetNeighbors(FixtureData& td, int32 featureIdx, std::vector neighbors) +{ + td.neighborList->setList(featureIdx, std::make_shared>(std::move(neighbors))); +} + +// Build the canonical 5-feature pure-Phi Bunge fixture used by both the Pure-Phi Class 1 test +// and the RandomizeParentIds invariance test. 5 real features (1..5) + background feature 0. +// Phi: F1=0, F2=5, F3=60, F4=63, F5=25 (degrees). Contiguous neighbor adjacency: +// F1 -- F2 -- F3 -- F4 and F5 (isolated) +// Under a 10 deg tolerance the expected groupings are: {F1,F2}, {F3,F4}, {F5}. +FixtureData Build5FeaturePureBunge() +{ + FixtureData td = CreateScaffold(/*numFeatures=*/6); + + SetAvgQuat(td, 1, QuatFromPhiDeg(0.0f)); + SetAvgQuat(td, 2, QuatFromPhiDeg(5.0f)); + SetAvgQuat(td, 3, QuatFromPhiDeg(60.0f)); + SetAvgQuat(td, 4, QuatFromPhiDeg(63.0f)); + SetAvgQuat(td, 5, QuatFromPhiDeg(25.0f)); + + SetNeighbors(td, 1, {2}); + SetNeighbors(td, 2, {1, 3}); + SetNeighbors(td, 3, {2, 4}); + SetNeighbors(td, 4, {3}); + SetNeighbors(td, 5, {}); + + return td; +} + +Arguments BuildArgs(float32 cAxisToleranceDeg, bool useRunningAverage, bool randomizeParentIds, uint64 seed) +{ + Arguments args; + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_UseNonContiguousNeighbors_Key, std::make_any(false)); + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_NonContiguousNeighborListArrayPath_Key, std::make_any(k_ContigNeighborListPath)); + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_ContiguousNeighborListArrayPath_Key, std::make_any(k_ContigNeighborListPath)); + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_UseRunningAverage_Key, std::make_any(useRunningAverage)); + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_CAxisTolerance_Key, std::make_any(cAxisToleranceDeg)); + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_FeatureIdsArrayPath_Key, std::make_any(k_FeatureIdsPath)); + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_FeaturePhasesArrayPath_Key, std::make_any(k_FeaturePhasesPath)); + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_VolumesArrayPath_Key, std::make_any(k_VolumesPath)); + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_AvgQuatsArrayPath_Key, std::make_any(k_AvgQuatsPath)); + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_CrystalStructuresArrayPath_Key, std::make_any(k_CrystalStructuresPath)); + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_NewCellFeatureAttributeMatrixName_Key, std::make_any(k_NewFeatureAMPath)); + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_CellParentIdsArrayName_Key, std::make_any(k_CellParentIdsName)); + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_FeatureParentIdsArrayName_Key, std::make_any(k_FeatureParentIdsName)); + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_ActiveArrayName_Key, std::make_any(k_ActiveName)); + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_RandomizeParentIds_Key, std::make_any(randomizeParentIds)); + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_UseSeed_Key, std::make_any(true)); + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_SeedValue_Key, std::make_any(seed)); + args.insertOrAssign(GroupMicroTextureRegionsFilter::k_SeedArrayName_Key, std::make_any(k_SeedArrayName)); + return args; +} +} // namespace AnalyticalFixtures +} // namespace + +TEST_CASE("OrientationAnalysis::GroupMicroTextureRegionsFilter: Class 1 Analytical (Pure-Phi Bunge)", "[OrientationAnalysis][GroupMicroTextureRegionsFilter][Class1]") +{ + using namespace AnalyticalFixtures; + + // See Build5FeaturePureBunge() for the layout: 5 real features arranged so that the expected + // groupings under a 10 deg tolerance are {F1,F2}, {F3,F4}, {F5}. + FixtureData td = Build5FeaturePureBunge(); + + // Tolerance 10 deg, UseRunningAverage=false (compare against seed's c-axis), RandomizeParentIds=false + // (deterministic parent-id assignment), Seed=42 (deterministic seed-iteration order). + Arguments args = BuildArgs(/*cAxisToleranceDeg=*/10.0f, /*useRunningAverage=*/false, /*randomizeParentIds=*/false, /*seed=*/42ULL); + + GroupMicroTextureRegionsFilter filter; + auto preflightResult = filter.preflight(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + REQUIRE_NOTHROW(td.ds.getDataRefAs(k_FeatureDataPath.createChildPath(k_FeatureParentIdsName))); + const auto& featureParentIds = td.ds.getDataRefAs(k_FeatureDataPath.createChildPath(k_FeatureParentIdsName)); + REQUIRE_NOTHROW(td.ds.getDataRefAs(k_CellDataPath.createChildPath(k_CellParentIdsName))); + const auto& cellParentIds = td.ds.getDataRefAs(k_CellDataPath.createChildPath(k_CellParentIdsName)); + + // Class 1 (Analytical) — grouping outcome: paired features share a parent, non-paired don't. + CHECK(featureParentIds[1] == featureParentIds[2]); // F1, F2 grouped + CHECK(featureParentIds[3] == featureParentIds[4]); // F3, F4 grouped + CHECK(featureParentIds[1] != featureParentIds[3]); // F1/F2 group != F3/F4 group + CHECK(featureParentIds[5] != featureParentIds[1]); // F5 is alone, different from F1/F2 group + CHECK(featureParentIds[5] != featureParentIds[3]); // F5 is alone, different from F3/F4 group + + // Class 4 (Invariant) — all real features assigned a positive parent id; three distinct groups. + CHECK(featureParentIds[1] > 0); + CHECK(featureParentIds[2] > 0); + CHECK(featureParentIds[3] > 0); + CHECK(featureParentIds[4] > 0); + CHECK(featureParentIds[5] > 0); + std::set distinctParents{featureParentIds[1], featureParentIds[2], featureParentIds[3], featureParentIds[4], featureParentIds[5]}; + CHECK(distinctParents.size() == 3); + + // Class 4 (Invariant) — cell parent ids must equal feature parent ids of the underlying feature. + REQUIRE_NOTHROW(td.ds.getDataRefAs(k_FeatureIdsPath)); + const auto& featureIds = td.ds.getDataRefAs(k_FeatureIdsPath); + for(usize k = 0; k < featureIds.getNumberOfTuples(); k++) + { + CHECK(cellParentIds[k] == featureParentIds[featureIds[k]]); + } + + // New feature attribute matrix should be sized to >= maxParent + 1 (index 0 reserved). + const auto& newFeatureAM = td.ds.getDataRefAs(k_NewFeatureAMPath); + int32 maxParent = 0; + for(usize f = 1; f < featureParentIds.getNumberOfTuples(); f++) + { + if(featureParentIds[f] > maxParent) + { + maxParent = featureParentIds[f]; + } + } + CHECK(newFeatureAM.getNumberOfTuples() == static_cast(maxParent + 1)); + + // Active array: present and sized to AM, default value true everywhere. + REQUIRE_NOTHROW(td.ds.getDataRefAs(k_NewFeatureAMPath.createChildPath(k_ActiveName))); + const auto& active = td.ds.getDataRefAs(k_NewFeatureAMPath.createChildPath(k_ActiveName)); + CHECK(active.getNumberOfTuples() == newFeatureAM.getNumberOfTuples()); + + // Seed value array: written and contains the seed we asked for. + REQUIRE_NOTHROW(td.ds.getDataRefAs(DataPath({k_SeedArrayName}))); + CHECK(td.ds.getDataRefAs(DataPath({k_SeedArrayName}))[0] == 42ULL); + + UnitTest::CheckArraysInheritTupleDims(td.ds); +} + +TEST_CASE("OrientationAnalysis::GroupMicroTextureRegionsFilter: RandomizeParentIds invariants", "[OrientationAnalysis][GroupMicroTextureRegionsFilter][Class4]") +{ + // Randomization shuffles the parent-id LABELS but cannot change the GROUPING. The Class 4 + // (Invariant) assertions below are what randomization must preserve: + // (a) Same equivalence classes — features that grouped before still group together after. + // (b) Same number of distinct groups. + // (c) Cell parent ids agree with feature parent ids of the underlying feature. + // (d) Parent ids stay positive (0 is reserved for unassigned). + // (e) Same seed -> identical shuffle across runs. + // We do NOT assert that "the shuffle differs from the identity permutation" because some + // small-N seed combinations may legitimately yield the identity; the framework property is + // determinism, not non-identity. The non-identity sanity check is done loosely at the end + // by comparing against the non-randomized baseline. + using namespace AnalyticalFixtures; + + // Baseline run: deterministic parent ids (no shuffle). + FixtureData tdA = Build5FeaturePureBunge(); + Arguments argsA = BuildArgs(10.0f, /*useRunningAverage=*/false, /*randomizeParentIds=*/false, /*seed=*/42ULL); + GroupMicroTextureRegionsFilter filter; + { + auto preflightResult = filter.preflight(tdA.ds, argsA); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(tdA.ds, argsA); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + } + const auto& parentIdsA = tdA.ds.getDataRefAs(k_FeatureDataPath.createChildPath(k_FeatureParentIdsName)); + + // Randomized run with seed=42. + FixtureData tdB = Build5FeaturePureBunge(); + Arguments argsB = BuildArgs(10.0f, /*useRunningAverage=*/false, /*randomizeParentIds=*/true, /*seed=*/42ULL); + { + auto preflightResult = filter.preflight(tdB.ds, argsB); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(tdB.ds, argsB); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + } + const auto& parentIdsB = tdB.ds.getDataRefAs(k_FeatureDataPath.createChildPath(k_FeatureParentIdsName)); + const auto& cellParentIdsB = tdB.ds.getDataRefAs(k_CellDataPath.createChildPath(k_CellParentIdsName)); + const auto& featureIdsB = tdB.ds.getDataRefAs(k_FeatureIdsPath); + + // Second randomized run with seed=42 — for the determinism invariant. + FixtureData tdC = Build5FeaturePureBunge(); + Arguments argsC = BuildArgs(10.0f, /*useRunningAverage=*/false, /*randomizeParentIds=*/true, /*seed=*/42ULL); + { + auto preflightResult = filter.preflight(tdC.ds, argsC); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(tdC.ds, argsC); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + } + const auto& parentIdsC = tdC.ds.getDataRefAs(k_FeatureDataPath.createChildPath(k_FeatureParentIdsName)); + + // (a) Equivalence classes preserved between A (no shuffle) and B (shuffled): for every pair + // of features, they share a parent id in A iff they share one in B. + for(int32 i = 1; i <= 5; i++) + { + for(int32 j = i + 1; j <= 5; j++) + { + const bool sameInA = parentIdsA[i] == parentIdsA[j]; + const bool sameInB = parentIdsB[i] == parentIdsB[j]; + CHECK(sameInA == sameInB); + } + } + + // (b) Same number of distinct groups before and after the shuffle (must equal the 3 hand-derived). + std::set distinctA{parentIdsA[1], parentIdsA[2], parentIdsA[3], parentIdsA[4], parentIdsA[5]}; + std::set distinctB{parentIdsB[1], parentIdsB[2], parentIdsB[3], parentIdsB[4], parentIdsB[5]}; + CHECK(distinctA.size() == 3); + CHECK(distinctB.size() == 3); + + // (c) Cell parent ids agree with feature parent ids of the underlying feature, post-shuffle. + for(usize k = 0; k < featureIdsB.getNumberOfTuples(); k++) + { + CHECK(cellParentIdsB[k] == parentIdsB[featureIdsB[k]]); + } + + // (d) Positivity preserved post-shuffle. + for(int32 f = 1; f <= 5; f++) + { + CHECK(parentIdsB[f] > 0); + } + + // (e) Determinism: same seed -> identical shuffle result. Compare every feature's parent id. + for(usize f = 0; f < parentIdsB.getNumberOfTuples(); f++) + { + CHECK(parentIdsB[f] == parentIdsC[f]); + } + + // Loose non-identity check: at least one feature's parent id changed after shuffling. With the + // 5-feature / 3-group setup and the std::mt19937_64 default-seed-driven shuffle, the identity + // permutation is exceedingly unlikely; a hit here would mean the shuffle didn't run at all. + bool anyDifferent = false; + for(int32 f = 1; f <= 5; f++) + { + if(parentIdsA[f] != parentIdsB[f]) + { + anyDifferent = true; + break; + } + } + CHECK(anyDifferent); + + UnitTest::CheckArraysInheritTupleDims(tdB.ds); +} + +TEST_CASE("OrientationAnalysis::GroupMicroTextureRegionsFilter: Class 1 Analytical (Tolerance Boundary)", "[OrientationAnalysis][GroupMicroTextureRegionsFilter][Class1]") +{ + using namespace AnalyticalFixtures; + + // 3 real features on a chain F1 -- F2 -- F3. F1 c-axis at Phi=0, F2 at Phi=8, F3 at Phi=20. + // - F1 -- F2 : 8 deg -- under 10 deg tolerance -> GROUP + // - F2 -- F3 : 12 deg -- over 10 deg tolerance using F2's c-axis (since UseRunningAverage=false, + // the algorithm compares each candidate to the BFS seed's c-axis, but + // inside the BFS walk over already-grouped features, comparison is from + // THAT feature's c-axis, not the original seed's) -> DO NOT BRIDGE + // Expected: 2 distinct groups -> {F1, F2}, {F3}. + FixtureData td = CreateScaffold(/*numFeatures=*/4); + + SetAvgQuat(td, 1, QuatFromPhiDeg(0.0f)); + SetAvgQuat(td, 2, QuatFromPhiDeg(8.0f)); + SetAvgQuat(td, 3, QuatFromPhiDeg(20.0f)); + + SetNeighbors(td, 1, {2}); + SetNeighbors(td, 2, {1, 3}); + SetNeighbors(td, 3, {2}); + + Arguments args = BuildArgs(10.0f, /*useRunningAverage=*/false, /*randomizeParentIds=*/false, /*seed=*/42ULL); + + GroupMicroTextureRegionsFilter filter; + auto preflightResult = filter.preflight(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + const auto& featureParentIds = td.ds.getDataRefAs(k_FeatureDataPath.createChildPath(k_FeatureParentIdsName)); + CHECK(featureParentIds[1] == featureParentIds[2]); + CHECK(featureParentIds[2] != featureParentIds[3]); + CHECK(featureParentIds[1] > 0); + CHECK(featureParentIds[3] > 0); + std::set distinctParents{featureParentIds[1], featureParentIds[2], featureParentIds[3]}; + CHECK(distinctParents.size() == 2); + + UnitTest::CheckArraysInheritTupleDims(td.ds); +} + +TEST_CASE("OrientationAnalysis::GroupMicroTextureRegionsFilter: Regression — runs in default UseNonContiguousNeighbors=false mode", "[OrientationAnalysis][GroupMicroTextureRegionsFilter][Regression]") +{ + // Pins the defect-A fix: prior to the fix, execute() unconditionally returned error -99345 when + // UseNonContiguousNeighbors=false because the null-pointer guard on the non-contiguous list was + // outside the if-block that populated it. Filter could not run in its primary mode. + using namespace AnalyticalFixtures; + + FixtureData td = CreateScaffold(/*numFeatures=*/3); + SetAvgQuat(td, 1, QuatFromPhiDeg(0.0f)); + SetAvgQuat(td, 2, QuatFromPhiDeg(2.0f)); + SetNeighbors(td, 1, {2}); + SetNeighbors(td, 2, {1}); + + Arguments args = BuildArgs(10.0f, false, false, 42ULL); + + GroupMicroTextureRegionsFilter filter; + auto preflightResult = filter.preflight(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); +} + +TEST_CASE("OrientationAnalysis::GroupMicroTextureRegionsFilter: SIMPL Backwards Compatibility", "[OrientationAnalysis][GroupMicroTextureRegionsFilter][BackwardsCompatibility]") +{ + auto app = Application::GetOrCreateInstance(); + UnitTest::LoadPlugins(); + auto filterList = app->getFilterList(); + + const fs::path conversionDir = fs::path(nx::core::unit_test::k_SourceDir.view()) / "test" / "simpl_conversion"; + + const std::vector> fixtures = { + {"SIMPL 6.5 (UUID)", conversionDir / "6_5" / "GroupMicroTextureRegionsFilter.json"}, + {"SIMPL 6.4 (Filter_Name)", conversionDir / "6_4" / "GroupMicroTextureRegionsFilter.json"}, + }; + + for(const auto& [label, fixturePath] : fixtures) + { + DYNAMIC_SECTION(label) + { + auto pipelineResult = Pipeline::FromSIMPLFile(fixturePath, filterList); + REQUIRE(pipelineResult.valid()); + + auto& pipeline = pipelineResult.value(); + REQUIRE(pipeline.size() == 1); + + auto* pipelineFilter = dynamic_cast(pipeline.at(0)); + REQUIRE(pipelineFilter != nullptr); + + const IFilter* filter = pipelineFilter->getFilter(); + REQUIRE(filter != nullptr); + REQUIRE(filter->uuid() == FilterTraits::uuid); + + CHECK(pipelineFilter->getComments().empty()); + + const Arguments args = pipelineFilter->getArguments(); + CHECK(args.value(GroupMicroTextureRegionsFilter::k_ActiveArrayName_Key) == "TestName"); + CHECK(args.value(GroupMicroTextureRegionsFilter::k_AvgQuatsArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); + CHECK(args.value(GroupMicroTextureRegionsFilter::k_CAxisTolerance_Key) == 2.5f); + CHECK(args.value(GroupMicroTextureRegionsFilter::k_CellParentIdsArrayName_Key) == "TestName"); + CHECK(args.value(GroupMicroTextureRegionsFilter::k_ContiguousNeighborListArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); + CHECK(args.value(GroupMicroTextureRegionsFilter::k_CrystalStructuresArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); + CHECK(args.value(GroupMicroTextureRegionsFilter::k_FeatureIdsArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); + CHECK(args.value(GroupMicroTextureRegionsFilter::k_FeatureParentIdsArrayName_Key) == "TestName"); + CHECK(args.value(GroupMicroTextureRegionsFilter::k_FeaturePhasesArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); + CHECK(args.value(GroupMicroTextureRegionsFilter::k_NewCellFeatureAttributeMatrixName_Key) == DataPath({"TestName"})); + CHECK(args.value(GroupMicroTextureRegionsFilter::k_NonContiguousNeighborListArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); + CHECK(args.value(GroupMicroTextureRegionsFilter::k_UseNonContiguousNeighbors_Key) == true); + CHECK(args.value(GroupMicroTextureRegionsFilter::k_UseRunningAverage_Key) == true); + CHECK(args.value(GroupMicroTextureRegionsFilter::k_VolumesArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); + } + } +} diff --git a/src/Plugins/OrientationAnalysis/test/simpl_conversion/6_4/GroupMicroTextureRegionsFilter.json b/src/Plugins/OrientationAnalysis/test/simpl_conversion/6_4/GroupMicroTextureRegionsFilter.json new file mode 100644 index 0000000000..e9a2f5eaae --- /dev/null +++ b/src/Plugins/OrientationAnalysis/test/simpl_conversion/6_4/GroupMicroTextureRegionsFilter.json @@ -0,0 +1,54 @@ +{ + "PipelineBuilder": { + "Name": "Group Micro Texture Regions 6.4 Backwards Compatibility Test", + "Number_Filters": 1, + "Version": 6 + }, + "0": { + "Filter_Enabled": true, + "Filter_Human_Label": "Group MicroTexture Regions", + "Filter_Name": "GroupMicroTextureRegions", + "ActiveArrayName": "TestName", + "AvgQuatsArrayPath": { + "Data Container Name": "DataContainer", + "Attribute Matrix Name": "CellData", + "Data Array Name": "TestArray" + }, + "CAxisTolerance": 2.5, + "CellParentIdsArrayName": "TestName", + "ContiguousNeighborListArrayPath": { + "Data Container Name": "DataContainer", + "Attribute Matrix Name": "CellData", + "Data Array Name": "TestArray" + }, + "CrystalStructuresArrayPath": { + "Data Container Name": "DataContainer", + "Attribute Matrix Name": "CellData", + "Data Array Name": "TestArray" + }, + "FeatureIdsArrayPath": { + "Data Container Name": "DataContainer", + "Attribute Matrix Name": "CellData", + "Data Array Name": "TestArray" + }, + "FeatureParentIdsArrayName": "TestName", + "FeaturePhasesArrayPath": { + "Data Container Name": "DataContainer", + "Attribute Matrix Name": "CellData", + "Data Array Name": "TestArray" + }, + "NewCellFeatureAttributeMatrixName": "TestName", + "NonContiguousNeighborListArrayPath": { + "Data Container Name": "DataContainer", + "Attribute Matrix Name": "CellData", + "Data Array Name": "TestArray" + }, + "UseNonContiguousNeighbors": 1, + "UseRunningAverage": 1, + "VolumesArrayPath": { + "Data Container Name": "DataContainer", + "Attribute Matrix Name": "CellData", + "Data Array Name": "TestArray" + } + } +} diff --git a/src/Plugins/OrientationAnalysis/test/simpl_conversion/6_5/GroupMicroTextureRegionsFilter.json b/src/Plugins/OrientationAnalysis/test/simpl_conversion/6_5/GroupMicroTextureRegionsFilter.json new file mode 100644 index 0000000000..6fa40c7eb8 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/test/simpl_conversion/6_5/GroupMicroTextureRegionsFilter.json @@ -0,0 +1,55 @@ +{ + "PipelineBuilder": { + "Name": "Group Micro Texture Regions Backwards Compatibility Test", + "Number_Filters": 1, + "Version": 6 + }, + "0": { + "Filter_Enabled": true, + "Filter_Human_Label": "Group MicroTexture Regions", + "Filter_Name": "GroupMicroTextureRegions", + "Filter_Uuid": "{5e18a9e2-e342-56ac-a54e-3bd0ca8b9c53}", + "ActiveArrayName": "TestName", + "AvgQuatsArrayPath": { + "Data Container Name": "DataContainer", + "Attribute Matrix Name": "CellData", + "Data Array Name": "TestArray" + }, + "CAxisTolerance": 2.5, + "CellParentIdsArrayName": "TestName", + "ContiguousNeighborListArrayPath": { + "Data Container Name": "DataContainer", + "Attribute Matrix Name": "CellData", + "Data Array Name": "TestArray" + }, + "CrystalStructuresArrayPath": { + "Data Container Name": "DataContainer", + "Attribute Matrix Name": "CellData", + "Data Array Name": "TestArray" + }, + "FeatureIdsArrayPath": { + "Data Container Name": "DataContainer", + "Attribute Matrix Name": "CellData", + "Data Array Name": "TestArray" + }, + "FeatureParentIdsArrayName": "TestName", + "FeaturePhasesArrayPath": { + "Data Container Name": "DataContainer", + "Attribute Matrix Name": "CellData", + "Data Array Name": "TestArray" + }, + "NewCellFeatureAttributeMatrixName": "TestName", + "NonContiguousNeighborListArrayPath": { + "Data Container Name": "DataContainer", + "Attribute Matrix Name": "CellData", + "Data Array Name": "TestArray" + }, + "UseNonContiguousNeighbors": 1, + "UseRunningAverage": 1, + "VolumesArrayPath": { + "Data Container Name": "DataContainer", + "Attribute Matrix Name": "CellData", + "Data Array Name": "TestArray" + } + } +} diff --git a/src/Plugins/OrientationAnalysis/vv/GroupMicroTextureRegionsFilter.md b/src/Plugins/OrientationAnalysis/vv/GroupMicroTextureRegionsFilter.md new file mode 100644 index 0000000000..a29ac6b7ba --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/GroupMicroTextureRegionsFilter.md @@ -0,0 +1,146 @@ +# V&V Report: GroupMicroTextureRegionsFilter + +| | | +|----------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Plugin | OrientationAnalysis | +| SIMPLNX UUID | `3f695987-81b1-47c3-8cff-b49cfa219be0` | +| SIMPLNX Human Name | Group MicroTexture Regions | +| DREAM3D 6.5.171 equivalent | `GroupMicroTextureRegions` — `Source/Plugins/Reconstruction/ReconstructionFilters/GroupMicroTextureRegions.{h,cpp}` (inherits from `GroupFeatures` base class) | +| DREAM3D 6.5.172 reference | commit `3d513ea1` (2025-10-23) `BUG: GroupMicrotextureRegions bug fixes, expose as usable filter` — used here as a corroborating reference for port-time fixes | +| Verified commit | ** | +| Status | DRAFT | +| Sign-off | *Michael Jackson — V&V pending review* | + +## At a glance + +| Aspect | Current state | +|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Algorithm Relationship | **Port** (with deliberate inheritance flattening and 2 port-time **regressions fixed during this V&V cycle**, plus 1 latent legacy bug already corrected in the original 2024-01-08 port). Legacy is a `GroupFeatures` subclass; SIMPLNX inlines the `GroupFeatures::execute` BFS loop into a single algorithm class. UUID retained: `5e18a9e2-…` → `3f695987-…`. | +| Oracle (confirmed) | **Class 1 (Analytical) primary + Class 4 (Invariant) companion** — pure Bunge φ1=0, Φ=φ°, φ2=0 fixtures make the c-axis angular distance between any two features exactly \|Φ_A − Φ_B\| (folded into [0°, 90°]); expected groupings derive in closed form from the chosen tolerance and neighbor adjacency. | +| Code paths enumerated | 9 of 9 (line-by-line walk of `GroupMicroTextureRegions::operator()`, `execute`, `getSeed`, `determineGrouping`, `randomizeParentIds`) | +| Tests today | 5 test cases: Class 1 "Pure-Phi Bunge" (5 features, 3 groups), Class 4 "RandomizeParentIds invariants" (equivalence-class preservation + seed determinism), Class 1 "Tolerance Boundary" (3 features, 2 groups), defect-A regression pin (UseNonContiguousNeighbors=false runs without error), SIMPL 6.4+6.5 backwards-compat (DYNAMIC_SECTION). | +| Exemplar archive | **None — data inlined in test source** (`test/GroupMicroTextureRegionsTest.cpp` namespace `AnalyticalFixtures`). Scaffold helper builds an `{nX,1,1}` ImageGeom + Cell/Feature/Ensemble AMs; per-feature pure-Phi quats and per-feature neighbor lists are set explicitly per test. | +| Legacy comparison | **Not run.** Legacy 6.5.171 randomizes parent IDs by default with a non-reproducible seed; bit-identical comparison is not meaningful. The 6.5.172 reference commit `3d513ea1` exposes `RandomizeParentIds` as a user parameter (default false) — the same design SIMPLNX now uses post-fix; this V&V verifies SIMPLNX against an independent oracle. | +| Bug flags | **None remaining.** Two port-time regressions (D1, D2) were found and fixed during this V&V cycle and are pinned by tests. One legacy 6.5.171 bug (D3) was already corrected in the original 2024 SIMPLNX port and is documented for migration users. | +| V&V phase | Phases 1, 3, 4, 5, 6, 7, 8, 9, 11 — complete. **Outstanding:** OOC build verification (dual-build protocol), Phase 13 (Status promotion to READY FOR REVIEW), second-engineer review of the oracle design and the 6.5.172 reference commit. | + +## Summary + +`GroupMicroTextureRegionsFilter` groups neighboring **Features** whose c-axes are aligned within a user-specified tolerance. It is intended for Hexagonal_High materials and operates only on features whose phase resolves to `Hexagonal_High`; features in any other Laue class are silently left ungrouped. The algorithm seeds with a randomly-chosen unassigned feature, then walks the contiguous (and optionally non-contiguous) neighbor list, grouping any neighbor whose c-axis falls within tolerance, then repeats with the next seed until all features have a parent. Verification used a **Class 1 (Analytical) oracle**: pure Bunge (0, Φ, 0) Euler angles make the sample-frame c-axis exactly `(0, sin Φ, cos Φ)` and the angular distance between any two c-axes exactly `|Φ_A − Φ_B|`, so the expected groupings on a small hand-built fixture follow directly from the chosen tolerance and contiguous neighbor list. Two port-time bugs that prevented the filter from running in its default mode (D1) and from randomizing parent IDs at all (D2) were found and fixed during the V&V cycle; the fixes are pinned by dedicated regression tests. + +## Algorithm Relationship + +*Classification:* **Port** ~~| Minor changes | Rewrite | New filter~~ + +*Evidence:* Legacy 6.5.171 inherits from a `GroupFeatures` base class that owns the BFS-over-neighbor-list loop; SIMPLNX inlines that loop into a single `GroupMicroTextureRegions` algorithm class. SIMPL UUID `5e18a9e2-e342-56ac-a54e-3bd0ca8b9c53` is preserved via `OrientationAnalysisLegacyUUIDMapping.hpp` → `3f695987-81b1-47c3-8cff-b49cfa219be0`. The selection logic (random-feature seed, BFS over contiguous-then-optional-non-contiguous neighbors, Hex_High-only acceptance criterion with optional running-average c-axis, deterministic-seeded RNG) is line-for-line equivalent. SIMPL 6.4 + 6.5 conversion fixtures live at `test/simpl_conversion/6_*/GroupMicroTextureRegionsFilter.json`. + +*Port-time deltas (each tracked as a deviation — see `vv/deviations/GroupMicroTextureRegionsFilter.md`):* + +1. **Inheritance flattened.** Legacy's `GroupFeatures::execute()` is the BFS driver; concrete-filter `GroupMicroTextureRegions::getSeed/determineGrouping` are the per-call hooks (Template Method). SIMPLNX inlines the BFS into `GroupMicroTextureRegions::execute()` (algorithm class) and keeps `getSeed`/`determineGrouping` as private methods. Same control flow, simpler class graph. +2. **Modern math API.** Legacy uses raw `float[3][3]` matrices + `MatrixMath::*` helpers + `QuaternionMathF`. SIMPLNX uses `ebsdlib::Matrix3X1F` / `Matrix3X3F` value-types and `ebsdlib::Quaternion::toOrientationMatrix().toGMatrix().transpose()`. Same arithmetic, different type wrapping; no observable output difference. +3. **Defect A (D1) — `UseNonContiguousNeighbors=false` default mode was broken.** The original SIMPLNX port placed the null-pointer check on the non-contiguous neighbor list *outside* the conditional that populates the pointer, so `execute()` unconditionally returned error `-99345` whenever `UseNonContiguousNeighbors=false` — the filter's documented primary mode. **Fixed during this V&V cycle** (`GroupMicroTextureRegions.cpp` lines 56–66): null check moved inside the `if(m_InputValues->UseNonContiguousNeighbors)` block. Pinned by `Regression — runs in default UseNonContiguousNeighbors=false mode` test case. +4. **Defect B (D2) — randomization permanently disabled.** The original SIMPLNX port had the seed-array machinery and a `randomizeParentIds()` helper, but the call site in `operator()` was a `// !!! COMMENT OUT FOR DEMONSTRATION !!!` comment block. The infrastructure existed (seed parameter, seed-output array, machinery to plumb a deterministic seed) but produced no randomization. Legacy 6.5.171 randomizes by default with a clock-derived seed (irreproducible). 6.5.172 commit `3d513ea1` (2025-10-23) — Mike's own legacy backport — exposes `RandomizeParentIds` as a user parameter defaulting to `false` for reproducible parity. **Fixed during this V&V cycle** to match the 6.5.172 design: new `k_RandomizeParentIds_Key` parameter (default `false`), restored `randomizeParentIds()` Fisher-Yates shuffle in the algorithm using `m_Generator` already seeded by `operator()`. The `UseSeed`/`SeedValue` parameters now correctly drive the algorithm's RNG (they were previously declared as parameters but the algorithm hard-coded `std::mt19937::default_seed` — also fixed). `parametersVersion()` bumped to 2. +5. **RNG architecture.** Legacy 6.5.171 calls `SIMPL_RANDOMNG_NEW()` inside `getSeed()` (creates a fresh RNG each call). SIMPLNX uses a single class-level `m_Generator` + `std::uniform_real_distribution` initialized once in `operator()` from `m_InputValues->SeedValue`. Matches the 6.5.172 design and is reproducible across runs given the same seed. Numerical sequence is different from 6.5.171 by construction; not a parity defect. +6. **Latent legacy bug already corrected (D3).** Legacy `determineGrouping` declares `uint32_t phase1 = 0` but only assigns it inside the `if(!m_UseRunningAverage)` branch. When `UseRunningAverage=true`, `phase1` stays at 0, and the subsequent `phase1 == phase2 && phase1 == Hexagonal_High` check fails silently — no features ever group. Bug introduced by J. Tucker 2014-01-30 (commit `7e49e52f` in upstream DREAM3D). SIMPLNX's port assigns `phase1` outside the conditional → bug already fixed in the 2024-01-08 initial port. The 6.5.172 reference commit `3d513ea1` (2025-10-23) deliberately back-ported the same fix to legacy as part of its "expose as usable filter" cleanup — confirmed by inspecting the commit's parent, which still carries the buggy code. The 6.5.171 release line was never patched. +7. **Hex-only restriction preserved.** Both versions reject non-Hexagonal_High pairs in `determineGrouping`. Not a deviation; documented as a filter precondition. + +*Material PRs since baseline (filter was in `SimplnxReview` until 2026-06-11):* + +- `2024-01-08 ca6d0aa` — initial port (`Add: GroupMicroTextureRegions and FindGroupingDensity`). Includes the D3 phase1 fix at port time. +- `2024-01-08 15daa51` — `Added Warnings to the filter, in the docs and as a Preflight Returned value.` Introduced the `-65432` "experimental, untested, unverified, unvalidated" preflight warning. +- `2025-10-07 ac46cab` — neighbor-list API update. +- `2025-12-03 db623d3` — "Microtexture Related bug fixes and code review (#7)". Did not address D1 or D2. +- `2026-03-03 6634fb8` — "New rewrite based on feedback". Did not address D1 or D2. +- `2026-06-11 ddf63bb` — moved from SimplnxReview to OrientationAnalysis (this branch). +- `2026-06-11` (this V&V cycle) — D1 fixed, D2 fixed, RNG seeded from `SeedValue`, `RandomizeParentIds` parameter added, `parametersVersion()` bumped to 2, dead `growPatch`/`growGrouping` stubs and dead `m_PatchGrouping` field removed, preflight warning `-65432` ("experimental, untested, unverified, unvalidated") removed now that V&V is in place, filter documentation updated to remove the matching banner and add the new `Randomize Parent Ids` section, V&V deliverables added. + +## Oracle + +*Class:* **1 (Analytical)** primary + **4 (Invariant)** companion. + +### Applied (Class 1 — Analytical) + +For pure Bunge Euler angles `(φ1=0, Φ=Φᵢ, φ2=0)`: + +- The Bunge passive orientation matrix reduces to `g = R_x(Φ)`. +- The crystal-frame c-axis `[0, 0, 1]` projected into sample frame becomes `g^T · [0, 0, 1] = R_x(-Φ) · [0, 0, 1] = (0, sin Φ, cos Φ)`. +- For any two features A, B with Φ-only tilts: `c_A · c_B = sin Φ_A · sin Φ_B + cos Φ_A · cos Φ_B = cos(Φ_A − Φ_B)`, so the angular distance is exactly `|Φ_A − Φ_B|`. +- The filter applies `w ≤ tol_rad || (π − w) ≤ tol_rad`, so the effective angular-distance metric is `min(θ, π − θ)`, folded into [0°, 90°]. + +Quaternion storage convention follows the sibling `ComputeFeatureNeighborCAxisMisalignmentsFilter` Class 1 test (the format-of-record for pure-Phi fixtures in OA): `{x = sin(Φ/2), y = 0, z = 0, w = cos(Φ/2)}`. The expected grouping outcomes follow directly from the chosen tolerance, the per-feature Φ, and the per-feature neighbor list. + +Fixture A — **Pure-Phi 5-feature chain**: F1(Φ=0°), F2(Φ=5°), F3(Φ=60°), F4(Φ=63°), F5(Φ=25°). Contiguous neighbors: F1↔F2, F2↔F3, F3↔F4, F5 isolated. Tolerance 10°. Expected: {F1,F2} group (Δ=5°), {F3,F4} group (Δ=3°), F2↔F3 bridge fails (Δ=55°), F5 alone — **3 distinct groups**. + +Fixture B — **Tolerance boundary 3-feature chain**: F1(Φ=0°), F2(Φ=8°), F3(Φ=20°). Contiguous neighbors: F1↔F2↔F3. Tolerance 10°. Expected: F1↔F2 group (Δ=8° ≤ 10°), F2↔F3 bridge fails (Δ=12° > 10° using F2's c-axis vs F3's c-axis under `UseRunningAverage=false`), so F3 alone — **2 distinct groups**. Exercises the on-the-boundary acceptance vs rejection. + +### Applied (Class 4 — Invariant) + +Companion assertions, applicable independent of which feature is picked as the first random seed: + +- **Same-group invariant**: features designed to bridge produce equal parent IDs; features designed not to bridge produce distinct parent IDs. +- **Positivity invariant**: every real feature's parent ID is `> 0` (parent ID 0 is reserved for unassigned). +- **Group count invariant**: the number of distinct parent IDs across real features matches the hand-derived count. +- **Cell-feature consistency**: `cellParentIds[k] == featureParentIds[featureIds[k]]` for every cell. +- **Attribute matrix sizing**: `newFeatureAM.getNumberOfTuples() == max(featureParentIds) + 1` (index 0 reserved). +- **Seed-roundtrip invariant**: the user-supplied seed value lands in the `_Group_MicroTexture_Regions_Seed_Value_` top-level array. + +### Encoded + +- **Class 1 (Analytical) + Class 4 (Invariant)**: + - `test/GroupMicroTextureRegionsTest.cpp::"OrientationAnalysis::GroupMicroTextureRegionsFilter: Class 1 Analytical (Pure-Phi Bunge)"` — 5-feature fixture, 12 assertions. + - `test/GroupMicroTextureRegionsTest.cpp::"OrientationAnalysis::GroupMicroTextureRegionsFilter: Class 1 Analytical (Tolerance Boundary)"` — 3-feature fixture, 5 assertions. +- **Regression pin (Class 4 invariant — runs cleanly)**: `test/GroupMicroTextureRegionsTest.cpp::"OrientationAnalysis::GroupMicroTextureRegionsFilter: Regression — runs in default UseNonContiguousNeighbors=false mode"` — guards against D1 regression. +- **SIMPL backward-compat (kept from move)**: `test/GroupMicroTextureRegionsTest.cpp::"OrientationAnalysis::GroupMicroTextureRegionsFilter: SIMPL Backwards Compatibility"` — DYNAMIC_SECTION over `simpl_conversion/6_4` and `simpl_conversion/6_5` fixtures. Validates UUID + argument-key conversion only; not a behavioral test. + +### Second-engineer review + +*Pending.* The c-axis closed-form derivation for pure-Phi Bunge angles is sibling-shared with `ComputeFeatureNeighborCAxisMisalignmentsFilter`, whose Class 1 oracle was reviewed previously; the same derivation applies here. A second-engineer pass on (a) the 6.5.172 reference commit `3d513ea1` as the corroborating source for the D2 design, and (b) the on-the-boundary 12°-rejection assumption in Fixture B (i.e., that under `UseRunningAverage=false` the algorithm compares each candidate against `firstFeature`'s c-axis, not against the running seed's c-axis), is recommended before Status promotion. + +## Code path coverage + +*9 of 9 paths exercised.* + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/GroupMicroTextureRegions.cpp` (266 lines). + +Logical phases: **(a) per-call init** in `operator()` (RNG seed, parent-ID init), **(b) seed-loop driver** in `execute()` (BFS over neighbor lists), **(c) per-candidate grouping decision** in `determineGrouping()`, **(d) per-seed bookkeeping** in `getSeed()` (parent-ID assignment, running-average update), **(e) post-loop finalize** in `operator()` (AM resize, cell-parent backfill, optional shuffle). + +| # | Phase | Path | Test case | +|----|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 1 | (a) Init | `m_Generator = std::mt19937_64(m_InputValues->SeedValue)` → seed propagates to all `getSeed` random draws | `Class 1 (Pure-Phi)` — seed=42 produces deterministic parent IDs; seed-roundtrip assertion confirms the SeedValue write. | +| 2 | (b) BFS driver | `UseNonContiguousNeighbors == false` → null-pointer guard NOT triggered (defect-A regression path) | `Regression — runs in default UseNonContiguousNeighbors=false mode` | +| 3 | (b) BFS driver | `UseNonContiguousNeighbors == true` → guard fires only if the optional list pointer is genuinely null | *Not directly tested.* Selection parameter validates path existence; the residual guard is defensive. Low-value gap. | +| 4 | (b) BFS driver | Inside the BFS, walk contiguous neighbors (`k=0`) and optionally non-contiguous (`k=1`) | `Class 1 (Pure-Phi)` exercises `k=0`. `k=1` only exercised via the `UseNonContiguousNeighbors=true` branch (see #3 — not directly tested in oracle fixtures). | +| 5 | (c) Grouping | `m_FeatureParentIds[neighborFeature] != -1` (already parented) → skip | `Class 1 (Pure-Phi)` — F2's neighbor list includes F1; after F1 is grouped, F1 fails this check when F2 walks back to it. | +| 6 | (c) Grouping | `referenceFeaturePhase == 0` or `neighborFeaturePhase == 0` (background) → skip | *Not directly tested.* Background feature 0 has phase 0; would be reached only if the algorithm picked f0 as a seed and tried to grow it. Low-value gap. | +| 7 | (c) Grouping | `phase1 == phase2 && phase1 == Hexagonal_High` AND `angularDist ≤ tol` (or `π − angularDist ≤ tol`) → assign parent, optionally update running-average c-axis | `Class 1 (Pure-Phi)` — F1↔F2 and F3↔F4 cover the accept-with-tolerance branch. | +| 8 | (c) Grouping | `phase1 == phase2 && phase1 == Hexagonal_High` BUT `angularDist > tol` AND `π − angularDist > tol` → no grouping | `Class 1 (Pure-Phi)` — F2↔F3 (55°) and `Class 1 (Tolerance Boundary)` — F2↔F3 (12°) cover the reject branch. | +| 9 | (d)/(e) Finalize | `m_NumTuples >= 2` → AM resize + cell-parent backfill; `RandomizeParentIds == true` invokes the Fisher-Yates shuffle | `Class 1 (Pure-Phi)` — AM-size and cell-feature-consistency assertions cover the unshuffled path. `RandomizeParentIds invariants` covers the shuffle path: equivalence-class preservation, group-count preservation, cell/feature consistency, positivity, and same-seed-determinism. | + +## Test inventory + +| Test case | Status | Notes | +|----------------------------------------------------------------------------------------------------------------------------------------|-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `OrientationAnalysis::GroupMicroTextureRegionsFilter: Class 1 Analytical (Pure-Phi Bunge)` | new-for-V&V | 5 features arranged in two chains + an isolated feature, shared scaffold via `AnalyticalFixtures::Build5FeaturePureBunge()`. 12 assertions covering same-group, different-group, group count, positivity, AM sizing, cell/feature consistency, and seed-roundtrip. | +| `OrientationAnalysis::GroupMicroTextureRegionsFilter: RandomizeParentIds invariants` | new-for-V&V | Re-uses the same 5-feature scaffold. Runs the filter three times — baseline (no shuffle) + two same-seed shuffled runs — and asserts the Class 4 invariants randomization must preserve: equivalence-class preservation (pairwise sameness pattern), group count, cell/feature consistency, positivity, same-seed determinism, plus a loose non-identity sanity check. | +| `OrientationAnalysis::GroupMicroTextureRegionsFilter: Class 1 Analytical (Tolerance Boundary)` | new-for-V&V | 3-feature chain probing the 12°-on-10°-tolerance acceptance boundary under `UseRunningAverage=false`. 5 assertions. | +| `OrientationAnalysis::GroupMicroTextureRegionsFilter: Regression — runs in default UseNonContiguousNeighbors=false mode` | new-for-V&V | Regression pin for defect A. Pre-fix `execute()` returned error `-99345` here; post-fix succeeds. | +| `OrientationAnalysis::GroupMicroTextureRegionsFilter: SIMPL Backwards Compatibility` | kept | DYNAMIC_SECTION over SIMPL 6.4 + 6.5 conversion fixtures. Validates UUID and argument-key conversion only; not a behavioral test. Was the only passing test under the old `[.][UNIMPLEMENTED][!mayfail]` regime. | +| *(retired)* `OrientationAnalysis::GroupMicroTextureRegionsFilter: Valid Filter Execution` (tag `[.][UNIMPLEMENTED][!mayfail]`) | retired | Old test used empty `DataPath{}` arguments throughout; could not pass preflight and was tagged hidden/expected-fail. Replaced by the two `Class 1 Analytical` tests above. The replacement actually exercises the algorithm with real data. | + +All non-retired tests pass on `vv/group_microtexture_regions` (verified on the in-core release build at 2026-06-11). OOC verification: pending — this V&V cycle did not run the OOC build profile, but the algorithm reads `Int32Array`, `Float32Array`, `UInt32Array`, and `NeighborList` via reference-binding in the constructor; OOC-incompatible patterns (raw-pointer access, parallel writes to the same array) are not used. + +## Exemplar archive + +- **Archive:** None — data inlined in `test/GroupMicroTextureRegionsTest.cpp` namespace `AnalyticalFixtures`. +- **SHA512:** N/A +- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/GroupMicroTextureRegionsFilter.md` + +The fixture scaffold (`AnalyticalFixtures::CreateScaffold(numFeatures)`) builds an `{nX,1,1}` ImageGeom with one cell per real feature, plus a background feature 0 / Cell/Feature/Ensemble attribute matrices. Per-feature pure-Phi quats are set via `SetAvgQuat(td, idx, QuatFromPhiDeg(phiDeg))`; per-feature neighbor lists via `SetNeighbors(td, idx, {...})`. The canonical 5-feature fixture used by the Pure-Phi Class 1 test and the RandomizeParentIds invariance test is built by `AnalyticalFixtures::Build5FeaturePureBunge()` (see the provenance sidecar for the per-feature Φ values, adjacency, and expected groupings). No `download_test_data()` entry is required. + +## Deviations from DREAM3D 6.5.171 + +Direct array-by-array comparison against 6.5.171 is not meaningful: 6.5.171 randomizes parent IDs by default using a clock-derived seed (irreproducible) and produces *grouping equivalence classes* that match SIMPLNX's groups under any permutation, but never *bit-identical* arrays. The three documented deviations are design-level statements rather than per-array diffs. + +- `GroupMicroTextureRegionsFilter-D1` — Defect A: pre-fix SIMPLNX returned error `-99345` when `UseNonContiguousNeighbors=false` (filter unusable in default mode). **Fixed.** See `vv/deviations/GroupMicroTextureRegionsFilter.md`. +- `GroupMicroTextureRegionsFilter-D2` — Defect B: SIMPLNX never randomized parent IDs (legacy 6.5.171 always randomized; 6.5.172 commit `3d513ea1` exposes `RandomizeParentIds` as a user parameter, default false). **Fixed by exposing `RandomizeParentIds` (default false) + restoring the helper and plumbing the user seed through.** See `vv/deviations/GroupMicroTextureRegionsFilter.md`. +- `GroupMicroTextureRegionsFilter-D3` — Legacy 6.5.171 bug: when `UseRunningAverage=true`, `phase1` is never assigned and the Hex_High acceptance check silently fails — no features ever group. Bug introduced upstream by J. Tucker on 2014-01-30 (commit `7e49e52f` in original DREAM3D). SIMPLNX corrected this in the 2024-01-08 initial port; the 6.5.172 reference commit `3d513ea1` applied the same fix to legacy. **Documented for migration users.** See `vv/deviations/GroupMicroTextureRegionsFilter.md`. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/GroupMicroTextureRegionsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/GroupMicroTextureRegionsFilter.md new file mode 100644 index 0000000000..fbc730ccc1 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/deviations/GroupMicroTextureRegionsFilter.md @@ -0,0 +1,102 @@ +# Deviations from DREAM3D 6.5.171: GroupMicroTextureRegionsFilter + +This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`Source/Plugins/Reconstruction/ReconstructionFilters/GroupMicroTextureRegions.{h,cpp}`, which inherits from `GroupFeatures` base). + +Entries are referenced by stable ID (`GroupMicroTextureRegionsFilter-D`) from the V&V report and from public migration guidance. The ID is stable across renames; the Filter UUID field is the permanent cross-reference anchor. + +For the 6.5.172 reference commit (Mike Jackson's custom backport branch that mirrors the SIMPLNX design fixes onto the legacy codebase for root-cause confirmation), see `/Users/mjackson/DREAM3D-Dev/DREAM3D` commit `3d513ea1` (2025-10-23) `BUG: GroupMicrotextureRegions bug fixes, expose as usable filter`. + +--- + +## GroupMicroTextureRegionsFilter-D1 + +| Field | Value | +|-----------------|--------------------------------------------------------------------| +| **Deviation ID** | `GroupMicroTextureRegionsFilter-D1` | +| **Filter UUID** | `3f695987-81b1-47c3-8cff-b49cfa219be0` | +| **Status** | active (fix landed 2026-06-11 on `vv/group_microtexture_regions`) | + +**Symptom:** Prior to 2026-06-11, running the filter with `UseNonContiguousNeighbors=false` (the documented and default mode) always returned error `-99345` with message `"There was an error getting the Non-contiguous neighborlist from the DataStructure"`. The filter was effectively unusable except via the non-default `UseNonContiguousNeighbors=true` path. + +**Root cause:** Bug. In `GroupMicroTextureRegions::execute()` (algorithm class), the null-pointer guard on `nonContigNeighListPtr` was placed *outside* the conditional that populates the pointer: + +```cpp +NeighborList* nonContigNeighListPtr = nullptr; +if(m_InputValues->UseNonContiguousNeighbors) +{ + nonContigNeighListPtr = m_DataStructure.getDataAs>(...); +} +if(nullptr == nonContigNeighListPtr) // <-- always triggers when UseNonContiguousNeighbors=false +{ + return MakeErrorResult(-99345, "..."); +} +``` + +Legacy 6.5.171 `GroupFeatures::execute()` gates the *use* of `nonContigNeighList` behind `if(m_UseNonContiguousNeighbors)`, not the existence check — so legacy handles the default-mode case correctly. The SIMPLNX port preserved the use-site guard but introduced an unconditional existence check. + +**Fix:** Moved the null check inside the `if(m_InputValues->UseNonContiguousNeighbors)` block, so it only fires when the user opted into the non-contiguous-neighbor path and the data store does not have the requested array. Pinned by `OrientationAnalysis::GroupMicroTextureRegionsFilter: Regression — runs in default UseNonContiguousNeighbors=false mode`. + +**Affected users:** Anyone who tried to run the filter before 2026-06-11 in its documented default mode. (Realistically: nobody, given the filter was shipped behind a `[.][UNIMPLEMENTED][!mayfail]` test and a preflight warning that it is "untested, unverified, unvalidated".) + +**Recommendation:** Trust SIMPLNX post-fix. The pre-fix SIMPLNX output was an unconditional error; no real-world results were produced from the broken code path. + +--- + +## GroupMicroTextureRegionsFilter-D2 + +| Field | Value | +|-----------------|--------------------------------------------------------------------| +| **Deviation ID** | `GroupMicroTextureRegionsFilter-D2` | +| **Filter UUID** | `3f695987-81b1-47c3-8cff-b49cfa219be0` | +| **Status** | active (fix landed 2026-06-11 on `vv/group_microtexture_regions`) | + +**Symptom:** Prior to 2026-06-11, SIMPLNX never randomized parent IDs even though the seed-array machinery, the `UseSeed` parameter, and the `SeedValue` parameter were all present. Legacy 6.5.171 randomizes parent IDs by default with a clock-derived seed (irreproducible). Output parent IDs from SIMPLNX were therefore monotonically assigned in BFS-walk order (1, 2, 3, …); output from 6.5.171 was the same equivalence classes under a random permutation. + +**Root cause:** Bug. In `GroupMicroTextureRegions::operator()` (algorithm class), the `RandomizeFeatureIds` call was a commented-out block annotated `// !!! COMMENT OUT FOR DEMONSTRATION !!!`. The seed-array output was still written, but the randomization step was a no-op. Additionally, the algorithm's RNG (`m_Generator`) was initialized with `std::mt19937::default_seed` rather than `m_InputValues->SeedValue`, so the user-supplied seed never reached the seed-selection loop in `getSeed()`. + +**Fix:** Adopted the 6.5.172 design (Mike's backport commit `3d513ea1`, 2025-10-23): added a new user parameter `RandomizeParentIds` (default `false`, so default behaviour is reproducible parent-id assignment). Restored a `randomizeParentIds(totalPoints, totalParentIds)` helper that performs a Fisher-Yates shuffle using `m_Generator` already seeded by `operator()`. Fixed the `operator()` RNG initialization to use `m_InputValues->SeedValue`. `parametersVersion()` bumped from 1 to 2 to flag the new parameter for SIMPL-conversion JSON. + +**Affected users:** Anyone migrating a pipeline from 6.5.171 that depended on randomized parent IDs (e.g., feeding the parent IDs straight into a color-mapped visualization where adjacent groups should not share the same color by accident). Post-fix: +- `RandomizeParentIds=false` (default) → reproducible parent IDs, suitable for diff testing and exemplar comparisons. +- `RandomizeParentIds=true, UseSeed=false` → 6.5.171-like behaviour (clock-derived seed, irreproducible). +- `RandomizeParentIds=true, UseSeed=true, SeedValue=` → reproducible randomization for users who want both shuffled IDs *and* run-to-run reproducibility. + +**Recommendation:** Trust SIMPLNX. The pre-fix SIMPLNX output was deterministic but unintentionally so; the post-fix output gives users explicit control over both randomization and seed. + +--- + +## GroupMicroTextureRegionsFilter-D3 + +| Field | Value | +|-----------------|-----------------------------------------------------------------------| +| **Deviation ID** | `GroupMicroTextureRegionsFilter-D3` | +| **Filter UUID** | `3f695987-81b1-47c3-8cff-b49cfa219be0` | +| **Status** | active (legacy bug; corrected in original 2024-01-08 SIMPLNX port) | + +**Symptom:** On legacy DREAM3D 6.5.171 with `UseRunningAverage=true`, the filter produces *no* feature groupings — every feature receives a unique parent ID equal to its own iteration index. SIMPLNX with `UseRunningAverage=true` produces the expected grouping behaviour. + +**Root cause:** Bug in 6.5.171 `determineGrouping`. The local `uint32_t phase1` is declared with default value 0 and only assigned inside the `if(!m_UseRunningAverage)` branch: + +```cpp +uint32_t phase1 = 0, phase2 = 0; +... +if(!m_UseRunningAverage) +{ + ... + phase1 = m_CrystalStructures[m_FeaturePhases[referenceFeature]]; // <-- only here + ... +} +phase2 = m_CrystalStructures[m_FeaturePhases[neighborFeature]]; +if(phase1 == phase2 && (phase1 == Ebsd::CrystalStructure::Hexagonal_High)) // <-- phase1=0 when UseRunningAverage=true +{ + ... +} +``` + +When `UseRunningAverage=true`, `phase1` stays at 0; the subsequent `phase1 == Hexagonal_High` check fails for every candidate; no grouping ever occurs. Bug introduced upstream on 2014-01-30 by J. Tucker (commit `7e49e52f362005e44ea9bf21b7a717277b2af04e` in the original DREAM3D repository) and never caught. + +**Fix in SIMPLNX:** The 2024-01-08 initial port (`ca6d0aa`) corrected the bug by assigning `phase1` outside the conditional, before the Hex_High check. The same fix was *deliberately back-ported to legacy* by the 6.5.172 reference commit `3d513ea1` (2025-10-23) — confirmed by inspecting the commit's parent, which still has the buggy `phase1` declaration. The 6.5.172 commit renames the variable to `phase1Xtal`, hoists the assignment out of the `if(!m_UseRunningAverage)` block, and ships with a developer comment that explicitly names the J. Tucker 2014-01-30 introduction as the bug source. The 6.5.171 release line was never patched. + +**Affected users:** Anyone running 6.5.171 with `UseRunningAverage=true` saw degenerate output (one group per feature) without warning. Users migrating that workflow to SIMPLNX will see *real* groupings for the first time, and downstream filters that consumed the degenerate output may behave differently. + +**Recommendation:** Trust SIMPLNX. The 6.5.171 result with `UseRunningAverage=true` was mathematically incorrect; SIMPLNX produces the result the filter has always claimed to produce. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/GroupMicroTextureRegionsFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/GroupMicroTextureRegionsFilter.md new file mode 100644 index 0000000000..4d59b9aaef --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/provenance/GroupMicroTextureRegionsFilter.md @@ -0,0 +1,75 @@ +# Exemplar Archive Provenance: Inlined in Test + +This sidecar records how the test data used by `GroupMicroTextureRegionsFilter`'s Class 1 / Class 4 unit tests was generated. It is the answer to "where did this hand-built data come from?" + +The test data is **inlined** in the test source — there is no separate tar.gz archive, no `download_test_data()` entry, and no `.dream3d` exemplar file to fetch. The fixture scaffold is built in C++ via `AnalyticalFixtures::CreateScaffold(numFeatures)` and the canonical 5-feature fixture by `AnalyticalFixtures::Build5FeaturePureBunge()`. + +--- + +## Archive identity + +| Field | Value | +|-------------------|----------------------------------------------------------------------------------------------------------------| +| **Archive** | Inlined (no separate archive) | +| **SHA512** | N/A | +| **Used by tests** | `OrientationAnalysis::GroupMicroTextureRegionsFilter: Class 1 Analytical (Pure-Phi Bunge)`, `…: RandomizeParentIds invariants`, `…: Class 1 Analytical (Tolerance Boundary)`, `…: Regression — runs in default UseNonContiguousNeighbors=false mode` | +| **Generated by** | Michael Jackson (V&V cycle), inspired by the `ComputeFeatureNeighborCAxisMisalignmentsFilter` Class 1 scaffold pattern | +| **Generated on** | 2026-06-11 | +| **Generated at commit** | *Pending — V&V branch `vv/group_microtexture_regions`; commit hash filled in at SBIR deliverable assembly* | + +## How it was generated + +The dataset is a hand-rolled minimal `{nX, 1, 1}` ImageGeom designed as a **Class 1 (Analytical) + Class 4 (Invariant) oracle** for the c-axis-tolerance grouping algorithm. Every quantity needed by the filter is set explicitly per feature so the expected groupings derive in closed form from the chosen tolerance and neighbor adjacency. + +1. **Geometry:** `ImageGeom` of dimensions `{nX, 1, 1}` with `nX = numFeatures - 1`. Spacing `{1, 1, 1}` and origin `{0, 0, 0}`. Each cell maps to a different real feature (`FeatureIds[k] = k + 1`), so the geometric layout is incidental — the filter only consumes per-feature data (quaternions, phases, volumes, neighbor list) and the cell→feature mapping for the final cell-parent backfill. The compact one-cell-per-feature layout exists purely so the test can assert the cell-feature consistency invariant on a small known mapping. + +2. **Feature structure:** `numFeatures` total tuples (default 6 for the canonical fixture: background feature 0 + five real features F1..F5). Feature 0 is the background sentinel with `FeaturePhases[0] = 0`; it has no cell assigned to it but exists so `FeaturePhases`, `Volumes`, `AvgQuats`, and `NeighborList` all have a 0-th tuple for `featureId = 0` lookups. Real features get `FeaturePhases = 1`. All `Volumes` default to `1.0f` (uniform); only matters under `UseRunningAverage=true`, which the current fixtures do not exercise. + +3. **Ensemble structure:** 2 phase entries. `CrystalStructures[0] = 999u` (sentinel — phase 0 is never resolved against this index in well-formed inputs), `CrystalStructures[1] = ebsdlib::CrystalStructure::Hexagonal_High`. The filter only groups features whose phase resolves to Hexagonal_High, so this single Laue class is sufficient to cover the production code path. + +4. **Orientations (pure-Phi Bunge):** Each feature's average orientation is a pure Bunge ZXZ Euler rotation `(φ1 = 0, Φ = Φᵢ, φ2 = 0)`. With `φ1 = φ2 = 0`, the orientation matrix reduces to `R = R_x(Φᵢ)`, and the crystal-frame c-axis `[0, 0, 1]` projected into sample frame becomes exactly `(0, sin Φᵢ, cos Φᵢ)`. The closed-form angular distance between any two features' c-axes is therefore `|Φ_A − Φ_B|`, folded into `[0°, 90°]` by the algorithm's `w ≤ tol || (π − w) ≤ tol` acceptance check. The quaternion stored is `{x = sin(Φᵢ/2), y = 0, z = 0, w = cos(Φᵢ/2)}`, set via the helper `QuatFromPhiDeg(Φ_deg)`. This convention matches the sibling `ComputeFeatureNeighborCAxisMisalignmentsFilter` Class 1 oracle. + +5. **Neighbor list:** `NeighborList` of `numFeatures` tuples, each set explicitly via `SetNeighbors(td, featureId, {…})`. Empty by default; callers set the adjacency for the specific scenario being tested. + +6. **Canonical 5-feature pure-Phi fixture (`Build5FeaturePureBunge`):** + - Per-feature Φ (degrees): `F1=0, F2=5, F3=60, F4=63, F5=25`. + - Contiguous neighbor list: `F1↔F2, F2↔F3, F3↔F4`, F5 isolated. + - Under 10° tolerance: `|0−5|=5 ≤ 10` (F1↔F2 group), `|5−60|=55 > 10` (F2↔F3 no bridge), `|60−63|=3 ≤ 10` (F3↔F4 group), F5 has no neighbors (alone). **Expected: 3 distinct groups → {F1,F2}, {F3,F4}, {F5}.** + - Re-used by the `RandomizeParentIds invariants` test, which runs the filter three times on independent copies of the fixture to assert equivalence-class preservation and same-seed determinism. + +7. **Tolerance boundary fixture (inlined in the Tolerance Boundary test):** + - Per-feature Φ: `F1=0, F2=8, F3=20`. Chain neighbors: `F1↔F2↔F3`. + - Under 10° tolerance: `|0−8|=8 ≤ 10` (F1↔F2 group), `|8−20|=12 > 10` (F2↔F3 no bridge, using F2's c-axis vs F3's under `UseRunningAverage=false`). **Expected: 2 distinct groups → {F1,F2}, {F3}.** + - Exercises on-the-boundary acceptance (8° just inside) vs rejection (12° just outside). + +8. **Regression-pin fixture (inlined in the Regression test):** A trivial 2-feature scaffold (F1=0°, F2=2°, mutual neighbors). Whether grouping occurs is incidental — the test exists to confirm the filter no longer returns error `-99345` in the `UseNonContiguousNeighbors=false` default mode (defect-A regression pin). + +9. **Quaternion convention verification:** The `QuatFromPhiDeg` helper produces `{sin(Φ/2), 0, 0, cos(Φ/2)}` (storage order `{x, y, z, w}`), matching what `ebsdlib::Quaternion(q[0], q[1], q[2], q[3])` consumes in the algorithm at lines 257 and 305 of `Algorithms/GroupMicroTextureRegions.cpp`. The convention is sibling-shared with `ComputeFeatureNeighborCAxisMisalignmentsFilter`, whose Class 1 oracle was reviewed previously. + +The dataset definition lives in `src/Plugins/OrientationAnalysis/test/GroupMicroTextureRegionsTest.cpp` namespace `AnalyticalFixtures` (functions `CreateScaffold`, `Build5FeaturePureBunge`, helpers `QuatFromPhiDeg`, `SetAvgQuat`, `SetNeighbors`, `BuildArgs`). + +## Canonical oracle output + +| DataPath | Source of expected values | +|------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `/ImageGeometry/CellFeatureData/FeatureParentIds` | Class 1 (Analytical) — per-pair grouping outcome derived in closed form from `\|Φ_A − Φ_B\|` and the contiguous neighbor adjacency. Encoded as same-group / different-group pairwise checks rather than absolute parent-id values, to remain invariant under deterministic seed-iteration order. | +| `/ImageGeometry/CellData/CellParentIds` | Class 4 (Invariant) — must equal `FeatureParentIds[FeatureIds[k]]` for every cell `k`. Encoded as a loop check; no independent oracle derivation is needed because the value is determined by the feature-parent assignment plus the cell→feature mapping. | +| `/ImageGeometry/MicroTextureFeatureData` (`AttributeMatrix` tuple count) | Class 4 (Invariant) — must equal `max(FeatureParentIds) + 1` (index 0 reserved for unassigned). | +| `/ImageGeometry/MicroTextureFeatureData/Active` | Class 4 (Invariant) — sized to the AM, default-initialized `true`. Verified for presence and tuple count; values not asserted (the algorithm does not write to this array, it inherits the default-true from `CreateArrayAction`). | +| `/GroupMicroTextureRegions_Seed` (top-level `UInt64Array`) | Class 4 (Invariant) — must equal the user-supplied seed value when `UseSeed=true`. Pins the seed-roundtrip behaviour. | + +For the `RandomizeParentIds invariants` test the canonical oracle output is the *set of invariants randomization must preserve* (equivalence classes, group count, cell/feature consistency, positivity, same-seed determinism) rather than specific array values. See the V&V report's Code-path coverage row #9 and the test's inline comments for the invariant list. + +## Oracle provenance (Classes 2, 3, 5 only) + +N/A — Class 1 (Analytical) + Class 4 (Invariant) oracle. Both lower-drift classes have their derivation embedded in the test source (the `QuatFromPhiDeg` comment for the c-axis math; the per-test comment block at the top of each TEST_CASE for the expected groupings and the invariants). + +## Second-engineer oracle review + +- **Reviewer:** *Pending — the pure-Phi Bunge derivation is sibling-shared with `ComputeFeatureNeighborCAxisMisalignmentsFilter` (already reviewed); a second engineer should still confirm (a) the F2↔F3 rejection at 12° in the Tolerance Boundary fixture is correct under `UseRunningAverage=false` semantics (i.e., that the algorithm compares each BFS step against the immediately-preceding feature's c-axis, not against the original seed's c-axis), and (b) the invariant list in `RandomizeParentIds invariants` is exhaustive — that no observable behaviour of the shuffle is left unasserted.* +- **Date:** *YYYY-MM-DD (pending)* +- **Skip reason** (if skipped): *N/A — review recommended.* + +## Regenerated to fix a circular-oracle situation? + +N/A. This dataset is brand-new for the SIMPLNX V&V cycle; no prior exemplar existed for this filter that needed retroactive replacement. The pre-V&V test was tagged `[.][UNIMPLEMENTED][!mayfail]` with empty `DataPath{}` arguments throughout and never actually exercised the algorithm; it was retired in favour of the inlined Class 1 + Class 4 fixtures documented here.