diff --git a/src/Plugins/ComplexCore/src/ComplexCore/Filters/Algorithms/ApplyTransformationToGeometry.cpp b/src/Plugins/ComplexCore/src/ComplexCore/Filters/Algorithms/ApplyTransformationToGeometry.cpp index f9a7856302..eeb6561967 100644 --- a/src/Plugins/ComplexCore/src/ComplexCore/Filters/Algorithms/ApplyTransformationToGeometry.cpp +++ b/src/Plugins/ComplexCore/src/ComplexCore/Filters/Algorithms/ApplyTransformationToGeometry.cpp @@ -148,6 +148,20 @@ Result<> ApplyTransformationToGeometry::operator()() return MakeErrorResult(-84500, fmt::format("Keeping the original geometry is not supported.")); } + auto* imageGeometryPtr = m_DataStructure.getDataAs(m_InputValues->SelectedGeometryPath); + const bool isNodeBased = (imageGeometryPtr == nullptr); + + ImageRotationUtilities::Matrix4fR translationToGlobalOriginMat = ImageRotationUtilities::Matrix4fR::Identity(); + ImageRotationUtilities::Matrix4fR translationFromGlobalOriginMat = ImageRotationUtilities::Matrix4fR::Identity(); + if(isNodeBased) + { + auto& nodeGeometry0D = m_DataStructure.getDataRefAs(m_InputValues->SelectedGeometryPath); + auto boundingBox = nodeGeometry0D.getBoundingBox(); + Point3Df minPoint = boundingBox.getMinPoint(); + translationToGlobalOriginMat = ImageRotationUtilities::GenerateTranslationTransformationMatrix({-minPoint[0], -minPoint[1], -minPoint[2]}); + translationFromGlobalOriginMat = ImageRotationUtilities::GenerateTranslationTransformationMatrix({minPoint[0], minPoint[1], minPoint[2]}); + } + switch(m_InputValues->TransformationSelection) { case k_NoTransformIdx: // No-Op @@ -168,6 +182,12 @@ Result<> ApplyTransformationToGeometry::operator()() case k_RotationIdx: // Rotation via axis-angle { m_TransformationMatrix = ImageRotationUtilities::GenerateRotationTransformationMatrix(m_InputValues->Rotation); + if(isNodeBased) + { + // Translate the geometry to/from the global origin + m_TransformationMatrix = translationFromGlobalOriginMat * m_TransformationMatrix * translationToGlobalOriginMat; + } + break; } case k_TranslationIdx: // Translation @@ -178,6 +198,11 @@ Result<> ApplyTransformationToGeometry::operator()() case k_ScaleIdx: // Scale { m_TransformationMatrix = ImageRotationUtilities::GenerateScaleTransformationMatrix(m_InputValues->Scale); + if(isNodeBased) + { + // Translate the geometry to/from the global origin + m_TransformationMatrix = translationFromGlobalOriginMat * m_TransformationMatrix * translationToGlobalOriginMat; + } break; } } diff --git a/src/Plugins/OrientationAnalysis/CMakeLists.txt b/src/Plugins/OrientationAnalysis/CMakeLists.txt index 2fd722e2f0..3c12884616 100644 --- a/src/Plugins/OrientationAnalysis/CMakeLists.txt +++ b/src/Plugins/OrientationAnalysis/CMakeLists.txt @@ -65,6 +65,7 @@ set(FilterList ImportH5EspritDataFilter ImportH5OimDataFilter INLWriterFilter + MergeColoniesFilter MergeTwinsFilter NeighborOrientationCorrelationFilter ReadAngDataFilter @@ -199,6 +200,7 @@ set(filter_algorithms ImportH5EspritData ImportH5OimData INLWriter + MergeColonies MergeTwins NeighborOrientationCorrelation ReadAngData diff --git a/src/Plugins/OrientationAnalysis/docs/MergeColoniesFilter.md b/src/Plugins/OrientationAnalysis/docs/MergeColoniesFilter.md new file mode 100644 index 0000000000..08d5eb84d1 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/docs/MergeColoniesFilter.md @@ -0,0 +1,69 @@ +# Merge Colonies # + +## Group (Subgroup) ## + +Reconstruction (Grouping) + +## Description ## + +This **Filter** groups neighboring **Features** that have a *special* misorientation that is associated with *alpha* variants that transformed from the same *beta* grain in titanium. The algorithm for grouping the **Features** is analogous to the algorithm for segmenting the **Features**, except the average orientation of the **Features** are used instead of the orientations of the individual **Elements** and the criterion for grouping is specific to the *alpha-beta transformation*. The user can specify a tolerance on both the *axis* and the *angle* that defines the misorientation relationship (i.e., a tolerance of 1 degree for both tolerances would allow the neighboring **Features** to be grouped if their misorientation was between 59-61 degrees about an axis within 1 degree of a2, as given by the 3rd *special* misorientation below). + +The list of *special* misorientations can be found in the paper by Germain et al.1 and are listed here: + +| Angle | Axis | +|------|------| +| 0 | Identity | +| 10.529 | c = <0001> | +| 60 | a2 = <-12-10> | +| 60.832 | d1 at 80.97 degrees from c in the plane of (d3,c) | +| 63.262 | d2 at 72.73 degrees from c in the plane of (a2,c) | +| 90 | d3 at 5.26 degrees from a2 in the basal plane | + +## Parameters ## + +| Name | Type | Description | +|------|------| ----------- | +| Use Seed | bool | Whether a seed shouold be used for random generation | +| Seed | uint64 | This is the value fed into the random generator | +| Axis Tolerance (Degrees) | float32 | Tolerance allowed when comparing the axis part of the axis-angle representation of the misorientation to the *special* misorientations listed above | +| Angle Tolerance (Degrees) | float32 | Tolerance allowed when comparing the angle part of the axis-angle representation of the misorientation to the *special* misorientations listed above | +| Use Non-Contiguous Neighbors | bool | Whether to use a non-contiguous neighbor list during the merging process | + +## Required Geometry ## + +Not Applicable + +## Required Objects ## + +| Kind | Default Name | Type | Component Dimensions | Description | +|------|--------------|------|----------------------|-------------| +| **Feature Attribute Array** | NonContiguousNeighbors | Int32NeighborList | (1) | List of non-contiguous neighbors for each **Feature**. Only needed if *Use Non-Contiguous Neighbors* is checked | +| **Feature Attribute Array** | NeighborList | Int32NeighborList | (1) | List of neighbors for each **Feature** | +| **Element Attribute Array** | FeatureIds | int32 | (1) | Specifies to which **Feature** each **Element** belongs | +| **Element Attribute Array** | Phases | int32 | (1) | Specifies to which **Ensemble** each **Element** belongs | +| **Feature Attribute Array** | Phases | int32 | (1) | Specifies to which **Ensemble** each **Feature** belongs | +| **Feature Attribute Array** | AvgQuats | float32 | (4) | Specifies the average orientation of the **Feature** in quaternion representation | +| **Ensemble Attribute Array** | CrystalStructures | uint32 | (1) | Enumeration representing the crystal structure for each **Ensemble** | + +## Created Objects ## + +| Kind | Default Name | Type | Component Dimensions | Description | +|------|--------------|------|----------------------|-------------| +| **Element Attribute Array** | ParentIds | int32 | (1) | Specifies to which *parent* each **Element** belongs | +| **Attribute Matrix** | NewFeatureData | Feature | N/A | Created **Feature Attribute Matrix** name | +| **Feature Attribute Array** | ParentIds | int32 | (1) | Specifies to which *parent* each **Feature** belongs | +| **Feature Attribute Array** | Active | bool | (1) | Specifies if the **Feature** is still in the sample (*true* if the **Feature** is in the sample and *false* if it is not). At the end of the **Filter**, all **Features** will be *Active* | + +## References ## + +[1] L. Germain, N. Gey and M. Humbert, Reliability of reconstructed Beta orientation maps in titanium alloys, Ultramicrscopy, 2007, 1129-1135. + +## Example Pipelines ## + +## License & Copyright ## + +Please see the description file distributed with this **Plugin** + +## DREAM.3D Mailing Lists ## + +If you need more help with a **Filter**, please consider asking your question on the [DREAM.3D Users Google group!](https://groups.google.com/forum/?hl=en#!forum/dream3d-users) diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/MergeColonies.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/MergeColonies.cpp new file mode 100644 index 0000000000..e9149cbefc --- /dev/null +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/MergeColonies.cpp @@ -0,0 +1,366 @@ +#include "MergeColonies.hpp" + +#include "complex/Common/Array.hpp" +#include "complex/Common/Constants.hpp" +#include "complex/DataStructure/DataArray.hpp" +#include "complex/DataStructure/DataGroup.hpp" +#include "complex/DataStructure/NeighborList.hpp" +#include "complex/Utilities/Math/GeometryMath.hpp" +#include "complex/Utilities/Math/MatrixMath.hpp" + +#include "EbsdLib/Core/EbsdLibConstants.h" +#include "EbsdLib/Core/Orientation.hpp" +#include "EbsdLib/Core/OrientationTransformation.hpp" +#include "EbsdLib/Core/Quaternion.hpp" + +using namespace complex; +using LaueOpsShPtrType = std::shared_ptr; +using LaueOpsContainer = std::vector; + +namespace +{ +const float64 unit110 = 1.0 / std::sqrt(2.0); +const float64 unit111 = 1.0 / std::sqrt(3.0); +const float64 unit112_1 = 1.0 / std::sqrt(6.0); +const float64 unit112_2 = 2.0 / std::sqrt(6.0); + +float64 crystalDirections[12][3][3] = {{{unit111, unit112_1, unit110}, {-unit111, -unit112_1, unit110}, {unit111, -unit112_2, 0}}, + + {{-unit111, unit112_1, unit110}, {unit111, -unit112_1, unit110}, {unit111, unit112_2, 0}}, + + {{unit111, -unit112_1, unit110}, {unit111, -unit112_1, -unit110}, {unit111, unit112_2, 0}}, + + {{unit111, unit112_1, unit110}, {unit111, unit112_1, -unit110}, {-unit111, unit112_2, 0}}, + + {{unit111, unit112_1, unit110}, {unit111, -unit112_2, 0}, {unit111, unit112_1, -unit110}}, + + {{unit111, -unit112_1, unit110}, {-unit111, -unit112_2, 0}, {unit111, -unit112_1, -unit110}}, + + {{unit111, -unit112_1, unit110}, {unit111, unit112_2, 0}, {-unit111, unit112_1, unit110}}, + + {{-unit111, -unit112_1, unit110}, {unit111, -unit112_2, 0}, {unit111, unit112_1, unit110}}, + + {{unit111, -unit112_2, 0}, {unit111, unit112_1, unit110}, {-unit111, -unit112_1, unit110}}, + + {{unit111, unit112_2, 0}, {-unit111, unit112_1, unit110}, {unit111, -unit112_1, unit110}}, + + {{unit111, unit112_2, 0}, {unit111, -unit112_1, unit110}, {unit111, -unit112_1, -unit110}}, + + {{-unit111, unit112_2, 0}, {unit111, unit112_1, unit110}, {unit111, unit112_1, -unit110}}}; + +// ----------------------------------------------------------------------------- +// +// ----------------------------------------------------------------------------- +bool check_for_burgers(const QuatD& betaQuat, const QuatD& alphaQuat, float64 angleTolerance) +{ + float64 dP = 0.0; + float64 angle = 0.0; + float64 radToDeg = 180.0 / Constants::k_PiD; + + float64 gBeta[3][3] = {{0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}}; + float64 gBetaT[3][3] = {{0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}}; + OrientationTransformation::qu2om(betaQuat).toGMatrix(gBeta); + // transpose gBeta so the sample direction is the output when + // gBeta is multiplied by the crystal directions below + MatrixMath::Transpose3x3(gBeta, gBetaT); + + float64 gAlpha[3][3] = {{0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}}; + float64 gAlphaT[3][3] = {{0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}}; + OrientationTransformation::qu2om(alphaQuat).toGMatrix(gAlpha); + // transpose gBeta so the sample direction is the output when + // gBeta is multiplied by the crystal directions below + MatrixMath::Transpose3x3(gAlpha, gAlphaT); + + float64 mat[3][3] = {{0.0, 0.0, 0.0}, {0.0, 0.0, 0.0}}; + for(int32 i = 0; i < 12; i++) + { + MatrixMath::Multiply3x3with3x3(gBetaT, crystalDirections[i], mat); + Point3Dd a = Point3Dd(mat[0][2], mat[1][2], mat[2][2]); + Point3Dd b = Point3Dd(gAlphaT[0][2], gAlphaT[1][2], gAlphaT[2][2]); + dP = GeometryMath::CosThetaBetweenVectors(a, b); + angle = std::acos(dP); + if((angle * radToDeg) < angleTolerance || (180.0f - (angle * radToDeg)) < angleTolerance) + { + a[0] = mat[0][0]; + a[1] = mat[1][0]; + a[2] = mat[2][0]; + b[0] = gAlphaT[0][0]; + b[1] = gAlphaT[1][0]; + b[2] = gAlphaT[2][0]; + dP = GeometryMath::CosThetaBetweenVectors(a, b); + angle = std::acos(dP); + if((angle * radToDeg) < angleTolerance) + { + return true; + } + if((180.0 - (angle * radToDeg)) < angleTolerance) + { + return true; + } + b[0] = -0.5 * gAlphaT[0][0] + 0.866025 * gAlphaT[0][1]; + b[1] = -0.5 * gAlphaT[1][0] + 0.866025 * gAlphaT[1][1]; + b[2] = -0.5 * gAlphaT[2][0] + 0.866025 * gAlphaT[2][1]; + dP = GeometryMath::CosThetaBetweenVectors(a, b); + angle = std::acos(dP); + if((angle * radToDeg) < angleTolerance) + { + return true; + } + if((180.0 - (angle * radToDeg)) < angleTolerance) + { + return true; + } + b[0] = -0.5 * gAlphaT[0][0] - 0.866025 * gAlphaT[0][1]; + b[1] = -0.5 * gAlphaT[1][0] - 0.866025 * gAlphaT[1][1]; + b[2] = -0.5 * gAlphaT[2][0] - 0.866025 * gAlphaT[2][1]; + dP = GeometryMath::CosThetaBetweenVectors(a, b); + angle = std::acos(dP); + if((angle * radToDeg) < angleTolerance) + { + return true; + } + if((180.0 - (angle * radToDeg)) < angleTolerance) + { + return true; + } + } + } + return false; +} +} // namespace + +// ----------------------------------------------------------------------------- +MergeColonies::MergeColonies(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, MergeColoniesInputValues* inputValues, + GroupFeaturesInputValues* groupInputValues) +: GroupFeatures(dataStructure, mesgHandler, shouldCancel, groupInputValues) +, m_DataStructure(dataStructure) +, m_InputValues(inputValues) +, m_ShouldCancel(shouldCancel) +, m_MessageHandler(mesgHandler) +, m_OrientationOps(LaueOps::GetAllOrientationOps()) +, m_FeatureParentIds(dataStructure.getDataRefAs(inputValues->FeatureParentIdsPath)) +, m_FeaturePhases(dataStructure.getDataRefAs(inputValues->FeaturePhasesPath)) +, m_AvgQuats(dataStructure.getDataRefAs(inputValues->AvgQuatsPath)) +, m_CrystalStructures(dataStructure.getDataRefAs(inputValues->CrystalStructuresPath)) +, m_AxisToleranceRad(inputValues->AxisTolerance * Constants::k_PiF / 180.0f) +, m_AngleTolerance(inputValues->AngleTolerance) +{ +} + +// ----------------------------------------------------------------------------- +MergeColonies::~MergeColonies() noexcept = default; + +// ----------------------------------------------------------------------------- +const std::atomic_bool& MergeColonies::getCancel() +{ + return m_ShouldCancel; +} + +// ----------------------------------------------------------------------------- +Result<> MergeColonies::operator()() +{ + GroupFeatures::execute(); + + auto active = m_DataStructure.getDataRefAs(m_InputValues->ActivePath); + + usize totalFeatures = active.getNumberOfTuples(); + if(totalFeatures < 2) + { + return MakeErrorResult(-87000, "The number of Grouped Features was 0 or 1 which means no grouped Features were detected. A grouping value may be set too high"); + } + + auto featureIds = m_DataStructure.getDataRefAs(m_InputValues->FeatureIdsPath); + auto cellParentIds = m_DataStructure.getDataRefAs(m_InputValues->CellParentIdsPath); + + int32 numParents = 0; + usize totalPoints = featureIds.getNumberOfTuples(); + for(usize k = 0; k < totalPoints; k++) + { + int32 featurename = featureIds[k]; + cellParentIds[k] = m_FeatureParentIds[featurename]; + if(m_FeatureParentIds[featurename] > numParents) + { + numParents = m_FeatureParentIds[featurename]; + } + } + numParents += 1; + + m_MessageHandler({IFilter::Message::Type::Info, "Characterizing Colonies Starting"}); + characterize_colonies(); + m_MessageHandler({IFilter::Message::Type::Info, "Characterizing Colonies Complete"}); + + if(m_InputValues->RandomizeParentIds) + { + m_MessageHandler({IFilter::Message::Type::Info, "Randomizing Parent Ids...."}); + // Generate all the numbers up front + const int32 rangeMin = 1; + const int32 rangeMax = numParents - 1; + std::mt19937 generator(m_InputValues->SeedValue); // Standard mersenne_twister_engine seeded + std::uniform_int_distribution distribution(rangeMin, rangeMax); + + std::vector pid(numParents); + pid.push_back(0); + std::set parentIdSet; + parentIdSet.insert(0); + for(int32 i = 1; i < numParents; ++i) + { + pid.push_back(i); // numberGenerator(); + parentIdSet.insert(pid[i]); + } + + int32 r = 0; + int32 temp = 0; + + m_MessageHandler({IFilter::Message::Type::Info, "Shuffle elements ...."}); + //--- Shuffle elements by randomly exchanging each with one other. + for(int32 i = 1; i < numParents; i++) + { + r = distribution(generator); // Random remaining position. + if(r >= numParents) + { + continue; + } + temp = pid[i]; + pid[i] = pid[r]; + pid[r] = temp; + } + + m_MessageHandler({IFilter::Message::Type::Info, "Adjusting Feature Ids Array...."}); + // Now adjust all the Feature Id values for each Voxel + for(usize i = 0; i < totalPoints; ++i) + { + cellParentIds[i] = pid[cellParentIds[i]]; + m_FeatureParentIds[featureIds[i]] = cellParentIds[i]; + } + } + + return {}; +} + +// ----------------------------------------------------------------------------- +// +// ----------------------------------------------------------------------------- +int32 MergeColonies::getSeed(int32 newFid) +{ + usize numFeatures = m_FeaturePhases.getNumberOfTuples(); + + std::mt19937 generator(m_InputValues->SeedValue); // Standard mersenne_twister_engine seeded + std::uniform_real_distribution distribution(0, 1); + int32 seed = -1; + int32 randFeature = 0; + + // Precalculate some constants + usize totalFMinus1 = numFeatures - 1; + + usize counter = 0; + randFeature = int32(distribution(generator) * float32(totalFMinus1)); + while(seed == -1 && counter < numFeatures) + { + if(randFeature > totalFMinus1) + { + randFeature = randFeature - numFeatures; + } + if(m_FeatureParentIds[randFeature] == -1) + { + seed = randFeature; + } + randFeature++; + counter++; + } + if(seed >= 0) + { + m_FeatureParentIds[seed] = newFid; + std::vector tDims(1, newFid + 1); + m_DataStructure.getDataRefAs(m_InputValues->CellFeatureAMPath).resizeTuples(tDims); + } + return seed; +} + +// ----------------------------------------------------------------------------- +bool MergeColonies::determineGrouping(int32 referenceFeature, int32 neighborFeature, int32 newFid) +{ + float64 w = std::numeric_limits::max(); + bool colony = false; + + if(m_FeatureParentIds[neighborFeature] == -1 && m_FeaturePhases[referenceFeature] > 0 && m_FeaturePhases[neighborFeature] > 0) + { + usize avgQuatIdx = referenceFeature * 4; + QuatD q1(m_AvgQuats[avgQuatIdx], m_AvgQuats[avgQuatIdx + 1], m_AvgQuats[avgQuatIdx + 2], m_AvgQuats[avgQuatIdx + 3]); + avgQuatIdx = neighborFeature * 4; + QuatD q2(m_AvgQuats[avgQuatIdx], m_AvgQuats[avgQuatIdx + 1], m_AvgQuats[avgQuatIdx + 2], m_AvgQuats[avgQuatIdx + 3]); + + uint32 phase1 = m_CrystalStructures[m_FeaturePhases[referenceFeature]]; + uint32 phase2 = m_CrystalStructures[m_FeaturePhases[neighborFeature]]; + if(phase1 == phase2 && (phase1 == EbsdLib::CrystalStructure::Hexagonal_High)) + { + OrientationD ax = m_OrientationOps[phase1]->calculateMisorientation(q1, q2); + + auto rod = OrientationTransformation::ax2ro(ax); + rod = m_OrientationOps[phase1]->getMDFFZRod(rod); + ax = OrientationTransformation::ro2ax(rod); + + w = ax[3] * (Constants::k_180OverPiD); + float angdiff1 = std::fabs(w - 10.53f); + float axisdiff1 = std::acos(/*std::fabs(n1) * 0.0000f + std::fabs(n2) * 0.0000f +*/ std::fabs(ax[2]) /* * 1.0000f */); + if(angdiff1 < m_AngleTolerance && axisdiff1 < m_AxisToleranceRad) + { + colony = true; + } + float angdiff2 = std::fabs(w - 90.00f); + float axisdiff2 = std::acos(std::fabs(ax[0]) * 0.9958f + std::fabs(ax[1]) * 0.0917f /* + std::fabs(n3) * 0.0000f */); + if(angdiff2 < m_AngleTolerance && axisdiff2 < m_AxisToleranceRad) + { + colony = true; + } + float angdiff3 = std::fabs(w - 60.00f); + float axisdiff3 = std::acos(std::fabs(ax[0]) /* * 1.0000f + std::fabs(n2) * 0.0000f + std::fabs(n3) * 0.0000f*/); + if(angdiff3 < m_AngleTolerance && axisdiff3 < m_AxisToleranceRad) + { + colony = true; + } + float angdiff4 = std::fabs(w - 60.83f); + float axisdiff4 = std::acos(std::fabs(ax[0]) * 0.9834f + std::fabs(ax[1]) * 0.0905f + std::fabs(ax[2]) * 0.1570f); + if(angdiff4 < m_AngleTolerance && axisdiff4 < m_AxisToleranceRad) + { + colony = true; + } + float angdiff5 = std::fabs(w - 63.26f); + float axisdiff5 = std::acos(std::fabs(ax[0]) * 0.9549f /* + std::fabs(n2) * 0.0000f */ + std::fabs(ax[2]) * 0.2969f); + if(angdiff5 < m_AngleTolerance && axisdiff5 < m_AxisToleranceRad) + { + colony = true; + } + if(colony) + { + m_FeatureParentIds[neighborFeature] = newFid; + return true; + } + } + else if(EbsdLib::CrystalStructure::Cubic_High == phase2 && EbsdLib::CrystalStructure::Hexagonal_High == phase1) + { + colony = check_for_burgers(q2, q1, m_AngleTolerance); + if(colony) + { + m_FeatureParentIds[neighborFeature] = newFid; + return true; + } + } + else if(EbsdLib::CrystalStructure::Cubic_High == phase1 && EbsdLib::CrystalStructure::Hexagonal_High == phase2) + { + colony = check_for_burgers(q1, q2, m_AngleTolerance); + if(colony) + { + m_FeatureParentIds[neighborFeature] = newFid; + return true; + } + } + } + return false; +} + +// ----------------------------------------------------------------------------- +// +// ----------------------------------------------------------------------------- +void MergeColonies::characterize_colonies() +{ +} diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/MergeColonies.hpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/MergeColonies.hpp new file mode 100644 index 0000000000..24046d85ae --- /dev/null +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/MergeColonies.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include "OrientationAnalysis/Filters/Algorithms/GroupFeatures.hpp" +#include "OrientationAnalysis/OrientationAnalysis_export.hpp" + +#include "complex/DataStructure/AttributeMatrix.hpp" +#include "complex/DataStructure/DataArray.hpp" +#include "complex/DataStructure/DataPath.hpp" +#include "complex/DataStructure/DataStructure.hpp" +#include "complex/Filter/IFilter.hpp" +#include "complex/Parameters/ArrayCreationParameter.hpp" +#include "complex/Parameters/ArraySelectionParameter.hpp" +#include "complex/Parameters/NumberParameter.hpp" + +#include "EbsdLib/LaueOps/LaueOps.h" + +namespace complex +{ +struct ORIENTATIONANALYSIS_EXPORT MergeColoniesInputValues +{ + float32 AxisTolerance; + float32 AngleTolerance; + DataPath FeaturePhasesPath; + DataPath AvgQuatsPath; + DataPath FeatureIdsPath; + DataPath CellPhasesPath; + DataPath CrystalStructuresPath; + DataPath CellParentIdsPath; + DataPath CellFeatureAMPath; + DataPath FeatureParentIdsPath; + DataPath ActivePath; + uint64 SeedValue; + bool RandomizeParentIds = true; +}; + +/** + * @class ConditionalSetValue + * @brief This filter replaces values in the target array with a user specified value + * where a bool mask array specifies. + */ + +class ORIENTATIONANALYSIS_EXPORT MergeColonies : public GroupFeatures +{ + using LaueOpsShPtrType = std::shared_ptr; + using LaueOpsContainer = std::vector; + +public: + MergeColonies(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, MergeColoniesInputValues* inputValues, + GroupFeaturesInputValues* groupInputValues); + ~MergeColonies() noexcept; + + MergeColonies(const MergeColonies&) = delete; + MergeColonies(MergeColonies&&) noexcept = delete; + MergeColonies& operator=(const MergeColonies&) = delete; + MergeColonies& operator=(MergeColonies&&) noexcept = delete; + + Result<> operator()(); + + const std::atomic_bool& getCancel(); + + int getSeed(int32 newFid) override; + bool determineGrouping(int32 referenceFeature, int32 neighborFeature, int32 newFid) override; + void characterize_colonies(); + +private: + DataStructure& m_DataStructure; + const MergeColoniesInputValues* m_InputValues = nullptr; + const std::atomic_bool& m_ShouldCancel; + const IFilter::MessageHandler& m_MessageHandler; + LaueOpsContainer m_OrientationOps; + Int32Array& m_FeatureParentIds; + Int32Array& m_FeaturePhases; + Float32Array& m_AvgQuats; + UInt32Array& m_CrystalStructures; + float32 m_AxisToleranceRad = 0.0; + float32 m_AngleTolerance = 1.0; +}; +} // namespace complex diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/MergeColoniesFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/MergeColoniesFilter.cpp new file mode 100644 index 0000000000..2a9014b1a8 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/MergeColoniesFilter.cpp @@ -0,0 +1,190 @@ +#include "MergeColoniesFilter.hpp" + +#include "OrientationAnalysis/Filters/Algorithms/MergeColonies.hpp" + +#include "complex/DataStructure/DataPath.hpp" +#include "complex/Filter/Actions/CreateArrayAction.hpp" +#include "complex/Filter/Actions/CreateAttributeMatrixAction.hpp" +#include "complex/Parameters/ArraySelectionParameter.hpp" +#include "complex/Parameters/BoolParameter.hpp" +#include "complex/Parameters/DataGroupCreationParameter.hpp" +#include "complex/Parameters/DataObjectNameParameter.hpp" +#include "complex/Parameters/NeighborListSelectionParameter.hpp" +#include "complex/Parameters/NumberParameter.hpp" + +#include + +using namespace complex; + +namespace complex +{ +//------------------------------------------------------------------------------ +std::string MergeColoniesFilter::name() const +{ + return FilterTraits::name.str(); +} + +//------------------------------------------------------------------------------ +std::string MergeColoniesFilter::className() const +{ + return FilterTraits::className; +} + +//------------------------------------------------------------------------------ +Uuid MergeColoniesFilter::uuid() const +{ + return FilterTraits::uuid; +} + +//------------------------------------------------------------------------------ +std::string MergeColoniesFilter::humanName() const +{ + return "Merge Colonies"; +} + +//------------------------------------------------------------------------------ +std::vector MergeColoniesFilter::defaultTags() const +{ + return {"Reconstruction", "Grouping"}; +} + +//------------------------------------------------------------------------------ +Parameters MergeColoniesFilter::parameters() const +{ + Parameters params; + + // Create the parameter descriptors that are needed for this filter + params.insertSeparator(Parameters::Separator{"Optional Variables"}); + params.insertLinkableParameter(std::make_unique(k_UseSeed_Key, "Use Seed for Random Generation", "When true the user will be able to put in a seed for random generation", false)); + params.insert(std::make_unique>(k_SeedValue_Key, "Seed", "The seed fed into the random generator", std::mt19937::default_seed)); + + params.insertSeparator(Parameters::Separator{"Input Parameters"}); + params.insertLinkableParameter(std::make_unique(k_UseNonContiguousNeighbors_Key, "Use Non-Contiguous Neighbors", "", false)); + params.insert(std::make_unique(k_AxisTolerance_Key, "Axis Tolerance (Degrees)", "", 0.0f)); + params.insert(std::make_unique(k_AngleTolerance_Key, "Angle Tolerance (Degrees)", "", 0.0f)); + + params.insertSeparator(Parameters::Separator{"Required Feature Data"}); + params.insert(std::make_unique(k_NonContiguousNeighborListArrayPath_Key, "Non-Contiguous Neighbor List", "", DataPath{}, + NeighborListSelectionParameter::AllowedTypes{DataType::int32})); + params.insert(std::make_unique(k_ContiguousNeighborListArrayPath_Key, "Contiguous Neighbor List", "", DataPath{}, + NeighborListSelectionParameter::AllowedTypes{DataType::int32})); + params.insert(std::make_unique(k_FeaturePhasesArrayPath_Key, "Feature Phases", "", DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::int32}, + ArraySelectionParameter::AllowedComponentShapes{{1}})); + params.insert(std::make_unique(k_AvgQuatsArrayPath_Key, "Average Quaternions", "", DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::float32}, + ArraySelectionParameter::AllowedComponentShapes{{4}})); + + params.insertSeparator(Parameters::Separator{"Required Element Data"}); + params.insert(std::make_unique(k_FeatureIdsArrayPath_Key, "Feature Ids", "", DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::int32}, + ArraySelectionParameter::AllowedComponentShapes{{1}})); + params.insert(std::make_unique(k_CellPhasesArrayPath_Key, "Cell Phases", "", DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::int32}, + ArraySelectionParameter::AllowedComponentShapes{{1}})); + + params.insertSeparator(Parameters::Separator{"Required Ensemble Data"}); + params.insert(std::make_unique(k_CrystalStructuresArrayPath_Key, "Crystal Structures", "", DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::uint32}, + ArraySelectionParameter::AllowedComponentShapes{{1}})); + + params.insertSeparator(Parameters::Separator{"Created Element Data"}); + params.insert(std::make_unique(k_CellParentIdsArrayName_Key, "Cell Parent Ids", "", "Cell Parent Ids")); + + params.insertSeparator(Parameters::Separator{"Created Feature Data"}); + params.insert(std::make_unique(k_NewCellFeatureAttributeMatrixName_Key, "Feature Attribute Matrix", "", DataPath{})); + params.insert(std::make_unique(k_FeatureParentIdsArrayName_Key, "Feature Parent Ids", "", "Feature Parent Ids")); + params.insert(std::make_unique(k_ActiveArrayName_Key, "Active", "", "Active Features")); + + // 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 MergeColoniesFilter::clone() const +{ + return std::make_unique(); +} + +//------------------------------------------------------------------------------ +IFilter::PreflightResult MergeColoniesFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel) const +{ + auto pFeaturePhasesPathValue = filterArgs.value(k_FeaturePhasesArrayPath_Key); + auto pAvgQuatsPathValue = filterArgs.value(k_AvgQuatsArrayPath_Key); + auto pFeatureIdsPathValue = filterArgs.value(k_FeatureIdsArrayPath_Key); + auto pCellPhasesPathValue = filterArgs.value(k_CellPhasesArrayPath_Key); + auto pCrystalStructuresPathValue = filterArgs.value(k_CrystalStructuresArrayPath_Key); + auto pCellParentIdsNameValue = filterArgs.value(k_CellParentIdsArrayName_Key); + auto pCellFeatureAMPathValue = filterArgs.value(k_NewCellFeatureAttributeMatrixName_Key); + auto pFeatureParentIdsNameValue = filterArgs.value(k_FeatureParentIdsArrayName_Key); + auto pActiveNameValue = filterArgs.value(k_ActiveArrayName_Key); + + PreflightResult preflightResult; + complex::Result resultOutputActions; + std::vector preflightUpdatedValues; + + // Create the CreateArray action and add it to the resultOutputActions object + { + auto action = std::make_unique(pCellFeatureAMPathValue, std::vector{0}); + resultOutputActions.value().appendAction(std::move(action)); + } + + // Create the CreateArray action and add it to the resultOutputActions object + { + auto action = std::make_unique(DataType::boolean, std::vector{0}, std::vector{1}, pCellFeatureAMPathValue.createChildPath(pActiveNameValue)); + resultOutputActions.value().appendAction(std::move(action)); + } + + // Create the CreateArray action and add it to the resultOutputActions object + { + std::vector tupDims = dataStructure.getDataAs(pFeaturePhasesPathValue)->getTupleShape(); + DataPath featureParentIdsPath = pFeaturePhasesPathValue.getParent().createChildPath(pFeatureParentIdsNameValue); + auto action = std::make_unique(DataType::int32, tupDims, std::vector{1}, featureParentIdsPath); + resultOutputActions.value().appendAction(std::move(action)); + } + + // Create the CreateArray action and add it to the resultOutputActions object + { + std::vector tupDims = dataStructure.getDataAs(pFeatureIdsPathValue)->getTupleShape(); + DataPath cellParentIdsPath = pFeatureIdsPathValue.getParent().createChildPath(pCellParentIdsNameValue); + auto action = std::make_unique(DataType::int32, tupDims, std::vector{1}, cellParentIdsPath); + resultOutputActions.value().appendAction(std::move(action)); + } + + // Return both the resultOutputActions and the preflightUpdatedValues via std::move() + return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; +} + +//------------------------------------------------------------------------------ +Result<> MergeColoniesFilter::executeImpl(DataStructure& dataStructure, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, + const std::atomic_bool& shouldCancel) const +{ + MergeColoniesInputValues inputValues; + + 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()); + } + + inputValues.SeedValue = seed; + inputValues.AxisTolerance = filterArgs.value(k_AxisTolerance_Key); + inputValues.AngleTolerance = filterArgs.value(k_AngleTolerance_Key); + inputValues.FeaturePhasesPath = filterArgs.value(k_FeaturePhasesArrayPath_Key); + inputValues.AvgQuatsPath = filterArgs.value(k_AvgQuatsArrayPath_Key); + inputValues.FeatureIdsPath = filterArgs.value(k_FeatureIdsArrayPath_Key); + inputValues.CellPhasesPath = filterArgs.value(k_CellPhasesArrayPath_Key); + inputValues.CrystalStructuresPath = filterArgs.value(k_CrystalStructuresArrayPath_Key); + inputValues.CellParentIdsPath = inputValues.FeatureIdsPath.getParent().createChildPath(filterArgs.value(k_CellParentIdsArrayName_Key)); + inputValues.CellFeatureAMPath = filterArgs.value(k_NewCellFeatureAttributeMatrixName_Key); + inputValues.FeatureParentIdsPath = inputValues.FeaturePhasesPath.getParent().createChildPath(filterArgs.value(k_FeatureParentIdsArrayName_Key)); + inputValues.ActivePath = inputValues.CellFeatureAMPath.createChildPath(filterArgs.value(k_ActiveArrayName_Key)); + + GroupFeaturesInputValues gpInputValues; + + gpInputValues.UseNonContiguousNeighbors = filterArgs.value(k_UseNonContiguousNeighbors_Key); + gpInputValues.NonContiguousNeighborListArrayPath = filterArgs.value(k_NonContiguousNeighborListArrayPath_Key); + gpInputValues.ContiguousNeighborListArrayPath = filterArgs.value(k_ContiguousNeighborListArrayPath_Key); + + return MergeColonies(dataStructure, messageHandler, shouldCancel, &inputValues, &gpInputValues)(); +} +} // namespace complex diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/MergeColoniesFilter.hpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/MergeColoniesFilter.hpp new file mode 100644 index 0000000000..4631dfbb6c --- /dev/null +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/MergeColoniesFilter.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include "OrientationAnalysis/OrientationAnalysis_export.hpp" + +#include "complex/Filter/FilterTraits.hpp" +#include "complex/Filter/IFilter.hpp" + +namespace complex +{ +/** + * @class MergeColoniesFilter + * @brief This filter will .... + */ +class ORIENTATIONANALYSIS_EXPORT MergeColoniesFilter : public IFilter +{ +public: + MergeColoniesFilter() = default; + ~MergeColoniesFilter() noexcept override = default; + + MergeColoniesFilter(const MergeColoniesFilter&) = delete; + MergeColoniesFilter(MergeColoniesFilter&&) noexcept = delete; + + MergeColoniesFilter& operator=(const MergeColoniesFilter&) = delete; + MergeColoniesFilter& operator=(MergeColoniesFilter&&) noexcept = delete; + + // Parameter Keys + static inline constexpr StringLiteral k_UseNonContiguousNeighbors_Key = "use_non_contiguous_neighbors"; + static inline constexpr StringLiteral k_NonContiguousNeighborListArrayPath_Key = "non_contiguous_neighbor_list_array_path"; + static inline constexpr StringLiteral k_ContiguousNeighborListArrayPath_Key = "contiguous_neighbor_list_array_path"; + static inline constexpr StringLiteral k_AxisTolerance_Key = "axis_tolerance"; + static inline constexpr StringLiteral k_AngleTolerance_Key = "angle_tolerance"; + static inline constexpr StringLiteral k_FeaturePhasesArrayPath_Key = "feature_phases_array_path"; + static inline constexpr StringLiteral k_AvgQuatsArrayPath_Key = "avg_quats_array_path"; + static inline constexpr StringLiteral k_FeatureIdsArrayPath_Key = "feature_ids_array_path"; + static inline constexpr StringLiteral k_CellPhasesArrayPath_Key = "cell_phases_array_path"; + static inline constexpr StringLiteral k_CrystalStructuresArrayPath_Key = "crystal_structures_array_path"; + static inline constexpr StringLiteral k_CellParentIdsArrayName_Key = "cell_parent_ids_array_name"; + static inline constexpr StringLiteral k_NewCellFeatureAttributeMatrixName_Key = "new_cell_feature_attribute_matrix_name"; + static inline constexpr StringLiteral k_FeatureParentIdsArrayName_Key = "feature_parent_ids_array_name"; + static inline constexpr StringLiteral k_ActiveArrayName_Key = "active_array_name"; + static inline constexpr StringLiteral k_UseSeed_Key = "use_seed"; + static inline constexpr StringLiteral k_SeedValue_Key = "seed_value"; + + /** + * @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 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 ds 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 + * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + */ + PreflightResult preflightImpl(const DataStructure& ds, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel) 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 ds 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 + * @return Returns a Result object with error or warning values if any of those occurred during execution of this function + */ + Result<> executeImpl(DataStructure& data, const Arguments& filterArgs, const PipelineFilter* pipelineNode, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel) const override; +}; +} // namespace complex + +COMPLEX_DEF_FILTER_TRAITS(complex, MergeColoniesFilter, "7e3dbc15-51a3-482c-97c2-f82f7af685bf"); +/* LEGACY UUID FOR THIS FILTER 2c4a6d83-6a1b-56d8-9f65-9453b28845b9 */ diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp index 0ac13383e6..508ff3c838 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/OrientationAnalysisLegacyUUIDMapping.hpp @@ -69,6 +69,7 @@ #include "OrientationAnalysis/Filters/FindSlipTransmissionMetricsFilter.hpp" #include "OrientationAnalysis/Filters/WriteStatsGenOdfAngleFileFilter.hpp" #include "OrientationAnalysis/Filters/INLWriterFilter.hpp" +#include "OrientationAnalysis/Filters/MergeColoniesFilter.hpp" // @@__HEADER__TOKEN__DO__NOT__DELETE__@@ #include @@ -108,6 +109,7 @@ namespace complex {complex::Uuid::FromString("bf7036d8-25bd-540e-b6de-3a5ab0e42c5f").value(), complex::FilterTraits::uuid}, // FindAvgOrientations {complex::Uuid::FromString("bff6be19-1219-5876-8838-1574ad29d965").value(), complex::FilterTraits::uuid}, // CAxisSegmentFeatures {complex::Uuid::FromString("c5a9a96c-7570-5279-b383-cc25ebae0046").value(), complex::FilterTraits::uuid}, // FindAvgCAxes + {complex::Uuid::FromString("2c4a6d83-6a1b-56d8-9f65-9453b28845b9").value(), complex::FilterTraits::uuid}, // MergeColonies {complex::Uuid::FromString("c9af506e-9ea1-5ff5-a882-fa561def5f52").value(), complex::FilterTraits::uuid}, // MergeTwins {complex::Uuid::FromString("d1df969c-0428-53c3-b61d-99ea2bb6da28").value(), complex::FilterTraits::uuid}, // ReadCtfData {complex::Uuid::FromString("e5629880-98c4-5656-82b8-c9fe2b9744de").value(), complex::FilterTraits::uuid}, // ConvertOrientations diff --git a/src/Plugins/OrientationAnalysis/test/CMakeLists.txt b/src/Plugins/OrientationAnalysis/test/CMakeLists.txt index 3d96ac3716..a92e73b9d0 100644 --- a/src/Plugins/OrientationAnalysis/test/CMakeLists.txt +++ b/src/Plugins/OrientationAnalysis/test/CMakeLists.txt @@ -44,6 +44,7 @@ set(${PLUGIN_NAME}UnitTest_SRCS ImportH5EspritDataTest.cpp ImportH5OimDataTest.cpp INLWriterTest.cpp + MergeColoniesTest.cpp MergeTwinsTest.cpp NeighborOrientationCorrelationTest.cpp ReadAngDataTest.cpp diff --git a/src/Plugins/OrientationAnalysis/test/MergeColoniesTest.cpp b/src/Plugins/OrientationAnalysis/test/MergeColoniesTest.cpp new file mode 100644 index 0000000000..8d03c7edbb --- /dev/null +++ b/src/Plugins/OrientationAnalysis/test/MergeColoniesTest.cpp @@ -0,0 +1,41 @@ +#include + +#include "complex/Parameters/ArrayCreationParameter.hpp" +#include "complex/Parameters/BoolParameter.hpp" + +#include "OrientationAnalysis/Filters/MergeColoniesFilter.hpp" +#include "OrientationAnalysis/OrientationAnalysis_test_dirs.hpp" + +using namespace complex; + +TEST_CASE("OrientationAnalysis::MergeColoniesFilter: Valid Filter Execution", "[ComplexCore][MergeColoniesFilter]") +{ + // Instantiate the filter, a DataStructure object and an Arguments Object + MergeColoniesFilter filter; + DataStructure ds; + Arguments args; + + // Create default Parameters for the filter. + args.insertOrAssign(MergeColoniesFilter::k_UseNonContiguousNeighbors_Key, std::make_any(false)); + args.insertOrAssign(MergeColoniesFilter::k_NonContiguousNeighborListArrayPath_Key, std::make_any(DataPath{})); + args.insertOrAssign(MergeColoniesFilter::k_ContiguousNeighborListArrayPath_Key, std::make_any(DataPath{})); + args.insertOrAssign(MergeColoniesFilter::k_AxisTolerance_Key, std::make_any(1.23345f)); + args.insertOrAssign(MergeColoniesFilter::k_AngleTolerance_Key, std::make_any(1.23345f)); + args.insertOrAssign(MergeColoniesFilter::k_FeaturePhasesArrayPath_Key, std::make_any(DataPath{})); + args.insertOrAssign(MergeColoniesFilter::k_AvgQuatsArrayPath_Key, std::make_any(DataPath{})); + args.insertOrAssign(MergeColoniesFilter::k_FeatureIdsArrayPath_Key, std::make_any(DataPath{})); + args.insertOrAssign(MergeColoniesFilter::k_CellPhasesArrayPath_Key, std::make_any(DataPath{})); + args.insertOrAssign(MergeColoniesFilter::k_CrystalStructuresArrayPath_Key, std::make_any(DataPath{})); + args.insertOrAssign(MergeColoniesFilter::k_CellParentIdsArrayName_Key, std::make_any(DataPath{})); + args.insertOrAssign(MergeColoniesFilter::k_NewCellFeatureAttributeMatrixName_Key, std::make_any(DataPath{})); + args.insertOrAssign(MergeColoniesFilter::k_FeatureParentIdsArrayName_Key, std::make_any(DataPath{})); + args.insertOrAssign(MergeColoniesFilter::k_ActiveArrayName_Key, std::make_any(DataPath{})); + + // Preflight the filter and check result + auto preflightResult = filter.preflight(ds, args); + REQUIRE(preflightResult.outputActions.valid()); + + // Execute the filter and check the result + auto executeResult = filter.execute(ds, args); + REQUIRE(executeResult.result.valid()); +}