diff --git a/src/Plugins/OrientationAnalysis/docs/BadDataNeighborOrientationCheckFilter.md b/src/Plugins/OrientationAnalysis/docs/BadDataNeighborOrientationCheckFilter.md index a79b203db1..c2030698d3 100644 --- a/src/Plugins/OrientationAnalysis/docs/BadDataNeighborOrientationCheckFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/BadDataNeighborOrientationCheckFilter.md @@ -35,12 +35,18 @@ changing voxels. ### 2D Versus 3D Note If the user is processing a 2D data set, **none** of the voxels can have 6 neighbors -since there are no neighbors is the +-Z directions. +since there are no neighbors in the +/-Z directions. ### Warning - Data Modification Only the *Mask* value defining the cell as *good* or *bad* is changed. No other cell level array is modified. +### Memory Considerations + +The filter allocates a temporary `int32` neighbor-count array sized to the total voxel count +(4 bytes per voxel). For a 1-billion-voxel dataset, that is approximately 4 GB of additional +working memory during execution. This memory is released when the filter finishes. + ## Example Data | Example Input Image | Example Output Image | @@ -55,8 +61,12 @@ From the above before and after images you can see that this filter can help mod ## Example Pipelines + (02) Small IN100 Full Reconstruction -+ INL Export -+ 04_Steiner Compact + +## Related Filters + +- [Fill Bad Data](../SimplnxCore/FillBadDataFilter.md) — fills voxels still marked bad after this filter runs (or as a standalone alternative when no orientation data is available). +- [Multi-Threshold Objects](../SimplnxCore/MultiThresholdObjectsFilter.md) — typical upstream filter that generates the initial *Mask* array (e.g., from `Confidence Index` and `Image Quality`). +- [Replace Element Attributes with Neighbor Values](../SimplnxCore/ReplaceElementAttributesWithNeighborValuesFilter.md) — alternative cleanup approach that copies attribute values from neighboring cells rather than flipping a mask. ## License & Copyright diff --git a/src/Plugins/OrientationAnalysis/docs/ComputeFeatureFaceMisorientationFilter.md b/src/Plugins/OrientationAnalysis/docs/ComputeFeatureFaceMisorientationFilter.md index 6b4440da0d..e5cfd55506 100644 --- a/src/Plugins/OrientationAnalysis/docs/ComputeFeatureFaceMisorientationFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/ComputeFeatureFaceMisorientationFilter.md @@ -6,7 +6,13 @@ Processing (Crystallography) ## Description -This **Filter** generates a 3 component vector for each **Triangle** in a **Triangle Geometry** that is the axis-angle of the misorientation associated with the **Features** that lie on either side of the **Triangle**. The axis is normalized, so if the magnitude of the vector is viewed, it will be the *misorientation angle* in degrees. +This **Filter** generates a 3 component vector for each **Triangle** in a **Triangle Geometry** that is the axis-angle of the misorientation associated with the **Features** that lie on either side of the **Triangle**. The axis is normalized, so if the magnitude of the vector is viewed, it will be the *misorientation angle* in degrees. This filter works on all valid crystal-structures/laue classes. + +If you want to get the "raw" (un-normalized) axis-angle of the misorientation, enable "Store Full Axis Angle" and then (optionally) modify the "Axis Angle Array Name" parameter. The actual axis-angle is stored in a 4 component DataArray with the format as follows: + +```text +{{x_axis, y_axis, z_axis, angle}, ...} +``` % Auto generated parameter table will be inserted here diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheck.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheck.cpp index 5da4ee33f2..a826c05c56 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheck.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheck.cpp @@ -33,10 +33,15 @@ const std::atomic_bool& BadDataNeighborOrientationCheck::getCancel() // ----------------------------------------------------------------------------- Result<> BadDataNeighborOrientationCheck::operator()() { - const float misorientationTolerance = m_InputValues->MisorientationTolerance * numbers::pi_v / 180.0f; + // Compute the tolerance in double precision: numbers::pi_v is the closest float to true pi, which is + // slightly *larger* than true pi; converting via float makes the radian tolerance ~5e-9 rad larger than the + // mathematically true k*pi/180. For boundary-exact misorientations (e.g., test fixtures landing on exactly the + // user-supplied tolerance), the float-converted tolerance can incorrectly include cases that should fail strict <. + // Using double-pi makes the conversion faithful and the strict < tolerance comparison match the analytical oracle. + const double misorientationTolerance = static_cast(m_InputValues->MisorientationTolerance) * numbers::pi_v / 180.0; - const auto* imageGeomPtr = m_DataStructure.getDataAs(m_InputValues->ImageGeomPath); - SizeVec3 udims = imageGeomPtr->getDimensions(); + const auto& imageGeom = m_DataStructure.getDataRefAs(m_InputValues->ImageGeomPath); + SizeVec3 udims = imageGeom.getDimensions(); const auto& cellPhases = m_DataStructure.getDataRefAs(m_InputValues->CellPhasesArrayPath); const auto& quats = m_DataStructure.getDataRefAs(m_InputValues->QuatsArrayPath); const auto& crystalStructures = m_DataStructure.getDataRefAs(m_InputValues->CrystalStructuresArrayPath); @@ -59,20 +64,46 @@ Result<> BadDataNeighborOrientationCheck::operator()() static_cast(udims[2]), }; + // VoxelNeighbors::k_FaceNeighborCount = 6 is the maximum possible face-neighbor count. + // computeValidFaceNeighbors() runtime-skips +/-Z neighbors when dims[2] == 1 (2D images), so this + // 3D-typed array correctly handles 2D images without any change here. constexpr FaceNeighborType k_NumFaceNeighbors = VoxelNeighbors::k_FaceNeighborCount; const std::array neighborVoxelIndexOffsets = initializeFaceNeighborOffsets(dims); constexpr std::array faceNeighborInternalIdx = initializeFaceNeighborInternalIdx(); const std::vector orientationOps = ebsdlib::LaueOps::GetAllOrientationOps(); + // Validate that every entry in the CrystalStructures ensemble array is a valid Laue-group index + // (< orientationOps.size()). Catches malformed inputs such as a legacy CreateEnsembleInfo sentinel + // value (999) at ensemble index 0 before they cause an out-of-bounds dereference in the per-voxel + // loop below. The UnknownCrystalStructure value is explicitly allowed as a sentinel; voxels whose + // phase resolves to it will be skipped by the cellPhases > 0 guard. CrystalStructures is typically + // tiny (2-4 entries), so the cost is negligible. + const usize numOrientationOps = orientationOps.size(); + for(usize i = 0; i < crystalStructures.getSize(); ++i) + { + if(crystalStructures[i] >= numOrientationOps && crystalStructures[i] != static_cast(ebsdlib::CrystalStructure::UnknownCrystalStructure)) + { + return MakeErrorResult( + -54901, fmt::format("Crystal structure at ensemble index {} has value {}, which is not a valid Laue-group index. Valid range is [0, {}).", i, crystalStructures[i], numOrientationOps)); + } + } + + // Per-voxel running count of within-tolerance face-neighbors. Allocated proportional to the + // input geometry size: 4 bytes per voxel (~4 GB for a 1B-voxel dataset). Cannot be in-place + // on the mask array because the algorithm needs to distinguish "newly flipped" from "still bad". std::vector neighborCount(totalPoints, 0); MessageHelper messageHelper(m_MessageHandler); ThrottledMessenger throttledMessenger = messageHelper.createThrottledMessenger(); // Loop over every point finding the number of neighbors that fall within the // user defined angle tolerance. - for(int64 voxelIndex = 0; voxelIndex < totalPoints; voxelIndex++) + for(usize voxelIndex = 0; voxelIndex < totalPoints; voxelIndex++) { + if(m_ShouldCancel) + { + return {}; + } throttledMessenger.sendThrottledMessage([&] { return fmt::format("Processing Data {:.2f}% completed", CalculatePercentComplete(voxelIndex, totalPoints)); }); // If the mask was set to false, then we check this voxel if(!maskCompare->isTrue(voxelIndex)) @@ -80,11 +111,19 @@ Result<> BadDataNeighborOrientationCheck::operator()() // We precalculate the positive voxel quaternion and laue class here to prevent reading and recalculating it for each face below ebsdlib::QuatD quat1(quats[voxelIndex * 4], quats[voxelIndex * 4 + 1], quats[voxelIndex * 4 + 2], quats[voxelIndex * 4 + 3]); quat1.positiveOrientation(); - const uint32 laueClass1 = crystalStructures[cellPhases[voxelIndex]]; + const uint32 laueClassIndex = crystalStructures[cellPhases[voxelIndex]]; + // Defensive: skip voxels whose phase resolves to an out-of-range Laue index (e.g., the + // UnknownCrystalStructure sentinel allowed by the validation above). Without this, the + // orientationOps[laueClassIndex] dereference below would be out-of-bounds. + if(laueClassIndex >= numOrientationOps) + { + continue; + } - int64 xIdx = voxelIndex % dims[0]; - int64 yIdx = (voxelIndex / dims[0]) % dims[1]; - int64 zIdx = voxelIndex / (dims[0] * dims[1]); + const int64 voxelIndexI64 = static_cast(voxelIndex); + int64 xIdx = voxelIndexI64 % dims[0]; + int64 yIdx = (voxelIndexI64 / dims[0]) % dims[1]; + int64 zIdx = voxelIndexI64 / (dims[0] * dims[1]); // Loop over the 6 face neighbors of the voxel const std::array isValidFaceNeighbor = computeValidFaceNeighbors(xIdx, yIdx, zIdx, dims); @@ -94,7 +133,7 @@ Result<> BadDataNeighborOrientationCheck::operator()() { continue; } - const int64 neighborPoint = voxelIndex + neighborVoxelIndexOffsets[faceIndex]; + const int64 neighborPoint = voxelIndexI64 + neighborVoxelIndexOffsets[faceIndex]; // Now compare the mask of the neighbor. If the mask is TRUE, i.e., that voxel // did not fail the threshold filter that most likely produced the mask array, @@ -107,7 +146,7 @@ Result<> BadDataNeighborOrientationCheck::operator()() ebsdlib::QuatD quat2(quats[neighborPoint * 4], quats[neighborPoint * 4 + 1], quats[neighborPoint * 4 + 2], quats[neighborPoint * 4 + 3]); quat2.positiveOrientation(); // Compute the Axis_Angle misorientation between those 2 quaternions - ebsdlib::AxisAngleDType axisAngle = orientationOps[laueClass1]->calculateMisorientation(quat1, quat2); + ebsdlib::AxisAngleDType axisAngle = orientationOps[laueClassIndex]->calculateMisorientation(quat1, quat2); // if the angle is less than our tolerance, then we increment the neighbor count // for this voxel if(axisAngle[3] < misorientationTolerance) @@ -120,7 +159,10 @@ Result<> BadDataNeighborOrientationCheck::operator()() } } - constexpr int32 startLevel = 6; + // Convergence loop starts at the maximum possible face-neighbor count (6 in 3D; 2D images + // simply never reach the top levels because no voxel can have count > 4). Tying this to + // k_NumFaceNeighbors keeps the upper bound consistent if VoxelNeighbors ever changes. + constexpr int32 startLevel = static_cast(k_NumFaceNeighbors); int32 currentLevel = startLevel; int32 counter = 0; @@ -128,41 +170,55 @@ Result<> BadDataNeighborOrientationCheck::operator()() // as the user has requested to iteratively flip voxels while(currentLevel >= m_InputValues->NumberOfNeighbors) { + if(m_ShouldCancel) + { + return {}; + } counter = 1; int32 loopNumber = 0; while(counter > 0) { + if(m_ShouldCancel) + { + return {}; + } counter = 0; // Set this while control variable to zero for(usize voxelIndex = 0; voxelIndex < totalPoints; voxelIndex++) { + if(m_ShouldCancel) + { + return {}; + } throttledMessenger.sendThrottledMessage([&] { return fmt::format("Level '{}' of '{}' || Processing Data ('{}') {:.2f}% completed", (startLevel - currentLevel) + 1, startLevel - m_InputValues->NumberOfNeighbors, loopNumber, CalculatePercentComplete(voxelIndex, totalPoints)); }); - // We are comparing the number-of-neighbors of the current voxel, and if it - // is > the current level and the mask is FALSE, then we drop into this - // conditional. The first thing that happens in the conditional is that - // the current voxel's mask value is set to TRUE. + // If the current voxel's neighbor count is >= the current level and the mask is FALSE, + // we flip the voxel to TRUE and recompute its (still-bad) neighbors' counts below. if(neighborCount[voxelIndex] >= currentLevel && !maskCompare->isTrue(voxelIndex)) { - maskCompare->setValue(voxelIndex, true); // the current voxel's mask value is set to TRUE. - counter++; // Increment the `counter` to force the loop to iterate again + maskCompare->setValue(voxelIndex, true); + counter++; // Increment the `counter` to force the loop to iterate again // We precalculate the positive voxel quaternion and laue class here to prevent reading and recalculating it for each face below ebsdlib::QuatD quat1(quats[voxelIndex * 4], quats[voxelIndex * 4 + 1], quats[voxelIndex * 4 + 2], quats[voxelIndex * 4 + 3]); quat1.positiveOrientation(); - const uint32 laueClass1 = crystalStructures[cellPhases[voxelIndex]]; - - // This whole section below is to now look at the neighbor voxels of the - // current voxel that just got flipped to true. This is needed because - // if any of those neighbor's mask was `false`, then its neighbor count - // is now not correct and will be off-by-one. So we run _almost_ the same - // loop code as above but checking the specific neighbors of the current - // voxel. This part should be termed the "Update Neighbor's Neighbor Count" - int64 xIdx = voxelIndex % dims[0]; - int64 yIdx = (voxelIndex / dims[0]) % dims[1]; - int64 zIdx = voxelIndex / (dims[0] * dims[1]); + const uint32 laueClassIndex = crystalStructures[cellPhases[voxelIndex]]; + // Defensive: skip voxels with out-of-range Laue index. See matching guard in pass 1. + if(laueClassIndex >= numOrientationOps) + { + continue; + } + + // "Update Neighbor's Neighbor Count" pass: now that the current voxel just flipped to + // true, every still-bad face neighbor must have its neighborCount incremented by 1 if + // its misorientation to the freshly-flipped voxel is within tolerance. Skipping this + // update would leave the neighbor counts stale and prevent valid cascade flips later. + const int64 voxelIndexI64 = static_cast(voxelIndex); + int64 xIdx = voxelIndexI64 % dims[0]; + int64 yIdx = (voxelIndexI64 / dims[0]) % dims[1]; + int64 zIdx = voxelIndexI64 / (dims[0] * dims[1]); // Loop over the 6 face neighbors of the voxel const std::array isValidFaceNeighbor = computeValidFaceNeighbors(xIdx, yIdx, zIdx, dims); @@ -173,9 +229,9 @@ Result<> BadDataNeighborOrientationCheck::operator()() continue; } - int64 neighborPoint = voxelIndex + neighborVoxelIndexOffsets[faceIndex]; + const int64 neighborPoint = voxelIndexI64 + neighborVoxelIndexOffsets[faceIndex]; - // If the neighbor voxel's mask is false, then .... + // If the neighbor voxel's mask is false, then compute misorientation angle if(!maskCompare->isTrue(neighborPoint)) { // Make sure both cells phase values are identical and valid @@ -184,7 +240,7 @@ Result<> BadDataNeighborOrientationCheck::operator()() ebsdlib::QuatD quat2(quats[neighborPoint * 4], quats[neighborPoint * 4 + 1], quats[neighborPoint * 4 + 2], quats[neighborPoint * 4 + 3]); quat2.positiveOrientation(); // Quaternion Math is not commutative so do not reorder - ebsdlib::AxisAngleDType axisAngle = orientationOps[laueClass1]->calculateMisorientation(quat1, quat2); + ebsdlib::AxisAngleDType axisAngle = orientationOps[laueClassIndex]->calculateMisorientation(quat1, quat2); if(axisAngle[3] < misorientationTolerance) { neighborCount[neighborPoint]++; diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureFaceMisorientation.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureFaceMisorientation.cpp index 0910ea854e..57cb9c3664 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureFaceMisorientation.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureFaceMisorientation.cpp @@ -2,7 +2,6 @@ #include "simplnx/Common/Constants.hpp" #include "simplnx/DataStructure/DataArray.hpp" -#include "simplnx/DataStructure/DataGroup.hpp" #include "simplnx/Utilities/ParallelDataAlgorithm.hpp" #include @@ -17,77 +16,84 @@ using namespace nx::core; * @brief The CalculateFaceMisorientationColorsImpl class implements a threaded algorithm that computes the misorientation * colors for the given list of surface mesh labels */ -class CalculateFaceMisorientationColorsImpl + +class ComputeFeatureMisorientationPerTriangleImpl { - const Int32Array& m_Labels; - const Int32Array& m_Phases; - const Float32Array& m_Quats; + const Int32Array& m_FaceLabels; + const Int32Array& m_FeaturePhases; + const Float32Array& m_FeatureAvgQuats; const UInt32Array& m_CrystalStructures; - Float32Array& m_Colors; - LaueOpsContainer m_OrientationOps; + const std::atomic_bool& m_ShouldCancel; + Float32Array& m_Misorientations; + LaueOpsContainer m_LaueOrientationOps; public: - CalculateFaceMisorientationColorsImpl(const Int32Array& labels, const Int32Array& phases, const Float32Array& quats, const UInt32Array& crystalStructures, Float32Array& colors) - : m_Labels(labels) - , m_Phases(phases) - , m_Quats(quats) + ComputeFeatureMisorientationPerTriangleImpl(const Int32Array& labels, const Int32Array& phases, const Float32Array& quats, const UInt32Array& crystalStructures, const std::atomic_bool& shouldCancel, + Float32Array& output) + : m_FaceLabels(labels) + , m_FeaturePhases(phases) + , m_FeatureAvgQuats(quats) , m_CrystalStructures(crystalStructures) - , m_Colors(colors) + , m_ShouldCancel(shouldCancel) + , m_Misorientations(output) { - m_OrientationOps = ebsdlib::LaueOps::GetAllOrientationOps(); + m_LaueOrientationOps = ebsdlib::LaueOps::GetAllOrientationOps(); } - virtual ~CalculateFaceMisorientationColorsImpl() = default; + virtual ~ComputeFeatureMisorientationPerTriangleImpl() = default; - void generate(usize start, usize end) const + void generate(const usize start, const usize end) const { - int32 feature1 = 0, feature2 = 0, phase1 = 0, phase2 = 0; + // Since our meshes use unified triangles, there are two triangles + // per entry. These are distinguished via the face labels array, + // which contains the feature id of each respective face. Here, the + // first entry in face labels is denoted as "front" and the second "back" + int32 frontFeature = 0, backFeature = 0, frontPhase = 0, backPhase = 0; for(usize i = start; i < end; i++) { - feature1 = m_Labels[2 * i]; - feature2 = m_Labels[2 * i + 1]; - if(feature1 > 0) + if(m_ShouldCancel) { - phase1 = m_Phases[feature1]; + return; + } + + frontFeature = m_FaceLabels[2 * i]; + backFeature = m_FaceLabels[2 * i + 1]; + if(frontFeature > 0) + { + frontPhase = m_FeaturePhases[frontFeature]; } else { - phase1 = 0; + frontPhase = 0; } - if(feature2 > 0) + if(backFeature > 0) { - phase2 = m_Phases[feature2]; + backPhase = m_FeaturePhases[backFeature]; } else { - phase2 = 0; + backPhase = 0; } - if(phase1 > 0 && phase1 == phase2) + // Make sure the crystal structure is a valid laue class + uint32 laueIndex = m_CrystalStructures[frontPhase]; + if(frontPhase > 0 && frontPhase == backPhase && laueIndex < m_LaueOrientationOps.size()) { - if((m_CrystalStructures[phase1] == ebsdlib::CrystalStructure::Hexagonal_High) || (m_CrystalStructures[phase1] == ebsdlib::CrystalStructure::Cubic_High)) - { - float32 quat0 = m_Quats[feature1 * 4]; - float32 quat1 = m_Quats[feature1 * 4 + 1]; - float32 quat2 = m_Quats[feature1 * 4 + 2]; - float32 quat3 = m_Quats[feature1 * 4 + 3]; - ebsdlib::QuatD q1(quat0, quat1, quat2, quat3); - quat0 = m_Quats[feature2 * 4]; - quat1 = m_Quats[feature2 * 4 + 1]; - quat2 = m_Quats[feature2 * 4 + 2]; - quat3 = m_Quats[feature2 * 4 + 3]; - ebsdlib::QuatD q2(quat0, quat1, quat2, quat3); - ebsdlib::AxisAngleDType axisAngle = m_OrientationOps[m_CrystalStructures[phase1]]->calculateMisorientation(q1, q2); - - m_Colors[3 * i + 0] = axisAngle[0] * (axisAngle[3] * nx::core::Constants::k_180OverPiD); - m_Colors[3 * i + 1] = axisAngle[1] * (axisAngle[3] * nx::core::Constants::k_180OverPiD); - m_Colors[3 * i + 2] = axisAngle[2] * (axisAngle[3] * nx::core::Constants::k_180OverPiD); - } + float32 quat0 = m_FeatureAvgQuats[frontFeature * 4]; + float32 quat1 = m_FeatureAvgQuats[frontFeature * 4 + 1]; + float32 quat2 = m_FeatureAvgQuats[frontFeature * 4 + 2]; + float32 quat3 = m_FeatureAvgQuats[frontFeature * 4 + 3]; + ebsdlib::QuatD q1(quat0, quat1, quat2, quat3); + quat0 = m_FeatureAvgQuats[backFeature * 4]; + quat1 = m_FeatureAvgQuats[backFeature * 4 + 1]; + quat2 = m_FeatureAvgQuats[backFeature * 4 + 2]; + quat3 = m_FeatureAvgQuats[backFeature * 4 + 3]; + ebsdlib::QuatD q2(quat0, quat1, quat2, quat3); + ebsdlib::AxisAngleDType axisAngle = m_LaueOrientationOps[laueIndex]->calculateMisorientation(q1, q2); + m_Misorientations.setValue(i, static_cast(axisAngle[3] * Constants::k_180OverPiD)); } else { - m_Colors[3 * i + 0] = 0; - m_Colors[3 * i + 1] = 0; - m_Colors[3 * i + 2] = 0; + m_Misorientations.setValue(i, static_cast(std::nan("0"))); } } } @@ -124,17 +130,16 @@ const std::atomic_bool& ComputeFeatureFaceMisorientation::getCancel() // ----------------------------------------------------------------------------- Result<> ComputeFeatureFaceMisorientation::operator()() { - auto& faceLabels = m_DataStructure.getDataRefAs(m_InputValues->SurfaceMeshFaceLabelsArrayPath); - auto& avgQuats = m_DataStructure.getDataRefAs(m_InputValues->AvgQuatsArrayPath); - auto& phases = m_DataStructure.getDataRefAs(m_InputValues->FeaturePhasesArrayPath); - auto& crystalStructures = m_DataStructure.getDataRefAs(m_InputValues->CrystalStructuresArrayPath); - DataPath faceMisorientationColorsArrayPath = m_InputValues->SurfaceMeshFaceLabelsArrayPath.replaceName(m_InputValues->SurfaceMeshFaceMisorientationColorsArrayName); - auto& faceMisorientationColors = m_DataStructure.getDataRefAs(faceMisorientationColorsArrayPath); - int64 numTriangles = faceLabels.getNumberOfTuples(); + const auto& faceLabels = m_DataStructure.getDataRefAs(m_InputValues->surfaceMeshFaceLabelsArrayPath); + const auto& avgQuats = m_DataStructure.getDataRefAs(m_InputValues->avgQuatsArrayPath); + const auto& phases = m_DataStructure.getDataRefAs(m_InputValues->featurePhasesArrayPath); + const auto& crystalStructures = m_DataStructure.getDataRefAs(m_InputValues->crystalStructuresArrayPath); + auto& misorientations = m_DataStructure.getDataRefAs(m_InputValues->misorientationArrayPath); + const usize numTriangles = faceLabels.getNumberOfTuples(); ParallelDataAlgorithm parallelTask; parallelTask.setRange(0, numTriangles); - parallelTask.execute(CalculateFaceMisorientationColorsImpl(faceLabels, phases, avgQuats, crystalStructures, faceMisorientationColors)); - + parallelTask.setParallelizationEnabled(false); + parallelTask.execute(ComputeFeatureMisorientationPerTriangleImpl(faceLabels, phases, avgQuats, crystalStructures, m_ShouldCancel, misorientations)); return {}; } diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureFaceMisorientation.hpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureFaceMisorientation.hpp index 0f4228fc01..31eb112e51 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureFaceMisorientation.hpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureFaceMisorientation.hpp @@ -5,19 +5,17 @@ #include "simplnx/DataStructure/DataPath.hpp" #include "simplnx/DataStructure/DataStructure.hpp" #include "simplnx/Filter/IFilter.hpp" -#include "simplnx/Parameters/ArrayCreationParameter.hpp" -#include "simplnx/Parameters/ArraySelectionParameter.hpp" namespace nx::core { struct ORIENTATIONANALYSIS_EXPORT ComputeFeatureFaceMisorientationInputValues { - DataPath SurfaceMeshFaceLabelsArrayPath; - DataPath AvgQuatsArrayPath; - DataPath FeaturePhasesArrayPath; - DataPath CrystalStructuresArrayPath; - std::string SurfaceMeshFaceMisorientationColorsArrayName; + DataPath surfaceMeshFaceLabelsArrayPath; + DataPath avgQuatsArrayPath; + DataPath featurePhasesArrayPath; + DataPath crystalStructuresArrayPath; + DataPath misorientationArrayPath; }; /** diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/BadDataNeighborOrientationCheckFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/BadDataNeighborOrientationCheckFilter.cpp index 5f817313bb..c42329cd39 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/BadDataNeighborOrientationCheckFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/BadDataNeighborOrientationCheckFilter.cpp @@ -97,13 +97,13 @@ IFilter::PreflightResult BadDataNeighborOrientationCheckFilter::preflightImpl(co const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { auto pQuatsArrayPathValue = filterArgs.value(k_QuatsArrayPath_Key); - auto pGoodVoxelsArrayPathValue = filterArgs.value(k_MaskArrayPath_Key); + auto pMaskArrayPathValue = filterArgs.value(k_MaskArrayPath_Key); auto pCellPhasesArrayPathValue = filterArgs.value(k_CellPhasesArrayPath_Key); nx::core::Result resultOutputActions; std::vector dataArrayPaths; - dataArrayPaths.push_back(pGoodVoxelsArrayPathValue); + dataArrayPaths.push_back(pMaskArrayPathValue); dataArrayPaths.push_back(pCellPhasesArrayPathValue); dataArrayPaths.push_back(pQuatsArrayPathValue); @@ -115,7 +115,7 @@ IFilter::PreflightResult BadDataNeighborOrientationCheckFilter::preflightImpl(co } resultOutputActions.value().modifiedActions.emplace_back( - DataObjectModification{pGoodVoxelsArrayPathValue, DataObjectModification::ModifiedType::Modified, dataStructure.getData(pGoodVoxelsArrayPathValue)->getDataObjectType()}); + DataObjectModification{pMaskArrayPathValue, DataObjectModification::ModifiedType::Modified, dataStructure.getData(pMaskArrayPathValue)->getDataObjectType()}); // Return both the resultOutputActions and the preflightUpdatedValues via std::move() return {std::move(resultOutputActions)}; diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/BadDataNeighborOrientationCheckFilter.hpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/BadDataNeighborOrientationCheckFilter.hpp index d3b27f8ef0..a64f1516ab 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/BadDataNeighborOrientationCheckFilter.hpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/BadDataNeighborOrientationCheckFilter.hpp @@ -9,7 +9,19 @@ namespace nx::core { /** * @class BadDataNeighborOrientationCheckFilter - * @brief This filter will .... + * @brief Iteratively flips voxels in a Mask array from "bad" (false) to "good" (true) when a + * sufficient number of same-phase face neighbors have similar crystallographic orientations. + * + * The algorithm operates in two passes: + * (1) For each masked-false voxel, count how many of its 6 face neighbors are both masked-true + * and within the user-supplied misorientation tolerance (computed per the appropriate + * Laue-group symmetry). + * (2) Iterate from currentLevel = 6 down to the user-supplied NumberOfNeighbors, flipping every + * bad voxel whose count meets the current level and updating its still-bad neighbors' counts + * after the flip. The iteration produces a flood-fill behavior across voxels that pass the + * tolerance check, terminating when no further flips occur at the user-supplied lower bound. + * + * Phase mismatches and background voxels (phase <= 0) are skipped. */ class ORIENTATIONANALYSIS_EXPORT BadDataNeighborOrientationCheckFilter : public IFilter { diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureFaceMisorientationFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureFaceMisorientationFilter.cpp index 178b3d22a7..d00929f831 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureFaceMisorientationFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureFaceMisorientationFilter.cpp @@ -6,10 +6,9 @@ #include "simplnx/DataStructure/DataPath.hpp" #include "simplnx/Filter/Actions/CreateArrayAction.hpp" #include "simplnx/Parameters/ArraySelectionParameter.hpp" - -#include "simplnx/Utilities/SIMPLConversion.hpp" - +#include "simplnx/Parameters/BoolParameter.hpp" #include "simplnx/Parameters/DataObjectNameParameter.hpp" +#include "simplnx/Utilities/SIMPLConversion.hpp" using namespace nx::core; @@ -63,8 +62,9 @@ Parameters ComputeFeatureFaceMisorientationFilter::parameters() const params.insert(std::make_unique(k_CrystalStructuresArrayPath_Key, "Crystal Structures", "Enumeration representing the crystal structure for each Ensemble", DataPath{}, ArraySelectionParameter::AllowedTypes{DataType::uint32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); params.insertSeparator(Parameters::Separator{"Output Face Data"}); - params.insert(std::make_unique(k_SurfaceMeshFaceMisorientationColorsArrayName_Key, "Misorientation Colors", "A set of RGB color schemes encoded as floats for each Face", - "FaceMisorientationColors")); + + params.insert(std::make_unique(k_MisorientationArrayName_Key, "Misorientation", + "The name of the array containing the misorientation angle (in degrees) between the 2 features.", "Face Misorientations")); return params; } @@ -72,7 +72,20 @@ Parameters ComputeFeatureFaceMisorientationFilter::parameters() const //------------------------------------------------------------------------------ IFilter::VersionType ComputeFeatureFaceMisorientationFilter::parametersVersion() const { - return 1; + return 2; + + // Version 1 -> 2 + // Description: + // + // Change 1: + // Renamed parameter key + // k_SurfaceMeshFaceMisorientationColorsArrayName_Key ("surface_mesh_face_misorientation_colors_array_name") + // -> k_MisorientationArrayName_Key ("misorientation_array_name") + // + // Change 2: + // Output array component shape changed from {3} ("axis * angle" colors) to {1} (misorientation angle in degrees). + // + // Solution: Pipelines using the old key must be updated to the new key. No automatic migration is provided. } //------------------------------------------------------------------------------ @@ -85,14 +98,13 @@ IFilter::UniquePointer ComputeFeatureFaceMisorientationFilter::clone() const IFilter::PreflightResult ComputeFeatureFaceMisorientationFilter::preflightImpl(const DataStructure& dataStructure, const Arguments& filterArgs, const MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ExecutionContext& executionContext) const { - auto pSurfaceMeshFaceLabelsArrayPathValue = filterArgs.value(k_SurfaceMeshFaceLabelsArrayPath_Key); - auto pAvgQuatsArrayPathValue = filterArgs.value(k_AvgQuatsArrayPath_Key); - auto pFeaturePhasesArrayPathValue = filterArgs.value(k_FeaturePhasesArrayPath_Key); - auto pCrystalStructuresArrayPathValue = filterArgs.value(k_CrystalStructuresArrayPath_Key); - auto pSurfaceMeshFaceMisorientationColorsArrayNameValue = filterArgs.value(k_SurfaceMeshFaceMisorientationColorsArrayName_Key); - - PreflightResult preflightResult; - nx::core::Result resultOutputActions; + auto pSurfaceMeshFaceLabelsArrayPathValue = filterArgs.value(k_SurfaceMeshFaceLabelsArrayPath_Key); + auto pAvgQuatsArrayPathValue = filterArgs.value(k_AvgQuatsArrayPath_Key); + auto pFeaturePhasesArrayPathValue = filterArgs.value(k_FeaturePhasesArrayPath_Key); + auto pCrystalStructuresArrayPathValue = filterArgs.value(k_CrystalStructuresArrayPath_Key); + auto pSurfaceMeshFaceMisorientationColorsArrayNameValue = filterArgs.value(k_MisorientationArrayName_Key); + + Result resultOutputActions; std::vector preflightUpdatedValues; // make sure all the cell data has same number of tuples (i.e. they should all be coming from the same Image Geometry) @@ -109,9 +121,11 @@ IFilter::PreflightResult ComputeFeatureFaceMisorientationFilter::preflightImpl(c return MakePreflightErrorResult(-98411, fmt::format("Could not find the face labels data array at path '{}'", pSurfaceMeshFaceLabelsArrayPathValue.toString())); } - DataPath faceMisorientationColorsArrayPath = pSurfaceMeshFaceLabelsArrayPathValue.replaceName(pSurfaceMeshFaceMisorientationColorsArrayNameValue); - auto action = std::make_unique(DataType::float32, faceLabels->getTupleShape(), std::vector{3}, faceMisorientationColorsArrayPath); - resultOutputActions.value().appendAction(std::move(action)); + { + DataPath faceMisorientationColorsArrayPath = pSurfaceMeshFaceLabelsArrayPathValue.replaceName(pSurfaceMeshFaceMisorientationColorsArrayNameValue); + auto action = std::make_unique(DataType::float32, faceLabels->getTupleShape(), std::vector{1}, faceMisorientationColorsArrayPath); + resultOutputActions.value().appendAction(std::move(action)); + } return {std::move(resultOutputActions), std::move(preflightUpdatedValues)}; } @@ -122,11 +136,11 @@ Result<> ComputeFeatureFaceMisorientationFilter::executeImpl(DataStructure& data { ComputeFeatureFaceMisorientationInputValues inputValues; - inputValues.SurfaceMeshFaceLabelsArrayPath = filterArgs.value(k_SurfaceMeshFaceLabelsArrayPath_Key); - inputValues.AvgQuatsArrayPath = filterArgs.value(k_AvgQuatsArrayPath_Key); - inputValues.FeaturePhasesArrayPath = filterArgs.value(k_FeaturePhasesArrayPath_Key); - inputValues.CrystalStructuresArrayPath = filterArgs.value(k_CrystalStructuresArrayPath_Key); - inputValues.SurfaceMeshFaceMisorientationColorsArrayName = filterArgs.value(k_SurfaceMeshFaceMisorientationColorsArrayName_Key); + inputValues.surfaceMeshFaceLabelsArrayPath = filterArgs.value(k_SurfaceMeshFaceLabelsArrayPath_Key); + inputValues.avgQuatsArrayPath = filterArgs.value(k_AvgQuatsArrayPath_Key); + inputValues.featurePhasesArrayPath = filterArgs.value(k_FeaturePhasesArrayPath_Key); + inputValues.crystalStructuresArrayPath = filterArgs.value(k_CrystalStructuresArrayPath_Key); + inputValues.misorientationArrayPath = inputValues.surfaceMeshFaceLabelsArrayPath.replaceName(filterArgs.value(k_MisorientationArrayName_Key)); return ComputeFeatureFaceMisorientation(dataStructure, messageHandler, shouldCancel, &inputValues)(); } @@ -156,7 +170,7 @@ Result ComputeFeatureFaceMisorientationFilter::FromSIMPLJson(const nl results.push_back( SIMPLConversion::ConvertParameter(args, json, SIMPL::k_CrystalStructuresArrayPathKey, k_CrystalStructuresArrayPath_Key)); results.push_back(SIMPLConversion::ConvertParameter(args, json, SIMPL::k_SurfaceMeshFaceMisorientationColorsArrayNameKey, - k_SurfaceMeshFaceMisorientationColorsArrayName_Key)); + k_MisorientationArrayName_Key)); Result<> conversionResult = MergeResults(std::move(results)); diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureFaceMisorientationFilter.hpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureFaceMisorientationFilter.hpp index 1c76dc9df6..b3692c2f20 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureFaceMisorientationFilter.hpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeFeatureFaceMisorientationFilter.hpp @@ -29,7 +29,9 @@ class ORIENTATIONANALYSIS_EXPORT ComputeFeatureFaceMisorientationFilter : public static constexpr StringLiteral k_AvgQuatsArrayPath_Key = "avg_quats_array_path"; static constexpr StringLiteral k_FeaturePhasesArrayPath_Key = "feature_phases_array_path"; static constexpr StringLiteral k_CrystalStructuresArrayPath_Key = "crystal_structures_array_path"; - static constexpr StringLiteral k_SurfaceMeshFaceMisorientationColorsArrayName_Key = "surface_mesh_face_misorientation_colors_array_name"; + + // Parameter Keys V2 + static constexpr StringLiteral k_MisorientationArrayName_Key = "misorientation_array_name"; /** * @brief Reads SIMPL json and converts it simplnx Arguments. diff --git a/src/Plugins/OrientationAnalysis/test/BadDataNeighborOrientationCheckTest.cpp b/src/Plugins/OrientationAnalysis/test/BadDataNeighborOrientationCheckTest.cpp index a99d2b68a3..678995753b 100644 --- a/src/Plugins/OrientationAnalysis/test/BadDataNeighborOrientationCheckTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/BadDataNeighborOrientationCheckTest.cpp @@ -35,12 +35,57 @@ const std::string k_CStuctsName = "Crystal Structures"; const DataPath k_CStuctsArrayPath = k_CellEnsembleDataPath.createChildPath(k_CStuctsName); } // namespace VerificationConstants +namespace ClassFourInvariants +{ +// Capture the current mask array contents into a std::vector for before/after invariant checks. +inline std::vector CaptureMask(const DataStructure& dataStructure) +{ + const auto& maskArray = dataStructure.getDataRefAs(VerificationConstants::k_MaskArrayPath); + const auto& store = maskArray.getDataStoreRef(); + std::vector snapshot(store.getSize()); + for(usize i = 0; i < store.getSize(); ++i) + { + snapshot[i] = store.getValue(i); + } + return snapshot; +} + +// Assert Class 4 invariants on the post-execute mask: +// - Monotonicity: count of true mask values is non-decreasing across one filter run. +// - No-degrade: no voxel goes from true (good) to false (bad). +// The filter is specified to only ever flip false -> true, never the reverse. +inline void AssertInvariants(const std::vector& originalMask, const DataStructure& dataStructure) +{ + const auto& maskArray = dataStructure.getDataRefAs(VerificationConstants::k_MaskArrayPath); + const auto& store = maskArray.getDataStoreRef(); + REQUIRE(originalMask.size() == store.getSize()); + + usize countBefore = 0; + usize countAfter = 0; + for(usize i = 0; i < store.getSize(); ++i) + { + if(originalMask[i] == 1) + { + ++countBefore; + // No-degrade: a voxel that was good must still be good + REQUIRE(store.getValue(i) == 1); + } + if(store.getValue(i) == 1) + { + ++countAfter; + } + } + // Monotonicity: count after >= count before + REQUIRE(countAfter >= countBefore); +} +} // namespace ClassFourInvariants + // Case 1.1.1: Base Case | 2 phase | Tolerance 5 | 1 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.1.1", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_1/case_1_1_1/case_1_1_1_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_1/case_1_1_1/case_1_1_1_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -87,7 +132,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.1. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_1_1.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_1_1.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -96,9 +141,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.1. // Case 1.1.2: Invalid Base Case | 3 phase | Tolerance 5 | 1 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.1.2", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_1/case_1_1_2/case_1_1_2_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_1/case_1_1_2/case_1_1_2_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -145,7 +190,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.1. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_1_2.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_1_2.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -154,9 +199,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.1. // Case 1.1.3: Invalid Base Case | 2 phase | Tolerance 5 | 1 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.1.3", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_1/case_1_1_3/case_1_1_3_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_1/case_1_1_3/case_1_1_3_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -203,7 +248,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.1. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_1_3.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_1_3.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -212,9 +257,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.1. // Case 1.2.1: Base Case | 2 phase | Tolerance 5 | 2 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.2.1", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_2/case_1_2_1/case_1_2_1_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_2/case_1_2_1/case_1_2_1_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -261,18 +306,29 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.2. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_2_1.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_2_1.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); } // Case 1.2.2: Invalid Base Case | 2 phase | Tolerance 5 | 2 Min Neighbors +// +// Regression coverage for "Issue 2" (stale `w` variable bug) from legacy DREAM3D 6.5.171: +// In the legacy implementation, the misorientation threshold check sat outside the same-phase +// conditional, so a different-phase neighbor could inherit the prior same-phase iteration's `w` +// and incorrectly increment the count. SIMPLNX moves both the misorientation computation AND the +// threshold check inside the same-phase conditional (see Algorithms/BadDataNeighborOrientationCheck.cpp +// lines 105-117). This test exercises the bug-prone configuration: bad voxel 0 (phase=2) has a +// same-phase good neighbor (voxel 1, phase=2, identical quat -> w=0) followed by a different-phase +// good neighbor (voxel 9, phase=1). With NumberOfNeighbors=2 the expected output is mask[0]=0; +// if the SIMPLNX fix were reverted (axisAngle declaration moved outside the conditional), voxel 0 +// would falsely flip to mask[0]=1. See vv/deviations/BadDataNeighborOrientationCheckFilter.md D2. TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.2.2", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_2/case_1_2_2/case_1_2_2_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_2/case_1_2_2/case_1_2_2_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -319,7 +375,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.2. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_2_2.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_2_2.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -328,9 +384,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.2. // Case 1.2.3: Invalid Base Case | 2 phase | Tolerance 5 | 2 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.2.3", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_2/case_1_2_3/case_1_2_3_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_2/case_1_2_3/case_1_2_3_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -377,7 +433,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.2. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_2_3.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_2_3.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -386,9 +442,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.2. // Case 1.3.1: Base Case | 1 phase | Tolerance 5 | 3 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.3.1", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_3/case_1_3_1/case_1_3_1_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_3/case_1_3_1/case_1_3_1_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -435,7 +491,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.3. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_3_1.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_3_1.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -444,9 +500,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.3. // Case 1.3.2: Invalid Base Case | 2 phase | Tolerance 5 | 3 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.3.2", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_3/case_1_3_2/case_1_3_2_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_3/case_1_3_2/case_1_3_2_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -493,7 +549,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.3. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_3_2.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_3_2.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -502,9 +558,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.3. // Case 1.3.3: Invalid Base Case | 1 phase | Tolerance 5 | 3 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.3.3", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_3/case_1_3_3/case_1_3_3_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_3/case_1_3_3/case_1_3_3_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -551,7 +607,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.3. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_3_3.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_3_3.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -560,9 +616,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.3. // Case 1.4.1: Base Case | 1 phase | Tolerance 5 | 4 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.4.1", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_4/case_1_4_1/case_1_4_1_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_4/case_1_4_1/case_1_4_1_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -609,7 +665,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.4. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_4_1.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_4_1.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -618,9 +674,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.4. // Case 1.4.2: Invalid Base Case | 2 phase | Tolerance 5 | 4 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.4.2", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_4/case_1_4_2/case_1_4_2_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_4/case_1_4_2/case_1_4_2_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -667,7 +723,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.4. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_4_2.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_4_2.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -676,9 +732,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.4. // Case 1.4.3: Invalid Base Case | 1 phase | Tolerance 5 | 4 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.4.3", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_4/case_1_4_3/case_1_4_3_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_4/case_1_4_3/case_1_4_3_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -725,7 +781,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.4. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_4_3.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_4_3.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -734,9 +790,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.4. // Case 1.5.1: Base Case | 1 phase | Tolerance 5 | 5 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.5.1", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_5/case_1_5_1/case_1_5_1_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_5/case_1_5_1/case_1_5_1_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -783,7 +839,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.5. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_5_1.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_5_1.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -792,9 +848,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.5. // Case 1.5.2: Invalid Base Case | 2 phase | Tolerance 5 | 5 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.5.2", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_5/case_1_5_2/case_1_5_2_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_5/case_1_5_2/case_1_5_2_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -841,7 +897,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.5. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_5_2.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_5_2.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -850,9 +906,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.5. // Case 1.5.3: Invalid Base Case | 1 phase | Tolerance 5 | 5 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.5.3", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_5/case_1_5_3/case_1_5_3_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_5/case_1_5_3/case_1_5_3_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -899,7 +955,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.5. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_5_3.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_5_3.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -908,9 +964,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.5. // Case 1.6.1: Base Case | 1 phase | Tolerance 5 | 6 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.6.1", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_6/case_1_6_1/case_1_6_1_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_6/case_1_6_1/case_1_6_1_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -957,7 +1013,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.6. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_6_1.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_6_1.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -966,9 +1022,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.6. // Case 1.6.2: Invalid Base Case | 2 phase | Tolerance 5 | 6 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.6.2", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_6/case_1_6_2/case_1_6_2_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_6/case_1_6_2/case_1_6_2_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -1015,7 +1071,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.6. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_6_2.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_6_2.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -1024,9 +1080,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.6. // Case 1.6.3: Invalid Base Case | 1 phase | Tolerance 5 | 6 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.6.3", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_1/case_1_6/case_1_6_3/case_1_6_3_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_1/case_1_6/case_1_6_3/case_1_6_3_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -1073,7 +1129,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.6. } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_1_6_3.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_1_6_3.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -1082,9 +1138,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.6. // Case 2.1: X+ Dim Case (Sequential) | Valid | 1 phase | Tolerance 5 | 5 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.1", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_2/case_2_1/case_2_1_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_2/case_2_1/case_2_1_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -1127,7 +1183,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.1" } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_2_1.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_2_1.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -1136,9 +1192,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.1" // Case 2.2: Y+ Dim Case (Sequential) | Valid | 1 phase | Tolerance 5 | 5 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.2", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_2/case_2_2/case_2_2_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_2/case_2_2/case_2_2_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -1181,7 +1237,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.2" } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_2_2.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_2_2.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -1190,9 +1246,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.2" // Case 2.3: Z+ Dim Case (Sequential) | Valid | 1 phase | Tolerance 5 | 5 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.3", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_2/case_2_3/case_2_3_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_2/case_2_3/case_2_3_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -1235,7 +1291,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.3" } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_2_3.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_2_3.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -1244,9 +1300,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.3" // Case 2.4: X- Dim Case (Recursive) | Valid | 1 phase | Tolerance 5 | 5 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.4", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_2/case_2_4/case_2_4_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_2/case_2_4/case_2_4_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -1289,7 +1345,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.4" } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_2_4.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_2_4.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -1298,9 +1354,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.4" // Case 2.5: Y- Dim Case (Recursive) | Valid | 1 phase | Tolerance 5 | 5 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.5", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_2/case_2_5/case_2_5_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_2/case_2_5/case_2_5_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -1343,7 +1399,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.5" } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_2_5.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_2_5.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -1352,9 +1408,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.5" // Case 2.6: Z- Dim Case (Recursive) | Valid | 1 phase | Tolerance 5 | 5 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.6", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_2/case_2_6/case_2_6_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_2/case_2_6/case_2_6_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -1397,7 +1453,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.6" } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_2_6.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_2_6.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -1406,9 +1462,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.6" // Case 3.1: Long Sequential | Valid | 1 phase | Tolerance 5 | 1 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 3.1", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_3/case_3_1/case_3_1_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_3/case_3_1/case_3_1_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -1451,7 +1507,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 3.1" } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_3_1.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_3_1.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -1460,9 +1516,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 3.1" // Case 3.2: Long Recursive | Valid | 1 phase | Tolerance 5 | 1 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 3.2", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_3/case_3_2/case_3_2_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_3/case_3_2/case_3_2_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -1505,7 +1561,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 3.2" } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_3_2.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_3_2.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -1514,9 +1570,9 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 3.2" // Case 4: Semi-Complex Synthetic Structure | Valid | 3 phase | Tolerance 5 | 4 Min Neighbors TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 4", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") { - const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "7_bad_data_neighbor_orientation_check.tar.gz", "bad_data_neighbor_orientation_check"); + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); - auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check/case_4/case_4_input.dream3d", unit_test::k_TestFilesDir)); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_4/case_4_input.dream3d", unit_test::k_TestFilesDir)); DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); // Bad Data Neighbor Orientation Check Filter @@ -1598,7 +1654,7 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 4", } #ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check/case_4.dream3d", unit_test::k_BinaryTestOutputDir)); + WriteTestDataStructure(dataStructure, fmt::format("{}/verification/bad_data_neighbor_orientation_check_v2/case_4.dream3d", unit_test::k_BinaryTestOutputDir)); #endif UnitTest::CheckArraysInheritTupleDims(dataStructure); @@ -1647,3 +1703,198 @@ TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: SIMPL Bac } } } + +// ============================================================================= +// V&V Class 4 (Invariant) oracle — added 2026-05-29 per V&V cycle. +// +// These tests complement the Class 1 (Analytical) per-case `expectedMask` arrays above with +// invariant-based assertions that hold for ANY input configuration. They are cheap to evaluate +// and catch whole classes of regressions (e.g., a future refactor that accidentally allowed a +// good voxel to be flipped back to bad) that the per-case Class 1 oracle would miss unless the +// regression happened to manifest at exactly one of the 28 fixture points. +// +// Reference: src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md Phase 4. +// ============================================================================= + +// V&V Class 4 — Invariants Sweep across all 18 base-case fixtures. +// Runs each Case 1.X.Y input through the filter and asserts monotonicity + no-degrade. Does not +// check specific expected mask values (Class 1 tests above do that); this test specifically +// targets the invariant guarantees. +TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Class 4 Invariants Sweep", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") +{ + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); + + struct Fixture + { + std::string relPath; + int32 numberOfNeighbors; + }; + + // All 18 Case 1.X.Y base-case fixtures. (Case 2.X, 3.X, 4 also satisfy invariants but their + // input dimensions vary; covering Case 1 already exercises every NumberOfNeighbors value 1-6 + + // every phase configuration variant.) + const std::vector fixtures = { + {"case_1/case_1_1/case_1_1_1/case_1_1_1_input.dream3d", 1}, {"case_1/case_1_1/case_1_1_2/case_1_1_2_input.dream3d", 1}, {"case_1/case_1_1/case_1_1_3/case_1_1_3_input.dream3d", 1}, + {"case_1/case_1_2/case_1_2_1/case_1_2_1_input.dream3d", 2}, {"case_1/case_1_2/case_1_2_2/case_1_2_2_input.dream3d", 2}, {"case_1/case_1_2/case_1_2_3/case_1_2_3_input.dream3d", 2}, + {"case_1/case_1_3/case_1_3_1/case_1_3_1_input.dream3d", 3}, {"case_1/case_1_3/case_1_3_2/case_1_3_2_input.dream3d", 3}, {"case_1/case_1_3/case_1_3_3/case_1_3_3_input.dream3d", 3}, + {"case_1/case_1_4/case_1_4_1/case_1_4_1_input.dream3d", 4}, {"case_1/case_1_4/case_1_4_2/case_1_4_2_input.dream3d", 4}, {"case_1/case_1_4/case_1_4_3/case_1_4_3_input.dream3d", 4}, + {"case_1/case_1_5/case_1_5_1/case_1_5_1_input.dream3d", 5}, {"case_1/case_1_5/case_1_5_2/case_1_5_2_input.dream3d", 5}, {"case_1/case_1_5/case_1_5_3/case_1_5_3_input.dream3d", 5}, + {"case_1/case_1_6/case_1_6_1/case_1_6_1_input.dream3d", 6}, {"case_1/case_1_6/case_1_6_2/case_1_6_2_input.dream3d", 6}, {"case_1/case_1_6/case_1_6_3/case_1_6_3_input.dream3d", 6}, + }; + + for(const auto& fixture : fixtures) + { + DYNAMIC_SECTION("Fixture: " << fixture.relPath << " NumberOfNeighbors=" << fixture.numberOfNeighbors) + { + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/{}", unit_test::k_TestFilesDir, fixture.relPath)); + DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); + + const auto originalMask = ClassFourInvariants::CaptureMask(dataStructure); + + BadDataNeighborOrientationCheckFilter filter; + Arguments args; + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_MisorientationTolerance_Key, std::make_any(5.0f)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_NumberOfNeighbors_Key, std::make_any(fixture.numberOfNeighbors)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_ImageGeometryPath_Key, std::make_any(VerificationConstants::k_ImagePath)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_QuatsArrayPath_Key, std::make_any(VerificationConstants::k_QuatsArrayPath)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_MaskArrayPath_Key, std::make_any(VerificationConstants::k_MaskArrayPath)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_CellPhasesArrayPath_Key, std::make_any(VerificationConstants::k_PhasesArrayPath)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_CrystalStructuresArrayPath_Key, std::make_any(VerificationConstants::k_CStuctsArrayPath)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + ClassFourInvariants::AssertInvariants(originalMask, dataStructure); + } + } +} + +// V&V Class 4 — Idempotence. +// Running the filter twice on the same input must produce identical output to running it once: +// once the inner convergence loop terminates, the algorithm has reached a fixed point. Uses Case 4 +// (the semi-complex 5x5x5 fixture with 3 phases and 4 NumberOfNeighbors) as a non-trivial input. +TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Class 4 Idempotence", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") +{ + const UnitTest::TestFileSentinel testDataSentinel(unit_test::k_TestFilesDir, "bad_data_neighbor_orientation_check_v2.tar.gz", "bad_data_neighbor_orientation_check_v2", true, true); + auto baseDataFilePath = fs::path(fmt::format("{}/bad_data_neighbor_orientation_check_v2/case_4/case_4_input.dream3d", unit_test::k_TestFilesDir)); + DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); + + BadDataNeighborOrientationCheckFilter filter; + Arguments args; + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_MisorientationTolerance_Key, std::make_any(5.0f)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_NumberOfNeighbors_Key, std::make_any(4)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_ImageGeometryPath_Key, std::make_any(VerificationConstants::k_ImagePath)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_QuatsArrayPath_Key, std::make_any(VerificationConstants::k_QuatsArrayPath)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_MaskArrayPath_Key, std::make_any(VerificationConstants::k_MaskArrayPath)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_CellPhasesArrayPath_Key, std::make_any(VerificationConstants::k_PhasesArrayPath)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_CrystalStructuresArrayPath_Key, std::make_any(VerificationConstants::k_CStuctsArrayPath)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + // Run 1 + auto executeResult1 = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult1.result); + const auto maskAfterRun1 = ClassFourInvariants::CaptureMask(dataStructure); + + // Run 2 + auto executeResult2 = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult2.result); + + // Compare: Run 2's output must equal Run 1's output (filter has reached a fixed point). + const auto& maskArray = dataStructure.getDataRefAs(VerificationConstants::k_MaskArrayPath); + const auto& store = maskArray.getDataStoreRef(); + REQUIRE(maskAfterRun1.size() == store.getSize()); + for(usize i = 0; i < store.getSize(); ++i) + { + if(store.getValue(i) != maskAfterRun1[i]) + { + const std::string errorMsg = fmt::format("Idempotence violated at index {}. Run1: {} | Run2: {}", i, maskAfterRun1[i], store.getValue(i)); + CAPTURE(errorMsg); + REQUIRE(false); + } + } +} + +// V&V Class 1 — 2D Image Fixture. +// PR #1590 made `NeighborUtilities` dimensionality-aware (correctly omitting +/-Z face neighbors +// when dims[2]==1). This test exercises that path: a 3x3x1 image with a single bad voxel at the +// 2D center surrounded by its 4 valid X/Y face neighbors. With NumberOfNeighbors=4 the center +// must flip. Expected output: mask = [0,1,0, 1,1,1, 0,1,0]. +TEST_CASE("OrientationAnalysis::BadDataNeighborOrientationCheckFilter: 2D Image Fixture (3x3x1)", "[OrientationAnalysis][BadDataNeighborOrientationCheckFilter]") +{ + DataStructure dataStructure; + ImageGeom* imageGeom = ImageGeom::Create(dataStructure, VerificationConstants::k_ImageName); + imageGeom->setSpacing({1.0f, 1.0f, 1.0f}); + imageGeom->setOrigin({0.0f, 0.0f, 0.0f}); + imageGeom->setDimensions({3, 3, 1}); + + AttributeMatrix* cellData = AttributeMatrix::Create(dataStructure, Constants::k_Cell_Data, ShapeType{1, 3, 3}, imageGeom->getId()); + AttributeMatrix* cellEnsemble = AttributeMatrix::Create(dataStructure, Constants::k_Cell_Ensemble_Data, ShapeType{2}, imageGeom->getId()); + + // Mask: center voxel bad, 4 face neighbors good, 4 corners bad + // Layout (Z=0 plane, row-major y-then-x): + // row 0: 0 1 0 + // row 1: 1 0 1 + // row 2: 0 1 0 + UInt8Array* maskArray = UnitTest::CreateTestDataArray(dataStructure, VerificationConstants::k_MaskName, {1, 3, 3}, {1}, cellData->getId()); + const std::array inputMask = {0, 1, 0, 1, 0, 1, 0, 1, 0}; + for(usize i = 0; i < 9; ++i) + { + (*maskArray)[i] = inputMask[i]; + } + + // Phases: all 1 (single Cubic_High phase) + Int32Array* phasesArray = UnitTest::CreateTestDataArray(dataStructure, VerificationConstants::k_PhasesName, {1, 3, 3}, {1}, cellData->getId()); + phasesArray->fill(1); + + // Quats: all (0, 0, sin(0.5deg), cos(0.5deg)) — identical 1-degree rotation about Z. + // Identical quats -> misorientation = 0 -> within any positive tolerance. + Float32Array* quatsArray = UnitTest::CreateTestDataArray(dataStructure, VerificationConstants::k_QuatsName, {1, 3, 3}, {4}, cellData->getId()); + const float32 q_z = 0.00872654f; // sin(0.5 deg) + const float32 q_w = 0.99996191f; // cos(0.5 deg) + for(usize i = 0; i < 9; ++i) + { + (*quatsArray)[i * 4 + 0] = 0.0f; + (*quatsArray)[i * 4 + 1] = 0.0f; + (*quatsArray)[i * 4 + 2] = q_z; + (*quatsArray)[i * 4 + 3] = q_w; + } + + // CrystalStructures: [sentinel=999, Cubic_High=1]. Matches the structure produced by legacy + // CreateEnsembleInfo (which prepends a sentinel at index 0) so Phases=1 dispatches to Cubic_High. + UInt32Array* crystalStructures = UnitTest::CreateTestDataArray(dataStructure, VerificationConstants::k_CStuctsName, {2}, {1}, cellEnsemble->getId()); + (*crystalStructures)[0] = 999u; // sentinel + (*crystalStructures)[1] = 1u; // Cubic_High (EbsdLib LaueOps index 1) + + // Run filter with NumberOfNeighbors=4 + BadDataNeighborOrientationCheckFilter filter; + Arguments args; + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_MisorientationTolerance_Key, std::make_any(5.0f)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_NumberOfNeighbors_Key, std::make_any(4)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_ImageGeometryPath_Key, std::make_any(VerificationConstants::k_ImagePath)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_QuatsArrayPath_Key, std::make_any(VerificationConstants::k_QuatsArrayPath)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_MaskArrayPath_Key, std::make_any(VerificationConstants::k_MaskArrayPath)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_CellPhasesArrayPath_Key, std::make_any(VerificationConstants::k_PhasesArrayPath)); + args.insertOrAssign(BadDataNeighborOrientationCheckFilter::k_CrystalStructuresArrayPath_Key, std::make_any(VerificationConstants::k_CStuctsArrayPath)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + // Expected: center (index 4) flips; corners stay bad (only 2 good neighbors each). + const std::array expectedMask = {0, 1, 0, 1, 1, 1, 0, 1, 0}; + const auto& maskStore = dataStructure.getDataAs(VerificationConstants::k_MaskArrayPath)->getDataStoreRef(); + for(usize i = 0; i < 9; ++i) + { + if(maskStore.getValue(i) != expectedMask[i]) + { + const std::string errorMsg = fmt::format("2D fixture: values diverged at index {}. Expected: {} | Actual: {}", i, expectedMask[i], maskStore.getValue(i)); + CAPTURE(errorMsg); + REQUIRE(false); + } + } +} diff --git a/src/Plugins/OrientationAnalysis/test/CMakeLists.txt b/src/Plugins/OrientationAnalysis/test/CMakeLists.txt index ec156df973..7df0cb46de 100644 --- a/src/Plugins/OrientationAnalysis/test/CMakeLists.txt +++ b/src/Plugins/OrientationAnalysis/test/CMakeLists.txt @@ -136,7 +136,6 @@ if(EXISTS "${DREAM3D_DATA_DIR}" AND SIMPLNX_DOWNLOAD_TEST_FILES) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME align_sections_misorientation.tar.gz SHA512 8a186b2e96dd94a8583eacaec768c252885d89c8f5734b6511d573235beae075971e6e81b42bb517b7cd617fc478ed394abf8ea4fe3188f50d340f90573013f4) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME align_sections_mutual_information.tar.gz SHA512 0c3b917a6f3b5ed587a4629fc0fa35c0108d927c9d0596854a95e7d792d29f6edd42f3129307e613fea0dd5665fdfbad8b3896e6f307c546b90076a4b83b1d6d) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME align_sections.tar.gz SHA512 b6892e437df86bd79bd2f1d8f48e44d05bfe38b3453058744320bfaf1b1dc461a484edc9e593f6b9de4ad4d04c41b5dbd0a30e6fc605341d046aec4c3062e33e) - download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME 7_bad_data_neighbor_orientation_check.tar.gz SHA512 60089eecfe679466f63ef46839f194f83185a5987f51a0e23b9670e50d967ae49451bcfa43c0d44d6fb12cd55b73d208b36825251842d2b2568ffe521be12fbe) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME caxis_data.tar.gz SHA512 56468d3f248661c0d739d9acd5a1554abc700bf136586f698a313804536916850b731603d42a0b93aae47faf2f7ee49d4181b1c3e833f054df6f5c70b5e041dc) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME compute_feature_neighbor_caxis_misalignments.tar.gz SHA512 955cd35b7ae24579ef9c533df34e1118012a8e5e2a71f8613117c714fc220c5dfa78d91a2964b41752e70684b79d4aa790e488e9a7be4c9dcf7b642ee2897ceb) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME compute_misorientations.tar.gz SHA512 31e649921eebf1e5dd1882279d0ec4d640e2c377a9edbb24d7b81eba74ec3656bd6236b1d1c038aa2123aa5959b529c144915f885b8e08fe1a90eee60f52e766) @@ -158,6 +157,7 @@ if(EXISTS "${DREAM3D_DATA_DIR}" AND SIMPLNX_DOWNLOAD_TEST_FILES) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME write_stats_gen_odf_angle_file.tar.gz SHA512 be3f663aae1f78e5b789200421534ed9fe293187ec3514796ac8177128b34ded18bb9a98b8e838bb283f9818ac30dc4b19ec379bdd581b1a98eb36d967cdd319) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME 6_5_MergeTwins.tar.gz SHA512 756da6b9a2fdc6c7f1cf611243b889b8da0bdc172c1cd184f81672c3cdf651f1f450aecff2e2e0c9b1fa367735ca1df26436d88fa342cea1825b4e5665aa7dfd) download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME compute_feature_reference_misorientation.tar.gz SHA512 6ea9c04ca5b0c0439573b5a14bda63592181c6badb4dd325b542fb97ff2a5d492e83d2bac1bf5999612cbdb7697ec48e321549427470f1f23ccd37921c6a95f1) + download_test_data(DREAM3D_DATA_DIR ${DREAM3D_DATA_DIR} ARCHIVE_NAME bad_data_neighbor_orientation_check_v2.tar.gz SHA512 c311d636f56027da8f3b665375005230be83bb9060aed29dd1aada928d7afbce89d7be845626139c19025a89aaf1ac52b099c8efb8b99f246fc0bfad3c4ce128) endif() diff --git a/src/Plugins/OrientationAnalysis/test/ComputeFeatureFaceMisorientationTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeFeatureFaceMisorientationTest.cpp index e545f212f0..49c471b4fa 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeFeatureFaceMisorientationTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeFeatureFaceMisorientationTest.cpp @@ -3,49 +3,458 @@ #include "OrientationAnalysis/OrientationAnalysis_test_dirs.hpp" #include "simplnx/Core/Application.hpp" -#include "simplnx/Parameters/ArrayCreationParameter.hpp" #include "simplnx/Pipeline/Pipeline.hpp" #include "simplnx/Pipeline/PipelineFilter.hpp" #include "simplnx/UnitTest/UnitTestCommon.hpp" #include #include -#include using namespace nx::core; -using namespace nx::core::UnitTest; namespace fs = std::filesystem; namespace { -constexpr StringLiteral k_FaceMisorientationColors("SurfaceMeshFaceMisorientationColors"); constexpr StringLiteral k_NXFaceMisorientationColors("NXFaceMisorientationColors"); constexpr StringLiteral k_AvgQuats("AvgQuats"); -DataPath smallIn100Group({nx::core::Constants::k_SmallIN100}); -DataPath featureDataPath = smallIn100Group.createChildPath(nx::core::Constants::k_Grain_Data); -DataPath avgEulerAnglesPath = featureDataPath.createChildPath(nx::core::Constants::k_AvgEulerAngles); -DataPath featurePhasesPath = featureDataPath.createChildPath(nx::core::Constants::k_Phases); -DataPath crystalStructurePath = smallIn100Group.createChildPath(nx::core::Constants::k_Phase_Data).createChildPath(nx::core::Constants::k_CrystalStructures); -DataPath avgQuatsPath = featureDataPath.createChildPath(k_AvgQuats); +bool CompareFloats(const float32 generated, const float32 expected) +{ + return std::abs(generated - expected) < 0.000012f; +} +} // namespace -DataPath triangleDataContainerPath({nx::core::Constants::k_TriangleDataContainerName}); -DataPath faceDataGroup = triangleDataContainerPath.createChildPath(nx::core::Constants::k_FaceData); +namespace curated +{ +// Make sure we can instantiate the Align Sections Feature Centroid +constexpr Uuid k_ChangeAngleRepresentationFilterId = *Uuid::FromString("565e06e2-6fd0-4232-89c4-ee672926d565"); +constexpr Uuid k_SimplnxCorePluginId = *Uuid::FromString("05cc618b-781f-4ac0-b9ac-43f26ce1854f"); +const FilterHandle k_ChangeAngleRepresentationFilterHandle(k_ChangeAngleRepresentationFilterId, k_SimplnxCorePluginId); -DataPath faceLabels = faceDataGroup.createChildPath(nx::core::Constants::k_FaceLabels); -DataPath faceNormals = faceDataGroup.createChildPath(nx::core::Constants::k_FaceNormals); -DataPath faceAreas = faceDataGroup.createChildPath(nx::core::Constants::k_FaceAreas); -} // namespace +constexpr StringLiteral k_TriGeomName = "triangle_geom"; +const DataPath k_TriGeomPath({k_TriGeomName}); -TEST_CASE("OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Valid filter execution", "[OrientationAnalysis][ComputeFeatureFaceMisorientationFilter]") +const DataPath k_FaceDataPath = k_TriGeomPath.createChildPath(Constants::k_FaceData); +const DataPath k_FaceLabelsPath = k_FaceDataPath.createChildPath(Constants::k_FaceLabels); + +const DataPath k_FeatureDataPath = k_TriGeomPath.createChildPath(Constants::k_Grain_Data); +const DataPath k_AvgEulerAnglesPath = k_FeatureDataPath.createChildPath(Constants::k_AvgEulerAngles); +const DataPath k_FeaturePhasesPath = k_FeatureDataPath.createChildPath(Constants::k_Phases); +const DataPath k_AvgQuatsPath = k_FeatureDataPath.createChildPath(k_AvgQuats); + +const DataPath k_PhaseDataPath = k_TriGeomPath.createChildPath(Constants::k_Phase_Data); +const DataPath k_CrystalStructurePath = k_PhaseDataPath.createChildPath(Constants::k_CrystalStructures); + +/** + * The data for this test structure was hand-rolled and provided by Mike Jackson. + */ +DataStructure CreateTestDataStructure() { + DataStructure dataStructure = {}; + + TriangleGeom* geom = TriangleGeom::Create(dataStructure, k_TriGeomName); + + // Make Shared Vertex List + { + // clang-format off + std::unique_ptr sharedVertexListBuffer(new TriangleGeom::SharedVertexList::value_type[] { + 0.0f,0.0f,0.0f, + 1.0f,0.0f,0.0f, + 0.0f,1.0f,0.0f, + 2.0f,0.0f,0.0f, + 3.0f,0.0f,0.0f, + 2.0f,1.0f,0.0f, + 4.0f,0.0f,0.0f, + 5.0f,0.0f,0.0f, + 4.0f,1.0f,0.0f, + 0.0f,2.0f,0.0f, + 1.0f,2.0f,0.0f, + 0.0f,3.0f,0.0f, + 2.0f,2.0f,0.0f, + 3.0f,2.0f,0.0f, + 2.0f,3.0f,0.0f, + 4.0f,2.0f,0.0f, + 5.0f,2.0f,0.0f, + 4.0f,3.0f,0.0f, + 0.0f,4.0f,0.0f, + 1.0f,4.0f,0.0f, + 0.0f,5.0f,0.0f, + 2.0f,4.0f,0.0f, + 3.0f,4.0f,0.0f, + 2.0f,5.0f,0.0f, + 4.0f,4.0f,0.0f, + 5.0f,4.0f,0.0f, + 4.0f,5.0f,0.0f, + 0.0f,6.0f,0.0f, + 1.0f,6.0f,0.0f, + 0.0f,7.0f,0.0f, + 2.0f,6.0f,0.0f, + 3.0f,6.0f,0.0f, + 2.0f,7.0f,0.0f, + 4.0f,6.0f,0.0f, + 5.0f,6.0f,0.0f, + 4.0f,7.0f,0.0f, + 0.0f,8.0f,0.0f, + 1.0f,8.0f,0.0f, + 0.0f,9.0f,0.0f, + 2.0f,8.0f,0.0f, + 3.0f,8.0f,0.0f, + 2.0f,9.0f,0.0f, + 4.0f,8.0f,0.0f, + 5.0f,8.0f,0.0f, + 4.0f,9.0f,0.0f, + 0.0f,10.0f,0.0f, + 1.0f,10.0f,0.0f, + 0.0f,11.0f,0.0f, + 2.0f,10.0f,0.0f, + 3.0f,10.0f,0.0f, + 2.0f,11.0f,0.0f, + 4.0f,10.0f,0.0f, + 5.0f,10.0f,0.0f, + 4.0f,11.0f,0.0f, + 0.0f,12.0f,0.0f, + 1.0f,12.0f,0.0f, + 0.0f,13.0f,0.0f, + 2.0f,12.0f,0.0f, + 3.0f,12.0f,0.0f, + 2.0f,13.0f,0.0f, + 4.0f,12.0f,0.0f, + 5.0f,12.0f,0.0f, + 4.0f,13.0f,0.0f, + 0.0f,14.0f,0.0f, + 1.0f,14.0f,0.0f, + 0.0f,15.0f,0.0f, + 2.0f,14.0f,0.0f, + 3.0f,14.0f,0.0f, + 2.0f,15.0f,0.0f, + 4.0f,14.0f,0.0f, + 5.0f,14.0f,0.0f, + 4.0f,15.0f,0.0f, + 0.0f,16.0f,0.0f, + 1.0f,16.0f,0.0f, + 0.0f,17.0f,0.0f, + 2.0f,16.0f,0.0f, + 3.0f,16.0f,0.0f, + 2.0f,17.0f,0.0f, + 4.0f,16.0f,0.0f, + 5.0f,16.0f,0.0f, + 4.0f,17.0f,0.0f, + 0.0f,18.0f,0.0f, + 1.0f,18.0f,0.0f, + 0.0f,19.0f,0.0f, + 2.0f,18.0f,0.0f, + 3.0f,18.0f,0.0f, + 2.0f,19.0f,0.0f, + 4.0f,18.0f,0.0f, + 5.0f,18.0f,0.0f, + 4.0f,19.0f,0.0f, + 0.0f,20.0f,0.0f, + 1.0f,20.0f,0.0f, + 0.0f,21.0f,0.0f, + 2.0f,20.0f,0.0f, + 3.0f,20.0f,0.0f, + 2.0f,21.0f,0.0f, + 4.0f,20.0f,0.0f, + 5.0f,20.0f,0.0f, + 4.0f,21.0f,0.0f, + 0.0f,22.0f,0.0f, + 1.0f,22.0f,0.0f, + 0.0f,23.0f,0.0f, + // Trigonal_High (-3m, Laue index 10) -- appended after edge-case block + 0.0f,24.0f,0.0f, + 1.0f,24.0f,0.0f, + 0.0f,25.0f,0.0f, + 2.0f,24.0f,0.0f, + 3.0f,24.0f,0.0f, + 2.0f,25.0f,0.0f, + 4.0f,24.0f,0.0f, + 5.0f,24.0f,0.0f, + 4.0f,25.0f,0.0f + }); + // clang-format on + + const TriangleGeom::SharedVertexList* sharedVerts = + TriangleGeom::SharedVertexList::Create(dataStructure, TriangleGeom::k_SharedVertexListName, + std::make_shared>(std::move(sharedVertexListBuffer), ShapeType{111}, ShapeType{3}), geom->getId()); + + geom->setVertexListId(sharedVerts->getId()); + } + + // Create Shared Triangles List + { + // clang-format off + std::unique_ptr sharedFaceListBuffer(new TriangleGeom::SharedFaceList::value_type[] { + 0,1,2, + 3,4,5, + 6,7,8, + 9,10,11, + 12,13,14, + 15,16,17, + 18,19,20, + 21,22,23, + 24,25,26, + 27,28,29, + 30,31,32, + 33,34,35, + 36,37,38, + 39,40,41, + 42,43,44, + 45,46,47, + 48,49,50, + 51,52,53, + 54,55,56, + 57,58,59, + 60,61,62, + 63,64,65, + 66,67,68, + 69,70,71, + 72,73,74, + 75,76,77, + 78,79,80, + 81,82,83, + 84,85,86, + 87,88,89, + 90,91,92, + 93,94,95, + 96,97,98, + 99,100,101, + // Trigonal_High triangles -- vertices 102-110 (appended block) + 102,103,104, + 105,106,107, + 108,109,110 + }); + // clang-format on + + const TriangleGeom::SharedFaceList* sharedFaces = + TriangleGeom::SharedFaceList::Create(dataStructure, TriangleGeom::k_SharedFacesListName, + std::make_shared>(std::move(sharedFaceListBuffer), ShapeType{37}, ShapeType{3}), geom->getId()); + + geom->setFaceListId(sharedFaces->getId()); + } + + AttributeMatrix* faceDataAM = AttributeMatrix::Create(dataStructure, k_FaceDataPath.getTargetName(), ShapeType{37}, geom->getId()); + + // Make Face Labels + { + // clang-format off + std::unique_ptr faceLabelsBuffer(new int32[]{ + 1,2, + 1,3, + 1,4, + 5,6, + 5,7, + 5,8, + 9,10, + 9,11, + 9,12, + 13,14, + 13,15, + 13,16, + 17,18, + 17,19, + 17,20, + 21,22, + 21,23, + 21,24, + 25,26, + 25,27, + 25,28, + 29,30, + 29,31, + 29,32, + 33,34, + 33,35, + 33,36, + 37,38, + 37,39, + 37,40, + 0,1, + 1,0, + 1,5, + 5,1, + // Trigonal_High boundaries (F41-F44; appended after edge-case block) + 41,42, + 41,43, + 41,44 + }); + // clang-format on + + DataArray::Create(dataStructure, k_FaceLabelsPath.getTargetName(), std::make_shared(std::move(faceLabelsBuffer), faceDataAM->getShape(), ShapeType{2}), faceDataAM->getId()); + } + + AttributeMatrix* featureDataAM = AttributeMatrix::Create(dataStructure, k_FeatureDataPath.getTargetName(), ShapeType{45}, geom->getId()); + + // Create AvgEulers + { + // clang-format off + std::unique_ptr avgEulerAnglesBuffer(new float32[] { + 0.00f,0.00f,0.00f, + 0.00f,0.00f,0.00f, + 45.00f,0.00f,0.00f, + 90.00f,0.00f,0.00f, + 180.00f,0.00f,0.00f, + 0.00f,0.00f,0.00f, + 45.00f,0.00f,0.00f, + 90.00f,0.00f,0.00f, + 180.00f,0.00f,0.00f, + 0.00f,0.00f,0.00f, + 45.00f,0.00f,0.00f, + 90.00f,0.00f,0.00f, + 180.00f,0.00f,0.00f, + 0.00f,0.00f,0.00f, + 45.00f,0.00f,0.00f, + 90.00f,0.00f,0.00f, + 180.00f,0.00f,0.00f, + 0.00f,0.00f,0.00f, + 45.00f,0.00f,0.00f, + 90.00f,0.00f,0.00f, + 180.00f,0.00f,0.00f, + 0.00f,0.00f,0.00f, + 45.00f,0.00f,0.00f, + 90.00f,0.00f,0.00f, + 180.00f,0.00f,0.00f, + 0.00f,0.00f,0.00f, + 45.00f,0.00f,0.00f, + 90.00f,0.00f,0.00f, + 180.00f,0.00f,0.00f, + 0.00f,0.00f,0.00f, + 45.00f,0.00f,0.00f, + 90.00f,0.00f,0.00f, + 180.00f,0.00f,0.00f, + 0.00f,0.00f,0.00f, + 45.00f,0.00f,0.00f, + 90.00f,0.00f,0.00f, + 180.00f,0.00f,0.00f, + 0.00f,0.00f,0.00f, + 45.00f,0.00f,0.00f, + 90.00f,0.00f,0.00f, + 180.00f,0.00f,0.00f, + // Trigonal_High features F41-F44 (appended) + 0.00f,0.00f,0.00f, + 45.00f,0.00f,0.00f, + 90.00f,0.00f,0.00f, + 180.00f,0.00f,0.00f + }); + // clang-format on + + DataArray::Create(dataStructure, k_AvgEulerAnglesPath.getTargetName(), std::make_shared(std::move(avgEulerAnglesBuffer), featureDataAM->getShape(), ShapeType{3}), + featureDataAM->getId()); + } + + // Create Phases + { + // clang-format off + std::unique_ptr phasesBuffer(new int32[] { + 999, + 1, + 1, + 1, + 1, + 2, + 2, + 2, + 2, + 3, + 3, + 3, + 3, + 4, + 4, + 4, + 4, + 5, + 5, + 5, + 5, + 6, + 6, + 6, + 6, + 7, + 7, + 7, + 7, + 8, + 8, + 8, + 8, + 9, + 9, + 9, + 9, + 10, + 10, + 10, + 10, + // Trigonal_High features F41-F44 all reference phase 11 + 11, + 11, + 11, + 11 + }); + // clang-format on + + DataArray::Create(dataStructure, k_FeaturePhasesPath.getTargetName(), std::make_shared(std::move(phasesBuffer), featureDataAM->getShape(), ShapeType{1}), + featureDataAM->getId()); + } + + AttributeMatrix* phaseDataAM = AttributeMatrix::Create(dataStructure, k_PhaseDataPath.getTargetName(), ShapeType{13}, geom->getId()); + + // Create CrystalStructures + { + // clang-format off + std::unique_ptr crystalStructBuffer(new uint32[] { + 999, + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + // Phase 11 -> Trigonal_High (Laue index 10) + 10 + }); + // clang-format on + + DataArray::Create(dataStructure, k_CrystalStructurePath.getTargetName(), std::make_shared(std::move(crystalStructBuffer), phaseDataAM->getShape(), ShapeType{1}), + phaseDataAM->getId()); + } + + return dataStructure; +} +} // namespace curated + +TEST_CASE("OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Curated Data", "[OrientationAnalysis][ComputeFeatureFaceMisorientationFilter]") +{ + auto app = Application::GetOrCreateInstance(); UnitTest::LoadPlugins(); + auto filterList = app->getFilterList(); + + DataStructure dataStructure = curated::CreateTestDataStructure(); + + // Convert the EulerAngles to radians + { + auto filter = filterList->createFilter(curated::k_ChangeAngleRepresentationFilterHandle); + REQUIRE(nullptr != filter); + + Arguments args; + // Create default Parameters for the filter. + args.insertOrAssign("conversion_type_index", std::make_any(0ULL)); + args.insertOrAssign("angles_array_path", std::make_any(curated::k_AvgEulerAnglesPath)); - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_6_Small_IN100_GBCD.tar.gz", "6_6_Small_IN100_GBCD"); + // Preflight the filter and check result + auto preflightResult = filter->preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions) - // Read the Small IN100 Data set - auto baseDataFilePath = fs::path(fmt::format("{}/6_6_Small_IN100_GBCD/6_6_Small_IN100_GBCD.dream3d", unit_test::k_TestFilesDir)); - DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); + // Execute the filter and check the result + auto executeResult = filter->execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result) + } // Convert the AvgEulerAngles array to AvgQuats for use in ComputeFeatureFaceMisorientationFilter input { @@ -56,7 +465,7 @@ TEST_CASE("OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Valid fi // Create default Parameters for the filter. args.insertOrAssign(ConvertOrientationsFilter::k_InputType_Key, std::make_any(0)); args.insertOrAssign(ConvertOrientationsFilter::k_OutputType_Key, std::make_any(2)); - args.insertOrAssign(ConvertOrientationsFilter::k_InputOrientationArrayPath_Key, std::make_any(avgEulerAnglesPath)); + args.insertOrAssign(ConvertOrientationsFilter::k_InputOrientationArrayPath_Key, std::make_any(curated::k_AvgEulerAnglesPath)); args.insertOrAssign(ConvertOrientationsFilter::k_OutputOrientationArrayName_Key, std::make_any(k_AvgQuats)); // Execute the filter and check the result @@ -71,11 +480,11 @@ TEST_CASE("OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Valid fi Arguments args; // Create default Parameters for the filter. - args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_SurfaceMeshFaceLabelsArrayPath_Key, std::make_any(faceLabels)); - args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_AvgQuatsArrayPath_Key, std::make_any(avgQuatsPath)); - args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_FeaturePhasesArrayPath_Key, std::make_any(featurePhasesPath)); - args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_CrystalStructuresArrayPath_Key, std::make_any(crystalStructurePath)); - args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_SurfaceMeshFaceMisorientationColorsArrayName_Key, std::make_any(::k_NXFaceMisorientationColors)); + args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_SurfaceMeshFaceLabelsArrayPath_Key, std::make_any(curated::k_FaceLabelsPath)); + args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_AvgQuatsArrayPath_Key, std::make_any(curated::k_AvgQuatsPath)); + args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_FeaturePhasesArrayPath_Key, std::make_any(curated::k_FeaturePhasesPath)); + args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_CrystalStructuresArrayPath_Key, std::make_any(curated::k_CrystalStructurePath)); + args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_MisorientationArrayName_Key, std::make_any(::k_NXFaceMisorientationColors)); // Preflight the filter and check result auto preflightResult = filter.preflight(dataStructure, args); @@ -86,70 +495,62 @@ TEST_CASE("OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Valid fi SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); } - // compare the resulting face IPF Colors array - DataPath exemplarPath = faceDataGroup.createChildPath(::k_FaceMisorientationColors); - DataPath generatedPath = faceDataGroup.createChildPath(::k_NXFaceMisorientationColors); - CompareArrays(dataStructure, exemplarPath, generatedPath); - - UnitTest::CheckArraysInheritTupleDims(dataStructure); -} - -TEST_CASE("OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Invalid filter execution", "[OrientationAnalysis][ComputeFeatureFaceMisorientationFilter]") -{ - UnitTest::LoadPlugins(); - - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_6_Small_IN100_GBCD.tar.gz", "6_6_Small_IN100_GBCD"); - - // Read the Small IN100 Data set - auto baseDataFilePath = fs::path(fmt::format("{}/6_6_Small_IN100_GBCD/6_6_Small_IN100_GBCD.dream3d", unit_test::k_TestFilesDir)); - DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); - - // Instantiate the filter, a DataStructure object and an Arguments Object - ComputeFeatureFaceMisorientationFilter filter; - Arguments args; - - SECTION("Inconsistent cell data tuple dimensions") - { - // Convert the AvgEulerAngles array to AvgQuats for use in ComputeFeatureFaceMisorientationFilter input - { - // Instantiate the filter, and an Arguments Object - ConvertOrientationsFilter convertOrientationsFilter; - Arguments convertOrientationsArgs; - - // Create default Parameters for the filter. - convertOrientationsArgs.insertOrAssign(ConvertOrientationsFilter::k_InputType_Key, std::make_any(0)); - convertOrientationsArgs.insertOrAssign(ConvertOrientationsFilter::k_OutputType_Key, std::make_any(2)); - convertOrientationsArgs.insertOrAssign(ConvertOrientationsFilter::k_InputOrientationArrayPath_Key, std::make_any(avgEulerAnglesPath)); - convertOrientationsArgs.insertOrAssign(ConvertOrientationsFilter::k_OutputOrientationArrayName_Key, std::make_any(k_AvgQuats)); - - // Execute the filter and check the result - auto convertOrientationsResult = convertOrientationsFilter.execute(dataStructure, convertOrientationsArgs); - SIMPLNX_RESULT_REQUIRE_VALID(convertOrientationsResult.result); - } - - args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_SurfaceMeshFaceLabelsArrayPath_Key, std::make_any(faceLabels)); - args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_AvgQuatsArrayPath_Key, std::make_any(avgQuatsPath)); - args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_FeaturePhasesArrayPath_Key, std::make_any(faceAreas)); - args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_CrystalStructuresArrayPath_Key, std::make_any(crystalStructurePath)); - args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_SurfaceMeshFaceMisorientationColorsArrayName_Key, std::make_any(::k_NXFaceMisorientationColors)); - } - - SECTION("Missing input data path") - { - args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_SurfaceMeshFaceLabelsArrayPath_Key, std::make_any(faceLabels)); - args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_AvgQuatsArrayPath_Key, std::make_any(avgQuatsPath)); - args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_FeaturePhasesArrayPath_Key, std::make_any(faceAreas)); - args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_CrystalStructuresArrayPath_Key, std::make_any(crystalStructurePath)); - args.insertOrAssign(ComputeFeatureFaceMisorientationFilter::k_SurfaceMeshFaceMisorientationColorsArrayName_Key, std::make_any(::k_NXFaceMisorientationColors)); - } - - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); - - // Execute the filter and check the result - auto executeResult = filter.execute(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); + // Validate Computed Misorientations + const auto& faceMisorientations = dataStructure.getDataRefAs(curated::k_FaceDataPath.createChildPath(::k_NXFaceMisorientationColors)); + + // Outputs were validated by Mike Jackson + REQUIRE(::CompareFloats(faceMisorientations[0], 15.0f)); + REQUIRE(::CompareFloats(faceMisorientations[1], 30.0f)); + REQUIRE(::CompareFloats(faceMisorientations[2], 0.0f)); + REQUIRE(::CompareFloats(faceMisorientations[3], 45.0f)); + + // F5<->F7 (Cubic_High features F5,F7 with phi1 = 0deg and 90deg about c-axis): a 90deg + // rotation about c is a 4-fold cubic symmetry op, so the symmetry-reduced misorientation + // is exactly 0deg. Previously this returned ~0.0212deg due to a precision-fragile + // (qco.z()+qco.w())/sqrt(2) followed by acos near 1 in CubicOps::calculateMisorientationInternal. + // EbsdLib was patched to compute the reduced-quaternion's |v| from explicit components + // (so cancellations like qco.z()-qco.w() preserve precision in IEEE 754); the misorientation + // is now extracted as 2*atan2(|v|, w), which gives exactly 0 in this case. + REQUIRE(::CompareFloats(faceMisorientations[4], 0.0f)); + + REQUIRE(::CompareFloats(faceMisorientations[5], 0.0f)); + REQUIRE(::CompareFloats(faceMisorientations[6], 15.0f)); + REQUIRE(::CompareFloats(faceMisorientations[7], 30.0f)); + REQUIRE(::CompareFloats(faceMisorientations[8], 0.0f)); + REQUIRE(::CompareFloats(faceMisorientations[9], 45.0f)); + REQUIRE(::CompareFloats(faceMisorientations[10], 90.0f)); + REQUIRE(::CompareFloats(faceMisorientations[11], 0.0f)); + REQUIRE(::CompareFloats(faceMisorientations[12], 45.0f)); + REQUIRE(::CompareFloats(faceMisorientations[13], 90.0f)); + REQUIRE(::CompareFloats(faceMisorientations[14], 180.0f)); + REQUIRE(::CompareFloats(faceMisorientations[15], 45.0f)); + REQUIRE(::CompareFloats(faceMisorientations[16], 90.0f)); + REQUIRE(::CompareFloats(faceMisorientations[17], 180.0f)); + REQUIRE(::CompareFloats(faceMisorientations[18], 45.0f)); + REQUIRE(::CompareFloats(faceMisorientations[19], 90.0f)); + REQUIRE(::CompareFloats(faceMisorientations[20], 0.0f)); + REQUIRE(::CompareFloats(faceMisorientations[21], 45.0f)); + REQUIRE(::CompareFloats(faceMisorientations[22], 0.0f)); + REQUIRE(::CompareFloats(faceMisorientations[23], 0.0f)); + REQUIRE(::CompareFloats(faceMisorientations[24], 45.0f)); + REQUIRE(::CompareFloats(faceMisorientations[25], 0.0f)); + REQUIRE(::CompareFloats(faceMisorientations[26], 0.0f)); + REQUIRE(::CompareFloats(faceMisorientations[27], 45.0f)); + REQUIRE(::CompareFloats(faceMisorientations[28], 30.0f)); + REQUIRE(::CompareFloats(faceMisorientations[29], 60.0f)); + + // Special Cases + REQUIRE(std::isnan(faceMisorientations[30])); + REQUIRE(std::isnan(faceMisorientations[31])); + REQUIRE(std::isnan(faceMisorientations[32])); + REQUIRE(std::isnan(faceMisorientations[33])); + + // Trigonal_High (-3m, Laue index 10) -- appended after the edge-case block. + // 3-fold about c gives 120 deg equivalence; the mirror planes containing c do + // not further reduce z-rotation magnitudes. Expected values match Trigonal_Low. + REQUIRE(::CompareFloats(faceMisorientations[34], 45.0f)); // F41 (phi1=0) <-> F42 (phi1=45) + REQUIRE(::CompareFloats(faceMisorientations[35], 30.0f)); // F41 (phi1=0) <-> F43 (phi1=90) + REQUIRE(::CompareFloats(faceMisorientations[36], 60.0f)); // F41 (phi1=0) <-> F44 (phi1=180) UnitTest::CheckArraysInheritTupleDims(dataStructure); } @@ -160,7 +561,7 @@ TEST_CASE("OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: SIMPL Ba UnitTest::LoadPlugins(); auto filterList = app->getFilterList(); - const fs::path conversionDir = fs::path(nx::core::unit_test::k_SourceDir.view()) / "test" / "simpl_conversion"; + const fs::path conversionDir = fs::path(unit_test::k_SourceDir.view()) / "test" / "simpl_conversion"; const std::vector> fixtures = { {"SIMPL 6.5 (UUID)", conversionDir / "6_5" / "ComputeFeatureFaceMisorientationFilter.json"}, @@ -191,7 +592,7 @@ TEST_CASE("OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: SIMPL Ba CHECK(args.value(ComputeFeatureFaceMisorientationFilter::k_AvgQuatsArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); CHECK(args.value(ComputeFeatureFaceMisorientationFilter::k_FeaturePhasesArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); CHECK(args.value(ComputeFeatureFaceMisorientationFilter::k_CrystalStructuresArrayPath_Key) == DataPath({"DataContainer", "CellData", "TestArray"})); - CHECK(args.value(ComputeFeatureFaceMisorientationFilter::k_SurfaceMeshFaceMisorientationColorsArrayName_Key) == "TestName"); + CHECK(args.value(ComputeFeatureFaceMisorientationFilter::k_MisorientationArrayName_Key) == "TestName"); } } } diff --git a/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md b/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md new file mode 100644 index 0000000000..e2c20810b6 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md @@ -0,0 +1,168 @@ +# V&V Report: BadDataNeighborOrientationCheckFilter + +| | | +|----------------------------|-------------------------------------------------------------------------| +| Plugin | OrientationAnalysis | +| SIMPLNX UUID | `3f342977-aea1-49e1-a9c2-f73760eba0d3` | +| SIMPLNX Human Name | Neighbor Orientation Comparison (Bad Data)| +| DREAM3D 6.5.171 equivalent | `BadDataNeighborOrientationCheck` — `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/BadDataNeighborOrientationCheck.{h,cpp}` | +| Verified commit | ** | +| Status | COMPLETE | +| Sign-off | *Nathan Young (algorithm rewrite + initial dataset, PR #1499, 2026-02-02) — Michael Jackson (V&V cycle completion, 2026-06-01)* | + +## At a glance + +| Aspect | Current state | +|------------------------|--| +| Algorithm Relationship | **Port** of legacy `BadDataNeighborOrientationCheck::execute()` (DREAM3D 6.5.171). Same two-pass structure: (1) per-voxel face-neighbor count of within-tolerance misorientations; (2) iterative-decay pass that flips a bad voxel when its same-phase good-neighbor count meets the current level. SIMPLNX bundles two substantive bug fixes from PR #1499 (Issue 1: convergence-loop bound `>` → `>=`; Issue 2: misorientation-threshold check moved inside same-phase conditional) plus a Phase-6 SIMPLNX tolerance-precision fix. | +| Oracle (confirmed) | **Class 1 (Analytical) primary** — engineer's hand-derived `expectedMask` arrays for all 27 algorithmic fixtures, mirrored from `bad_data_neighbor_orientation_check_v2/test_design.md`. **Class 4 (Invariant) companion** — monotonicity + no-degrade asserted via `ClassFourInvariants` helper across all base fixtures and a dedicated idempotence test. | +| Code paths enumerated | 7 of 7 algorithmic paths exercised (cancel check, mask-skip, mixed-phase skip, background-voxel skip, within-tolerance increment, above-tolerance skip, iterative-decay flip + neighbor-count update). | +| Tests today | **31 TEST_CASEs / 49 ctest entries**, 100% pass (2.40s). 27 Class 1 base + 1 SIMPL backwards-compat + 1 Class 4 Invariants Sweep (18 DYNAMIC_SECTIONs) + 1 Class 4 Idempotence + 1 2D Image Fixture (inline-constructed). | +| Exemplar archive | `7_bad_data_neighbor_orientation_check.tar.gz` — **INPUT** `.dream3d` files only (one per case). Expected outputs are inline `expectedMask` literals in the test source. Class 1 oracle source-of-truth (`test_design.md`) bundled in the local archive copy. | +| Legacy comparison | **Run** against DREAM3D 6.5.171 on all 27 algorithmic fixtures. 12 of 27 bit-identical; 15 of 27 differ with 288 mask bytes total, 100% direction 1→0 (SIMPLNX flips correctly, 6.5.171 misses). All observed diffs trace to D1. | +| Bug flags | Two legacy defects, both fixed in the SIMPLNX rewrite and documented as deviations: **D1** (convergence-loop bound off-by-one, observable in 15 of 27 fixtures) and **D2** (stale-`w` variable across mixed-phase neighbors, latent but code-evident). | +| V&V phase | **All V&V work complete per V2 policy.** Class 1 + Class 4 oracle confirmed against 31-test suite; SIMPLNX float-π precision fix verified; legacy A/B comparison against DREAM3D 6.5.171 anchored to D1 + D2 + 3 non-deviations; provenance sidecar + user-facing doc review applied. Three source-tree deliverables (this report + `vv/deviations/...` + `vv/provenance/...`) are in place. **Outstanding:** Status promotion DRAFT → READY FOR REVIEW pending second-engineer oracle review (recommend Joey Kleingers). | + +## Summary + +`BadDataNeighborOrientationCheckFilter` iteratively flips "bad" voxels in a `Mask` array to "good" if a sufficient number of their same-phase face neighbors have crystallographically-similar orientations. The algorithm runs in two passes: (1) for each masked-false voxel, count how many of its 6 face neighbors fall within the user-supplied misorientation tolerance (computed per the appropriate Laue-group symmetry); (2) iterate `currentLevel = 6` down to the user-supplied `NumberOfNeighbors`, flipping every voxel whose count meets the current level and updating its still-bad neighbors' counts after each flip — producing a flood-fill behavior that converges when no flips occur at the user-supplied lower bound. + +Verification is via a **Class 1 (Analytical) hand-derived dataset of 27 algorithmic fixtures** (3×3×3 base cases for parameter combinations of `MisorientationTolerance`, `NumberOfNeighbors`, and phase configuration, plus 5×5×5 sequential / recursive / semi-complex fixtures), with expected `Mask` outputs encoded inline as `std::array expectedMask` literals in the test source. A **Class 4 (Invariant) companion oracle** adds monotonicity + no-degrade + idempotence assertions across all base fixtures. A **direct A/B comparison against DREAM3D 6.5.171** on the same 27 fixtures runs through the official `PipelineRunner` and confirms two legacy defects (D1: convergence-loop bound off-by-one; D2: stale `w` variable across mixed-phase neighbors) that the SIMPLNX rewrite correctly fixes; both are documented as Deviations. + +A SIMPLNX-side precision bug was uncovered and fixed during this V&V cycle: the misorientation tolerance was computed as `MisorientationTolerance × numbers::pi_v / 180.0f`, but `pi_v` is slightly larger than true π, making the float-radian tolerance ~5e-9 rad larger than mathematically faithful. For 4 boundary-exact Case 1.X.3 fixtures landing on *exactly* the user-supplied tolerance, the float-π conversion incorrectly included misorientations that should fail strict `<`. The fix promotes the tolerance computation to `double` + `numbers::pi_v`. With this fix in place, SIMPLNX is bit-identical to the engineer's hand-derived oracle across all 31 tests. + +## Algorithm Relationship + +*Classification:* **Port** ~~| Minor changes | Rewrite | New filter~~ + +*Evidence:* The SIMPLNX algorithm at `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheck.cpp` (~260 lines) is a near line-by-line translation of legacy `BadDataNeighborOrientationCheck::execute()` (DREAM3D 6.5.171, ~240 algorithm lines). Same SIMPL UUID retained via `OrientationAnalysisLegacyUUIDMapping.hpp` + SIMPL 6.4/6.5 conversion fixtures at `test/simpl_conversion/6_*/BadDataNeighborOrientationCheckFilter.json`. Same two-pass control flow: (a) initial per-voxel face-neighbor scan that populates a `neighborCount[]` array; (b) outer while-loop iterating `currentLevel = 6 → NumberOfNeighbors`, with an inner while-loop running until no more flips occur at the current level. Per-voxel logic in both passes is mathematically identical: extract the voxel's quaternion, compute misorientation to each of 6 face neighbors via `LaueOps::calculateMisorientation`, count those within `MisorientationTolerance`. + +*Port-time deltas (each tracked as a Deviation or Non-deviation — see `vv/deviations/BadDataNeighborOrientationCheckFilter.md`):* + +1. **EbsdLib API**: `getMisoQuat(q1, q2, axis_n1, axis_n2, axis_n3)` → `calculateMisorientation(q1, q2) → AxisAngleDType`. Modernized return type; mathematically equivalent in the absence of precision-sensitive boundary cases. +2. **Mask handling**: legacy unpacks `BoolArrayType` directly; SIMPLNX uses `MaskCompareUtilities::MaskCompare` to handle both `Bool` and `UInt8` mask backings transparently. UX-only, no behavioral delta. +3. **Face-neighbor offsets**: legacy hard-coded `int64 neighpoints[6]`; SIMPLNX uses `NeighborUtilities::initializeFaceNeighborOffsets()` + `computeValidFaceNeighbors()` to centralize boundary handling. PR #1590 made this 2D-aware (correctly skips +/-Z neighbors when `dims[2] == 1`). +4. **Progress reporting**: legacy direct `notifyStatusMessage`; SIMPLNX uses `ThrottledMessenger` + `MessageHelper` with stage info (`Level X of Y`). UX-only. +5. **`quat.positiveOrientation()`** added before each `calculateMisorientation` call. Mathematically a no-op for cubic LaueOps (which performs `elementWiseAbs` internally). No behavioral delta. +6. **EbsdLib internal `float` → `double` precision** in `calculateMisorientationInternal`. Modern API takes `QuatD`; legacy was `QuatF`. Mathematically equivalent in the absence of sym-op-aligned boundaries; visible for cubic misorientations that land on a 4-fold / 3-fold / 2-fold symmetry op. The engineer's 27 test fixtures do not include any such voxel pair, so the precision delta is non-observable in this filter's A/B — documented as a non-deviation in the deviation doc. +7. **Bug fix — Issue 1 (loop bound)**: `while(currentLevel > NumberOfNeighbors)` (legacy) → `while(currentLevel >= NumberOfNeighbors)` (SIMPLNX). Documented by the engineer in `bad_data_neighbor_orientation_check_v2/README.md` §"Issue 1". Confirmed by direct A/B against legacy 6.5.171. Tracked as **Deviation D1**. +8. **Bug fix — Issue 2 (stale `w` variable)**: legacy increment `if(w < tolerance) neighborCount++` lived OUTSIDE the same-phase conditional, allowing a different-phase neighbor to inherit the prior iteration's same-phase `w`. SIMPLNX moves both the misorientation computation AND the increment INSIDE the same-phase conditional. Tracked as **Deviation D2**. +9. **SIMPLNX-side tolerance precision fix (Phase 6 of this V&V cycle)**: tolerance computation promoted from `float` + `numbers::pi_v` to `double` + `numbers::pi_v` to remove a ~5e-9 rad amplification that caused 4 boundary-exact Case 1.X.3 fixtures to disagree with the analytical oracle. This was a SIMPLNX bug, not a port artifact; surfaced when the engineer's Class 1 oracle (mask[13]=0 for these cases) was correctly re-asserted in the test source. +10. **Algorithm review hardening (Phase 7 of this V&V cycle)**: cancel checking added at all loop levels; `getDataAs` replaced with `getDataRefAs`; CrystalStructures bounds-validation at `operator()` entry; defensive per-voxel `laueClassIndex` guard; tightened naming + comments. No behavioral delta. + +*Material PRs since baseline (2025-10-01):* + +- **PR #1499** — *"REV: Bad Data Neighbor Orientation Check"* (merged 2026-02-02) — **central V&V event.** Algorithm review pass + Issue 1 + Issue 2 fixes + comprehensive 28-case test rewrite. Engineer: Nathan Young. +- PR #1472 — EbsdLib 2.0.0 API bump (pinned dependency for this filter; effective EbsdLib pin at time of V&V completion is 2.4.1 commit `5c8c993`). +- PR #1523 — `NeighborUtilities` extracted as a shared module (no behavioral delta). +- PR #1538 — Test-sentinel infrastructure for tar.gz extraction (no behavioral delta). +- PR #1588 — SIMPL Backwards Compatibility test added. +- PR #1590 — `NeighborUtilities` 2D-aware path (`dims[2] == 1` correctly skips +/-Z neighbors). + +## Oracle + +*Class:* **1 (Analytical)** primary + **4 (Invariant)** companion. Class 3 (Paper-based) N/A — this filter delegates misorientation math to `ebsdlib::LaueOps::calculateMisorientation`; the Rowenhorst 2015 paper-based verification of that math is part of EbsdLib's own V&V, not this filter's. + +### Applied (Class 1 — Analytical) + +Expected `Mask` outputs are derived in closed form from the input `Quats` + `Phases` + initial `Mask` + `(MisorientationTolerance, NumberOfNeighbors)` parameters by hand-tracing the algorithm: (a) pairwise misorientations between same-phase voxel pairs (closed-form for pure φ1-rotations); (b) initial per-voxel count of within-tolerance face neighbors; (c) iterative-decay walk that flips each masked-false voxel whose count meets `currentLevel`, decrementing `currentLevel` from 6 to `NumberOfNeighbors`. The engineer's `bad_data_neighbor_orientation_check_v2/test_design.md` bundles the derivation for every one of 27 algorithmic cases, with `Mask` / `Phases` / `Quats` input arrays and the expected output `Mask` array depicted in 3×3 (or 5×5) grid form per case. + +The Class 1 oracle's design choices that govern boundary behavior: + +| Configuration | Cases | Engineer's design intent | +|------------------------------------------------|--------------------------------------|-----------------------| +| **Pure φ1 rotations** `(φ1, 0, 0)` Bunge ZXZ | All 27 | Misorientation between any two voxels equals `|Δφ1|` modulo the c-axis symmetry of the Laue group — closed-form derivable. | +| **Strict `<` tolerance comparison** | All 27 | Misorientations that land at *exactly* the user-supplied tolerance are excluded. Case 1.X.3 (X ∈ {3,4,5,6}) deliberately places voxel pairs at exactly 5° to exercise this boundary semantic. | +| **Same-phase requirement** | Case 1.X.2 + Case 1.X.3 (mixed) | Different-phase neighbors are skipped regardless of their misorientation. Case 1.2.2 implicitly serves as the SIMPLNX-side regression test for D2 (legacy stale-`w` bug) — see deviation doc. | +| **Background voxels (phase ≤ 0) skip** | Implicit | A voxel whose phase resolves to the `UnknownCrystalStructure` sentinel is skipped (cellPhases > 0 guard). Allows valid use of the `999` sentinel that `CreateEnsembleInfo` prepends at index 0. | + +### Applied (Class 4 — Invariant) + +Two invariants every filter run must satisfy regardless of input configuration, asserted via `namespace ClassFourInvariants` in the test source: + +- **Monotonicity** — count of `Mask == true` voxels is non-decreasing across one filter run. +- **No-degrade** — no voxel goes from `true` (good) to `false` (bad). + +A third invariant (**Idempotence**: running the filter on its own output produces no further change) is asserted via a dedicated test using Case 4 input. + +### Encoded + +- **Class 1 (Analytical)**: `test/BadDataNeighborOrientationCheckTest.cpp` — 27 `TEST_CASE` blocks (Case 1.1.1 through Case 4) each with an inline `std::array expectedMask` and per-voxel `REQUIRE(maskStore.getValue(i) == expectedMask[i])` checks. ~729 base-case assertions plus several thousand additional assertions for the 5×5×5 fixtures. +- **Class 4 (Invariant)**: `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Class 4 Invariants Sweep` — DYNAMIC_SECTIONs over all 18 Case 1.X.Y fixtures, asserting monotonicity + no-degrade via the `ClassFourInvariants::AssertInvariants` helper. +- **Class 4 (Idempotence)**: `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Class 4 Idempotence` — runs Case 4 input through the filter twice, asserting second-run mask equals first-run mask. +- **2D path coverage**: `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: 2D Image Fixture (3x3x1)` — inline-constructed 3×3×1 image with a single bad center voxel and 4 good face neighbors, NN=4, expected flip. Exercises the PR #1590 2D-aware `NeighborUtilities` path. +- *(kept)* `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: SIMPL Backwards Compatibility` — SIMPL 6.4 + 6.5 conversion paths via `DYNAMIC_SECTION`; UUID + argument-key + parameter-value validation only. + +### Second-engineer review + +- MAJ reviewed all topics and agrees with their assesment. +- *The Class 1 hand-derivations in `test_design.md` for plausibility (the 27 cases are small enough to walk through in ~1 hour).* +- *The Class 4 invariant set for completeness — are there other properties this algorithm must satisfy?* +- *The Phase 9 deviation narrative (D1 loop bound + D2 stale `w`) and the determination that the EbsdLib 2.4.1 CubicOps precision improvement is non-observable in this filter's test data.* + +## Code path coverage + +*7 of 7 paths exercised.* + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheck.cpp` (~260 lines). + +The algorithm has two passes: (a) initial face-neighbor count over all voxels, and (b) iterative-decay flip pass that decrements `currentLevel = 6 → NumberOfNeighbors`. Each pass's per-voxel kernel has branches for mask state, phase match, and tolerance pass. + +| # | Pass | Path | Test case | +|---|--------------------|----------------------------------------|---------------| +| 1 | (a) Initial scan | Voxel mask = true → skip | All cases — every fixture has a mix of true/false voxels | +| 2 | (a) Initial scan | Mask = false, neighbor in different phase or unphased (`cellPhases[voxelIndex] > 0` guard) → skip neighbor | `Case 1.X.2` (3-phase invalid) + Case 4 (mixed phases) | +| 3 | (a) Initial scan | Mask = false, neighbor on out-of-bounds face (corner / edge / 2D image +/-Z) → skip | All 3×3×3 cases (corners + edges) + `2D Image Fixture` (Z bounds) | +| 4 | (a) Initial scan | Mask = false, neighbor same-phase + misorientation `< tolerance` → increment `neighborCount` | All cases — primary algorithmic path | +| 5 | (a) Initial scan | Mask = false, neighbor same-phase + misorientation `>= tolerance` → don't increment | `Case 1.X.3` (boundary-exact at 5°) + `Case 1.1.3` (6° vs 1° = ~5°+ε) | +| 6 | (b) Iterative flip | `neighborCount[voxelIndex] >= currentLevel` AND mask still false → flip + update still-bad neighbors' counts | `Case 1.X.1` (basic flip), `Case 2.X` (sequential), `Case 3.X` (long chains), Case 4 (semi-complex) | +| 7 | (b) Iterative flip | Defensive `laueClassIndex >= numOrientationOps` skip (sentinel-aware bounds guard) | *Not directly tested.* Exercised implicitly when the filter runs on any fixture whose CrystalStructures contains the `UnknownCrystalStructure` sentinel at an unused index (all 27 base fixtures). Low-value gap — adding a deliberate sentinel-at-used-index fixture would only verify the early-exit branch. | + +## Test inventory + +| Test case | Status | Notes | +|-------------------|-------------|-----------------------------------------------| +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.1.1` through `Case 1.6.3` (18 cases) | retained | Class 1 hand-derived `expectedMask` per case, 27-element arrays. The 4 cases 1.X.3 (X ∈ {3,4,5,6}) were reverted from a 2026-05-29 circular-oracle update back to the engineer's hand-derived values during Phase 6. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 2.1` through `Case 2.6` (6 cases) | retained | 5×5×5 sequential / recursive fixtures. Expected output is `all 1` (full convergence), asserted via `maskStore.getValue(i) != 1` loop. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 3.1` + `Case 3.2` | retained | 5×5×5 long-chain cases with `NumberOfNeighbors = 1`; verifies full-grid convergence. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 4` | retained | 5×5×5 semi-complex fixture with 3 phases, `NumberOfNeighbors = 4`. Hand-derived 125-element expected mask. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Class 4 Invariants Sweep` | new-for-V&V | Added 2026-05-29. DYNAMIC_SECTIONs over all 18 Case 1.X.Y fixtures. Asserts monotonicity + no-degrade per filter run. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Class 4 Idempotence` | new-for-V&V | Added 2026-05-29. Runs Case 4 input through the filter twice; asserts second run reproduces first run exactly. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: 2D Image Fixture (3x3x1)` | new-for-V&V | Added 2026-05-29. Inline-constructed 3×3×1 image; exercises PR #1590's 2D-aware `computeValidFaceNeighbors`. Does not consume the exemplar archive. | +| `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: SIMPL Backwards Compatibility` | retained | Added by PR #1588. `DYNAMIC_SECTION` over SIMPL 6.4 + 6.5 conversion fixtures (`test/simpl_conversion/6_*/BadDataNeighborOrientationCheckFilter.json`); validates UUID + argument-key + parameter-value decoding. | + +All 31 TEST_CASEs (49 ctest entries) pass at the verified commit. Dual-build (in-core + OOC) verification deferred — this filter does not have an OOC algorithm variant (direct `Float32Array` / `UInt8Array` access; no `IDataStore` out-of-core path). + +## Exemplar archive + +- **Archive:** `bad_data_neighbor_orientation_check_v2.tar.gz` +- **SHA512:** `6452cfb1f2394c10050082256f60a2068cfad78ef742e9e35b1d6e63b3fb7c35c9fe7bbe093bed4dbb4e758c49ec6da7b1f7e2473838a0421f39fbdd9f4a2f76` +- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md` + +Archive contents: **INPUT** `.dream3d` files only — one per algorithmic test case, organized as `case_X/case_X_Y/case_X_Y_Z/case_X_Y_Z_input.dream3d` (3×3×3 fixtures) and `case_X/case_X_Y/case_X_Y_input.dream3d` (5×5×5 fixtures). Expected output `Mask` arrays are hard-coded inline in `test/BadDataNeighborOrientationCheckTest.cpp`. The archive's local copy also bundles the engineer's `README.md` (legacy bug documentation) and `test_design.md` (Class 1 oracle source-of-truth). No archive re-bundling was needed during this V&V cycle. + +## Deviations from DREAM3D 6.5.171 + +Two documented deviations, both legacy defects fixed by the SIMPLNX rewrite. Three further behaviors common to both implementations are explicitly captured as non-deviations to prevent future re-discovery. + +### BadDataNeighborOrientationCheckFilter-D1 + +- **Symptom:** 6.5.171 fails to flip a bad voxel whose good-neighbor count is exactly equal to `NumberOfNeighbors`. SIMPLNX correctly flips. Observable in 15 of the 27 V&V fixtures (288 mask bytes total). +- **Root cause:** Bug in 6.5.171. Legacy convergence loop `while(currentLevel > m_NumberOfNeighbors)` walks `currentLevel` from 6 down to `N + 1` and never executes the `currentLevel == N` iteration. SIMPLNX corrects to `>=`. +- See `vv/deviations/BadDataNeighborOrientationCheckFilter.md`. + +### BadDataNeighborOrientationCheckFilter-D2 + +- **Symptom (latent):** 6.5.171 can count a different-phase neighbor's misorientation as within tolerance if a *previous* same-phase neighbor's `w` was small, because the legacy threshold check sits outside the same-phase conditional and inherits stale `w`. Not directly observable in the V&V A/B because D1 masks D2 (the loop terminates before the bumped count can cross threshold). Real and code-evident. +- **Root cause:** Bug in 6.5.171. SIMPLNX moves both the misorientation computation AND the increment inside the same-phase conditional. Case 1.2.2 implicitly serves as the SIMPLNX-side regression coverage. +- See `vv/deviations/BadDataNeighborOrientationCheckFilter.md`. + +### Non-deviations (documented for future-engineer awareness) + +- **EbsdLib 2.4.1 CubicOps precision improvement** — `2·atan2(|v|, w)` vs `acos(w)` for cubic sym-op-aligned misorientations. Real precision improvement; not observed in this filter's test data because no engineer-supplied voxel pair lands on a cubic sym op. +- **Raster-order flood-fill** — both filters iterate voxels in linear order with immediate neighbor-count updates after each flip; the final mask depends on linear scan order. Algorithm characteristic, not a defect. +- **Mixed-phase neighbor rejection** — both filters require `cellPhases[voxelIndex] == cellPhases[neighborPoint]` AND `cellPhases[voxelIndex] > 0`. Algorithm characteristic. + +### Downstream impact note + +D1 and D2 propagate through any downstream filter that consumes this filter's `Mask` output. Users coming from DREAM3D 6.5.171 may see materially different reconstructions on data where the canonical Small IN100 pipeline is run with the default `NumberOfNeighbors = 4` — SIMPLNX flips more voxels than 6.5.171 did, producing fuller grain reconstructions and smoother grain boundaries. **Trust SIMPLNX.** diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientations.md b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientations.md new file mode 100644 index 0000000000..6a203511ff --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/ComputeFeatureFaceMisorientations.md @@ -0,0 +1,161 @@ +# V&V Report: ComputeFeatureFaceMisorientationFilter + +| | | +|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Plugin | OrientationAnalysis | +| SIMPLNX UUID | `f3473af9-db77-43db-bd25-60df7230ea73` | +| SIMPLNX Human Name | Compute Feature Face Misorientation (Face) | +| DREAM3D 6.5.171 equivalent | `GenerateFaceMisorientationColoring` — `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/GenerateFaceMisorientationColoring.{h,cpp}` | +| Verified commit | ** | +| Status | DRAFT | +| Sign-off | *Nathan Young (algorithm rewrite + initial dataset, 2026-05-19) — Michael Jackson (hand-built test data, V&V completion, 2026-05-28)* | + +## At a glance + +| Aspect | Current state | +|------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Algorithm Relationship | **Rewrite** — output semantics changed (3-component axis·angle → 1-component angle in degrees); Laue-class support expanded (Hex_High + Cubic_High → all 11 EbsdLib Laue classes); NaN explicit for invalid faces (was implicit 0); ParallelDataAlgorithm with parallelization disabled (was raw TBB); EbsdLib API modernized (`getMisoQuat` → `calculateMisorientation`). Also captures an EbsdLib precision fix in `CubicOps::calculateMisorientationInternal` discovered during this V&V cycle (see Algorithm Relationship section). | +| Oracle (confirmed) | **Class 1 (Analytical) primary** — 37-fixture hand-built dataset: 30 normal cases (10 Laue classes × 3 pure-φ1 boundaries at 0°↔45°, 0°↔90°, 0°↔180°) + 4 edge cases (background-front, background-back, mixed-phase fwd, mixed-phase rev) + 3 Trigonal_High cases. All 11 EbsdLib Laue classes (indices 0–10) exercised. Expected misorientations derived in closed form per Laue-class symmetry group. | +| Code paths enumerated | 7 (from line-by-line scan of the parallel-loop body in `ComputeFeatureFaceMisorientation.cpp`) | +| Tests today | 2: 1 valid-execution Class 1 (positive), 1 SIMPL 6.4+6.5 backwards-compat (DYNAMIC_SECTION). The old "Invalid filter execution" test from the pre-rewrite branch was retired during Nathan's algorithm rewrite (NaN-on-invalid-face semantics make most preflight-failure paths unreachable for the cell-feature data). | +| Exemplar archive | **None — data inlined in test source** (`test/ComputeFeatureFaceMisorientationTest.cpp` namespace `curated`). 102 vertices, 34+3 triangles, 41+4 features, 12+1 ensembles all encoded as `std::unique_ptr<…[]>` literals. No tar.gz archive, no download_test_data() entry needed. | +| Legacy comparison | **Not run.** Output structure differs by design (3-component axis·angle vs 1-component angle), so direct array comparison with DREAM3D 6.5.171's `GenerateFaceMisorientationColoring` output is not meaningful. The deviations are documented per-design rather than verified per-feature against the legacy output. | +| Bug flags | One root-caused precision issue **in EbsdLib** (not in this filter): `CubicOps::calculateMisorientationInternal` lost precision via `(qco.z()+qco.w())/sqrt(2)` followed by `acos(w)` near 1. Patched in EbsdLib to use `2·atan2(|v|, w)` with `|v|` from explicit reduced-quaternion components. Eliminated a ~0.02° residual on cubic boundaries that lie on a 4-fold sym op. | +| V&V phase | **Phases 1, 2 (N/A — new test set, no legacy exemplar to retro-promote), 3, 4, 5, 6, 7, 8, 11 — complete.** Class 1 oracle verifies all 11 Laue classes with hand-derived expected values; all 54 assertions pass. EbsdLib precision fix verified by 306/306 EbsdLib tests + 181/189 OrientationAnalysis tests (8 failures all small precision diffs in downstream filters — characterized below). **Outstanding:** Phase 9 (deviation narrative review by second engineer), Phase 13 (status promotion). | + +## Summary + +`ComputeFeatureFaceMisorientationFilter` computes a single per-triangle misorientation angle (in degrees) between the two grains on either side of each surface-mesh face. The algorithm reads each face's two `FaceLabels` features, looks up their average orientations (`AvgQuats`) and shared phase, and dispatches to the appropriate `LaueOps::calculateMisorientation` for the symmetry-reduced minimum angle; faces with mixed phases, background voxels (`featureId ≤ 0`), or unsupported Laue classes receive an explicit `NaN`. Verification used a **Class 1 (Analytical) hand-built 37-fixture dataset** that sweeps all 11 EbsdLib Laue classes via pure φ1-rotations (0°, 45°, 90°, 180° about the c-axis), allowing closed-form symmetry-group calculation of every expected value — all 54 test assertions pass. A precision issue uncovered during this V&V cycle (the `acos(w)`-near-1 catastrophic cancellation in `CubicOps::calculateMisorientationInternal` when the misorientation lies on a cubic symmetry op) was patched in EbsdLib by computing the reduced quaternion's `|v|` from explicit components, eliminating a ~0.02° residual. + +## Algorithm Relationship + +*Classification:* **Rewrite** ~~| Port | Minor changes | New filter~~ + +*Evidence:* The SIMPLNX algorithm at `Algorithms/ComputeFeatureFaceMisorientation.cpp` is a **deliberate rewrite** of legacy `GenerateFaceMisorientationColoring::CalculateFaceMisorientationColorsImpl` (DREAM3D 6.5.171, `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/GenerateFaceMisorientationColoring.cpp` lines 76–160). Same SIMPL UUID retained via `OrientationAnalysisLegacyUUIDMapping.hpp` + SIMPL 6.4/6.5 conversion fixtures at `test/simpl_conversion/6_*/ComputeFeatureFaceMisorientationFilter.json`. The legacy control flow is preserved at the structural level (parallel per-triangle loop, look up phase + crystal structure of both faces, dispatch by Laue class, fall through to "no value" path for mismatches) but every interior choice differs. + +*Port-time deltas (each tracked as a Deviation — see `vv/deviations/ComputeFeatureFaceMisorientations.md`):* + +1. **Output structure: 3-component → 1-component** (Deviation D2). Legacy writes `(w·n1, w·n2, w·n3)` per triangle — the rotation axis component-wise multiplied by the angle in degrees. SIMPLNX writes just the angle in degrees. The 3-component form encoded both magnitude AND direction of the misorientation; the 1-component form keeps only the magnitude. This better matches the typical downstream use (binning misorientation magnitude for grain-boundary statistics or histograms). +2. **Laue class support: 2 classes → 11 classes** (Deviation D1). Legacy hand-codes a check for `Hexagonal_High || Cubic_High` (line 127); all other Laue classes silently fall through to the implicit-zero path. SIMPLNX checks `laueIndex < m_LaueOrientationOps.size()`, allowing all Laue classes that `ebsdlib::LaueOps::GetAllOrientationOps()` returns. The modern EbsdLib has implementations for all 11 standard Laue classes. +3. **Invalid-face handling: implicit 0 → explicit NaN** (Deviation D3). Legacy writes `(0, 0, 0)` for any face where the algorithm cannot compute a meaningful misorientation (mixed phases, background voxel, unsupported Laue class). SIMPLNX writes `(NaN)`. Critical for downstream filters that previously had to treat all-zero outputs as "either genuine zero misorientation OR unprocessed face"; SIMPLNX disambiguates. +4. **EbsdLib API: `getMisoQuat(q1, q2, n1, n2, n3)` → `calculateMisorientation(q1, q2) → AxisAngleDType`** (Deviation D4 — partial). Legacy's `getMisoQuat` returned the angle directly and filled axis components by reference. The modern API returns a structured `AxisAngleDType` (axis + angle in a single value object). The legacy API is no longer present in the current EbsdLib. +5. **Parallelization: raw `tbb::parallel_for` → `ParallelDataAlgorithm` with `setParallelizationEnabled(false)`**. Legacy parallelizes via direct TBB calls under `SIMPL_USE_PARALLEL_ALGORITHMS`. SIMPLNX uses the wrapper `ParallelDataAlgorithm` but explicitly disables parallelization. **Per CLAUDE.md thread-safety guidance**: DataArray write access from worker threads is not guaranteed safe under SIMPLNX's out-of-core data store implementations; serial execution is the safe default. No algorithmic effect on completed runs. +6. **EbsdLib precision fix** (Deviation D4 — full root-cause). The `CubicOps::calculateMisorientationInternal` hand-rolled angle extraction (in EbsdLib, NOT in this filter's code) used `(qco.z()+qco.w())/sqrt(2)` followed by `acos(w)`. When the misorientation lies on a cubic symmetry op (e.g., 90° about c-axis is a 4-fold sym op), `w` lands at ~`1 - 2e-8` due to float32-input precision noise; `acos(w)` then amplifies this to ~2×10⁻⁴ rad ≈ 0.023° residual. Patched to compute the reduced quaternion's `|v|` from explicit components (subtractions of identical floats yield exactly 0) and use `2·atan2(|v|, w)`. The reduced-quaternion components form preserves the cancellation precision that `sqrt(1 - w²)` loses. Eliminates the ~0.02° residual; this filter's F5↔F7 cubic-on-symmetry test case now returns exactly 0° (was 0.0212°). + +*Material PRs since baseline (2026-05-19, Nathan's V&V doc commit):* + +- **`nathan/enh/issue_1596`** (squashed into this branch 2026-05-28) — Six commits authored by Nathan implementing the filter rewrite (deltas 1–5 above). Squash-merged because the intermediate commits ("filter and algorithm implementation; test pending", "Create new test (failing)", "patch test", etc.) are work-in-progress and the squashed unit is the meaningful change. +- **This V&V cycle (2026-05-28)** — Mike added the hand-built Class 1 test dataset, Mike added Trigonal_High coverage (originally missing — Laue indices 0–9 only; index 10 added to close the gap), Mike + Claude root-caused the F5↔F7 precision residual to EbsdLib `CubicOps::calculateMisorientationInternal`, Claude patched EbsdLib with the `2·atan2(|v|, w)` form, Mike updated test assertion from `0.0212f` → `0.0f`. + +## Oracle + +*Class:* **1 (Analytical)** primary. + +### Applied (Class 1 — Analytical) + +Expected misorientation values are derived from the closed-form symmetry-group reduction of the boundary's true rotation. The dataset uses pure φ1-rotations (Bunge Euler angles `(φ1, 0, 0)` with `Φ = φ2 = 0`), so the true misorientation between any two features is simply `|Δφ1|` modulo the c-axis-aligned symmetry operators of the Laue class. + +For each Laue class L, four features (one phase, four orientations) are constructed: +- Feature A: `φ1 = 0°` +- Feature B: `φ1 = 45°` +- Feature C: `φ1 = 90°` +- Feature D: `φ1 = 180°` + +And three boundary faces are constructed: A↔B, A↔C, A↔D. The symmetry-reduced expected misorientation depends on the Laue class's c-axis n-fold: + +| Laue class (idx) | c-axis n-fold | A↔B (0°↔45°) | A↔C (0°↔90°) | A↔D (0°↔180°) | +|--------------------------------------------|---------------|--------------|--------------|---------------| +| Hexagonal_High m⁻³m (0) | 6-fold | 15° | 30° | 0° | +| Cubic_High 6/mmm (1) | 4-fold | 45° | **0°*** | 0° | +| Hexagonal_Low 6/m (2) | 6-fold | 15° | 30° | 0° | +| Cubic_Low m-3 (3) | 2-fold (face) | 45° | 90° | 0° | +| Triclinic -1 (4) | 1-fold | 45° | 90° | 180° | +| Monoclinic 2/m (5) | 1-fold | 45° | 90° | 180° | +| OrthoRhombic mmm (6) | 2-fold | 45° | 90° | 0° | +| Tetragonal_Low 4/m (7) | 4-fold | 45° | 0° | 0° | +| Tetragonal_High 4/mmm (8) | 4-fold | 45° | 0° | 0° | +| Trigonal_Low -3 (9) | 3-fold | 45° | 30° | 60° | +| Trigonal_High -3m (10) — *added this cycle* | 3-fold | 45° | 30° | 60° | + +*Cubic_High A↔C: 90° about c-axis is a 4-fold cubic symmetry op of m-3m, so the true symmetry-reduced misorientation is exactly 0°. Before the EbsdLib precision fix, this returned ~0.0212° due to `acos(w)` near 1. See Deviation D4 / Algorithm Relationship delta 6. + +**Edge cases** (faces 30–33, after the 30 normal cases): all four expected to produce NaN. + +| Face | Front label | Back label | Expected | Path exercised | +|------|-------------|------------|----------|--------------------------------------------------| +| 30 | 0 | 1 | NaN | Background-front (frontFeature == 0) | +| 31 | 1 | 0 | NaN | Background-back (backFeature == 0) | +| 32 | 1 | 5 | NaN | Different phases (phase 1 Hex_High vs phase 2 Cubic_High) | +| 33 | 5 | 1 | NaN | Different phases, reversed | + +### Encoded + +- **Class 1 (Analytical)**: `test/ComputeFeatureFaceMisorientationTest.cpp::"OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Curated Data"` — 30 + 4 + 3 = 37 fixture assertions, 54 total assertions (including geometry setup REQUIRE-VALID checks). +- *(kept)* `test/ComputeFeatureFaceMisorientationTest.cpp::"OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: SIMPL Backwards Compatibility"` — SIMPL 6.4 + 6.5 conversion paths via `DYNAMIC_SECTION`. + +### Second-engineer review + +*Pending — recommend a second engineer (Joey or another OA-domain engineer) review the symmetry-group hand calculations for Trigonal_High and the EbsdLib precision-fix rationale before sign-off. The Trigonal_Low and Trigonal_High closed-form values are identical (mirror planes containing the c-axis do not reduce pure c-axis rotations further); a domain reviewer should verify this conclusion.* + +## Code path coverage + +*7 of 7 paths exercised. Cancel-check paths and "valid Laue class" type-dispatch are aggregate-tested via the Class 1 dataset; per-Laue-class paths are confirmed individually by the per-class assertions.* + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeFeatureFaceMisorientation.cpp` (146 lines). + +| # | Phase | Path | Test case | +|---|-----------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------| +| 1 | Cancel check | `m_ShouldCancel` checked at top of per-triangle loop → early return | *Not directly tested.* Loop-guard only; cancel-signal injection requires test infrastructure not present. Low-value gap. | +| 2 | Per-face | `frontFeature == 0` (background) → `frontPhase = 0` → falls through to "different phases" path → NaN written | `Curated Data` — face 30 `(0, 1)` covers this path | +| 3 | Per-face | `backFeature == 0` (background) → `backPhase = 0` → falls through to "different phases" path → NaN written | `Curated Data` — face 31 `(1, 0)` covers this path | +| 4 | Per-face | `frontPhase > 0 && frontPhase != backPhase` → falls through to "different phases" path → NaN written | `Curated Data` — faces 32 `(1, 5)` and 33 `(5, 1)` cover this path | +| 5 | Per-face | `frontPhase > 0 && frontPhase == backPhase && laueIndex >= m_LaueOrientationOps.size()` → NaN written (unsupported Laue class) | *Not directly tested.* All 11 Laue classes in the curated dataset are within EbsdLib's supported range. Low-value gap. | +| 6 | Per-face | `frontPhase > 0 && frontPhase == backPhase && laueIndex < m_LaueOrientationOps.size()` → call `m_LaueOrientationOps[laueIndex]->calculateMisorientation(q1, q2)` → write `axisAngle[3] * k_180OverPiD` (angle in degrees) | `Curated Data` — all 30 normal-case asserts + 3 Trigonal_High asserts exercise this path | +| 7 | Per-face (math) | Inside `calculateMisorientation`: cubic-class sym-op enumeration via type-1/2/3 reduced quaternion (in `CubicOps::calculateMisorientationInternal`), with the precision-fixed `2·atan2(|v|, w)` angle extraction | `Curated Data` — F5↔F6 (type 1), F5↔F7 (type 2, EbsdLib precision-fix-critical path), F5↔F8 (type 2 or 3 depending on which sym op wins) | + +## Test inventory + +| Test case | Status | Notes | +|--------------------------------------------------------------------------------------|-------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Curated Data` | new-for-V&V | Class 1 hand-built dataset; 30 normal + 4 edge + 3 Trigonal_High asserts. Replaces the legacy `Valid filter execution` test (which used the `6_6_Small_IN100_GBCD.tar.gz` exemplar) — the legacy test was a regression-against-exemplar test, not a closed-form correctness test, and was incompatible with the rewritten 1-component output. | +| `OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: SIMPL Backwards Compatibility` | kept | Unchanged. `DYNAMIC_SECTION` over SIMPL 6.4 and 6.5 conversion fixtures (`test/simpl_conversion/6_*/ComputeFeatureFaceMisorientationFilter.json`); validates UUID, argument keys, and parameter conversion only. | +| *(retired)* `OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Invalid filter execution` | retired | Removed during Nathan's rewrite. The Class 1 dataset's faces 30–33 cover the same paths via the NaN-on-invalid-face semantics; the explicit-preflight-failure tests are no longer reachable for the new code structure. | + +## Exemplar archive + +- **Archive:** None — data inlined in `test/ComputeFeatureFaceMisorientationTest.cpp` namespace `curated`. +- **SHA512:** N/A +- **Provenance:** `src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientations.md` + +Data construction details: 102 vertices laid out in a y-axis-stacked grid (one row of 9 vertices per Laue class block + edge case block), 34 + 3 triangles (3 per Laue class + 4 edge case + 3 Trigonal_High), 41 + 4 features, 12 + 1 ensembles. The unique-vertices-per-triangle layout means each triangle is geometrically independent (no shared edges or vertices between triangles) — this is intentional, since the algorithm only reads `FaceLabels`, not vertex coordinates, so the geometric layout is arbitrary. + +## Deviations from DREAM3D 6.5.171 + +Four documented deviation classes. All are deliberate design changes from the legacy filter (none are bugs in either side). One related EbsdLib precision fix (not a deviation in the strict V&V sense, since it improves both SIMPLNX and any other consumer of EbsdLib's CubicOps). + +### ComputeFeatureFaceMisorientations-D1 + +- Supports all 11 Laue classes; legacy supported only Hex_High and Cubic_High. See `vv/deviations/ComputeFeatureFaceMisorientations.md`. + +### ComputeFeatureFaceMisorientations-D2 + +— Output is a 1-component angle in degrees; legacy was a 3-component axis·angle vector. See `vv/deviations/ComputeFeatureFaceMisorientations.md`. + +### ComputeFeatureFaceMisorientations-D3 + +— Invalid faces (mixed phase, background voxel, unsupported Laue class) write NaN; legacy wrote 0. See `vv/deviations/ComputeFeatureFaceMisorientations.md`. + +### ComputeFeatureFaceMisorientations-D4 + +— Precision improvement on cubic boundaries that lie on a 4-fold symmetry op. Root-caused to EbsdLib `CubicOps::calculateMisorientationInternal`; patched at the EbsdLib level (replaces `acos(w)` with `2·atan2(|v|, w)` using explicit reduced-quaternion components). See `vv/deviations/ComputeFeatureFaceMisorientations.md`. + +### Downstream impact note (not a deviation, characterized for transparency): + +The EbsdLib precision fix in D4 propagates through any filter that consumes cubic misorientations. Eight OrientationAnalysis unit tests now fail against their pre-fix exemplars with diffs in the range `1.4× to 10× epsilon` (epsilons of `1e-4`, observed diffs `1.4e-4` to `1e-3`): +- `BadDataNeighborOrientationCheckFilter: Case 1.{3,4,5,6}.3` +- `ComputeFeatureReferenceMisorientationsFilter_AverageMisorientation` +- `ComputeFeatureReferenceMisorientationsFilter_EuclideanDistance` +- `ComputeKernelAvgMisorientationsFilter` +- `ComputeFeatureNeighborMisorientationsFilter`. + +These exemplar files were generated against the pre-fix algorithm; the new values are *more* mathematically correct. **The exemplar files will need to be regenerated**. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/BadDataNeighborOrientationCheckFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/BadDataNeighborOrientationCheckFilter.md new file mode 100644 index 0000000000..e9aef27b4a --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/deviations/BadDataNeighborOrientationCheckFilter.md @@ -0,0 +1,115 @@ +# Deviations from DREAM3D 6.5.171: BadDataNeighborOrientationCheckFilter + +This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`BadDataNeighborOrientationCheck`, source at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/BadDataNeighborOrientationCheck.{h,cpp}` in DREAM3D 6.5.171). + +Entries are referenced by stable ID (`BadDataNeighborOrientationCheckFilter-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. + +## Comparison summary + +A direct A/B comparison was run on 2026-05-29 across all 27 algorithmic test fixtures defined by the engineer in the V&V test data archive (`bad_data_neighbor_orientation_check_v2/test_design.md`). Inputs were identical (same `Quats`, `Phases`, `Mask`, `CrystalStructures`, `MisorientationTolerance`, `NumberOfNeighbors`) for both implementations. + +| | Cases | Mask bytes affected | +|---|---|---| +| Bit-identical SIMPLNX = 6.5.171 | 12 of 27 (all Case 1.X.{2,3} — "should not flip" scenarios) | 0 | +| SIMPLNX ≠ 6.5.171, direction 1→0 (SIMPLNX flips, 6.5.171 misses) | 15 of 27 (all Case 1.X.1 + all Case 2.X + both Case 3.X + Case 4) | 288 | +| SIMPLNX ≠ 6.5.171, direction 0→1 (SIMPLNX correct, 6.5.171 false-flips) | 0 | 0 | + +**100% of observed diffs are direction 1→0**, consistent with a single root cause: the legacy convergence-loop bound that terminates one level early (D1 below). The D2 stale-`w` defect is real and code-evident but does not produce a 0→1 diff in any of the engineer's tests because D1 prevents the bumped count from ever crossing threshold — the two legacy bugs mask each other. + +--- + +## BadDataNeighborOrientationCheckFilter-D1 + +| Field | Value | +|---|---| +| **Deviation ID** | `BadDataNeighborOrientationCheckFilter-D1` | +| **Filter UUID** | `3f342977-aea1-49e1-a9c2-f73760eba0d3` | +| **Status** | active | + +**Symptom:** DREAM3D 6.5.171 fails to flip a bad voxel whose good-neighbor count is exactly equal to the user-supplied `NumberOfNeighbors`. SIMPLNX correctly flips such voxels. Observable in 15 of the 27 V&V fixtures (288 mask bytes total) — every case where the algorithm depends on reaching the bottom level of the iterative-decay loop. + +**Root cause:** Bug in DREAM3D 6.5.171. + +The legacy iterative-decay loop is `while(currentLevel > m_NumberOfNeighbors)` (`Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/BadDataNeighborOrientationCheck.cpp:299`). With user-supplied `NumberOfNeighbors = N`, this walks `currentLevel` from 6 down through `N + 1` and never executes the `currentLevel == N` iteration. A bad voxel with exactly N good neighbors can therefore never be flipped — contradicting the parameter description ("Required Number of Neighbors") which implies that count to be sufficient. + +SIMPLNX corrects this to `while(currentLevel >= m_InputValues->NumberOfNeighbors)` (`src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/BadDataNeighborOrientationCheck.cpp:129`). The fix is also explicitly documented as "BUG: Fix only checking values greater than the supplied min number of neighbors" in the merge commit of PR #1499 and in the engineer's V&V test archive README at `bad_data_neighbor_orientation_check_v2/README.md` §"Issue 1". + +**Affected users:** Anyone running the filter in DREAM3D 6.5.171 with `NumberOfNeighbors < 6` on a dataset where the bottom level matters (i.e., where any bad voxel's eligible-neighbor count equals the user's `NumberOfNeighbors`). In practice this is the typical usage — the Small IN100 reconstruction pipeline (the canonical DREAM3D example) uses `NumberOfNeighbors = 4`. The 6.5.171 output left bad voxels unflipped that should have been flipped, manifesting downstream as smaller-than-expected grain reconstructions, more "rough" grain boundaries, and lower fraction of good voxels. + +**Recommendation:** Trust SIMPLNX. The 6.5.171 result was mathematically incorrect for the stated parameter semantics. The minimal legacy patch is a one-line change from `>` to `>=`; a local `v6_5_172` branch on `/Users/mjackson/DREAM3D-Dev/DREAM3D` carries this fix bundled with D2 (commit `0cad1b6b3`). + +--- + +## BadDataNeighborOrientationCheckFilter-D2 + +| Field | Value | +|---|---| +| **Deviation ID** | `BadDataNeighborOrientationCheckFilter-D2` | +| **Filter UUID** | `3f342977-aea1-49e1-a9c2-f73760eba0d3` | +| **Status** | active (latent — not observable in the V&V test suite, but real and code-evident) | + +**Symptom:** Latent. DREAM3D 6.5.171 can count a different-phase neighbor's misorientation as within tolerance if a *previous* same-phase neighbor's `w` was small, because the misorientation-threshold check sits outside the same-phase conditional and inherits the stale `w` from the prior iteration. SIMPLNX prevents this by moving the threshold check inside the same-phase conditional. + +This bug is not directly observable in any of the 27 V&V test fixtures, because the D1 loop-bound bug (above) terminates the iterative-decay loop before any voxel whose count was incorrectly bumped by D2 could be flipped. The two legacy bugs cancel each other in the engineer's test inputs. + +**Root cause:** Bug in DREAM3D 6.5.171. + +The legacy per-neighbor loop body is (`BadDataNeighborOrientationCheck.cpp:283-291`): + +```cpp +if(m_CellPhases[i] == m_CellPhases[neighbor] && m_CellPhases[i] > 0) +{ + w = m_OrientationOps[phase1]->getMisoQuat(q1, q2, n1, n2, n3); +} +if(w < misorientationTolerance) // <-- outside the same-phase conditional! +{ + neighborCount[i]++; +} +``` + +When the current neighbor has a different phase than the voxel, the `w = getMisoQuat(...)` assignment is skipped, and the subsequent `if(w < misorientationTolerance)` reads `w` from the *previous neighbor iteration* (or from `w`'s initial value `10000.0f` if no previous iteration matched). The previous iteration's `w` may be small (e.g., from a same-phase good neighbor with an identical orientation), in which case the comparison succeeds and the count is incorrectly bumped. + +SIMPLNX moves both the misorientation computation AND the increment inside the same-phase conditional (`Algorithms/BadDataNeighborOrientationCheck.cpp:105-117`): + +```cpp +if(cellPhases[voxelIndex] == cellPhases[neighborPoint] && cellPhases[voxelIndex] > 0) +{ + ebsdlib::QuatD quat2(quats[neighborPoint * 4], ...); + quat2.positiveOrientation(); + ebsdlib::AxisAngleDType axisAngle = orientationOps[laueClass1]->calculateMisorientation(quat1, quat2); + if(axisAngle[3] < misorientationTolerance) + { + neighborCount[voxelIndex]++; + } +} +``` + +The bug is documented as "Issue 2" in the engineer's V&V test archive README at `bad_data_neighbor_orientation_check_v2/README.md`, and was bundled into PR #1499's REV cleanup. + +**Affected users:** Anyone running the filter in DREAM3D 6.5.171 on a dataset with mixed phases adjacent to grain boundaries. The bug would manifest as voxels at phase boundaries being incorrectly flipped to "good" because they appear to have more within-tolerance neighbors than they actually do. + +**Why not observable in V&V A/B:** The D1 loop-bound bug prevents iteration from reaching the level where the bumped count would matter. With `NumberOfNeighbors = N`, D1 stops iteration at `currentLevel = N + 1`, so a voxel with count = N (true count) or N + 1 (bumped count) cannot be flipped at the N level. To isolate D2, one would need to patch legacy 6.5.171 with just the D1 fix (without D2 fix), then run a mixed-phase fixture where a bad voxel's neighbor sequence includes a same-phase good neighbor followed by a different-phase neighbor. This is a future Phase 8 regression test addition. + +**Recommendation:** Trust SIMPLNX. The 6.5.171 result was mathematically incorrect. The legacy backport for both D1 and D2 lives in commit `0cad1b6b3` on the local `v6_5_172` branch (see D1). Note: applying only the D1 fix to 6.5.171 without also applying the D2 fix would UNCOVER D2 as new false-positive flips at phase boundaries — both fixes belong together. + +--- + +### EbsdLib 2.4.1 CubicOps precision improvement (precision improvement; not a behavioral deviation in this filter's test data) + +SIMPLNX delegates misorientation math to `ebsdlib::LaueOps::calculateMisorientation` (EbsdLib 2.4.1+); legacy 6.5.171 delegates to `OrientationLib::CubicOps::getMisoQuat` (DREAM3D 6.5.x). The modern API recovers ~0.02° of precision for cubic misorientations that lie on a 4-fold, 3-fold, or 2-fold symmetry op (replacing the precision-fragile `acos(w)` near 1 with the numerically stable `2·atan2(|v|, w)` using explicit reduced-quaternion v components). The improvement is documented in the EbsdLib 2.4.1 release notes (commit `5c8c993` on `/Users/mjackson/Workspace9/EbsdLib`, 2026-05-29). + +**Not observed as a deviation in this filter** because the engineer's test fixtures do not include any voxel pair whose misorientation lands on a cubic sym op. The improvement is real and affects other downstream filters (see `ComputeFeatureFaceMisorientationFilter` V&V cycle's D4); for `BadDataNeighborOrientationCheck` specifically, this is a transparent dependency upgrade. + +--- + +## Comparison artifacts + +Verification fixtures + comparison results are at `/Users/mjackson/Workspace9/DREAM3D_Data/TestFiles/bad_data_neighbor_orientation_check_v2/`: + +- `case_*/case_*_*/case_*_*_cell_arrays.csv` — 27 CSV files, one per algorithmic case. Generated from engineer's hand-derived fixtures per `test_design.md`. +- `case_*/case_*_*/6_5_case_*_*_input.json` — 27 legacy DREAM3D pipelines that generate v7.0 `.dream3d` input + run `BadDataNeighborOrientationCheck` + write output. +- `vv_comparison/output_legacy/6_5_171_case_*.dream3d` — 27 legacy outputs from the official 6.5.171 PipelineRunner (`/Users/mjackson/Applications/DREAM3D.app/Contents/bin/PipelineRunner`). +- `bad_data_neighbor_orientation_check_v2/test_design.md` — engineer's hand-derived expected outputs (the Class 1 oracle SIMPLNX is verified against in Phase 6). +- `bad_data_neighbor_orientation_check_v2/README.md` — engineer's documentation of Issues 1 and 2. + +Comparison script (saved at `/tmp/diff_legacy_vs_simplnx.py`) extracts SIMPLNX expected output from the inline `expectedMask` arrays in `BadDataNeighborOrientationCheckTest.cpp` and diffs against the 6.5.171 outputs. Re-runnable. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureFaceMisorientations.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureFaceMisorientations.md new file mode 100644 index 0000000000..046a366b36 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeFeatureFaceMisorientations.md @@ -0,0 +1,114 @@ +# Deviations from DREAM3D 6.5.171: ComputeFeatureFaceMisorientationFilter + +This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`GenerateFaceMisorientationColoring`, source at `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/GenerateFaceMisorientationColoring.{h,cpp}` in DREAM3D 6.5.171). + +Entries are referenced by stable ID (`ComputeFeatureFaceMisorientations-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. + +All four entries below are **deliberate design changes** made during the rewrite (Nathan Young, 2026-05-19), not bugs in either implementation. D4 additionally documents a related precision fix made in EbsdLib during this V&V cycle (2026-05-28). + +--- + +## ComputeFeatureFaceMisorientations-D1 + +| Field | Value | +|------------------|----------------------------------------| +| **Deviation ID** | `ComputeFeatureFaceMisorientations-D1` | +| **Filter UUID** | `f3473af9-db77-43db-bd25-60df7230ea73` | +| **Status** | active | + +**Symptom:** For faces between two features whose shared phase has a Laue class other than `Hexagonal_High` (m-3m) or `Cubic_High` (6/mmm), SIMPLNX computes a real misorientation value; legacy DREAM3D 6.5.171 wrote `0` (implicit, via the fall-through to the else branch). The nine "new" Laue classes that SIMPLNX now handles are: `Hexagonal_Low` (6/m), `Cubic_Low` (m-3), `Triclinic` (-1), `Monoclinic` (2/m), `OrthoRhombic` (mmm), `Tetragonal_Low` (4/m), `Tetragonal_High` (4/mmm), `Trigonal_Low` (-3), and `Trigonal_High` (-3m). + +**Root cause:** **Library** + **algorithmic choice**. Legacy `GenerateFaceMisorientationColoring.cpp` line 127 explicitly checks `if((m_CrystalStructures[phase1] == Ebsd::CrystalStructure::Hexagonal_High) || (m_CrystalStructures[phase1] == Ebsd::CrystalStructure::Cubic_High))` and only computes the misorientation under that guard. The legacy `OrientationLib` of that era did not have `calculateMisorientation` (or its predecessor `getMisoQuat`) implementations for the other nine Laue classes — they would have returned undefined behavior had the guard been removed. The modern EbsdLib (`vcpkg-installed/.../EbsdLib`) has `calculateMisorientation` implementations for every Laue class, so SIMPLNX dispatches by `laueIndex < m_LaueOrientationOps.size()` instead of hard-coding the two-class enumeration. + +**Affected users:** Any user with surface-mesh face data spanning grain boundaries between non-hexagonal, non-cubic-high phases. Common cases: orthorhombic systems (alpha-uranium, many minerals), monoclinic systems (gypsum, many metals at low symmetry phases), tetragonal systems (TiO2, ZrO2 below transition), trigonal systems (quartz, alpha-corundum). Users running the legacy filter on these systems received silent zeros for every triangle between matched-phase non-hex-high/non-cubic-high features. + +**Recommendation:** **Trust SIMPLNX.** The legacy filter's silent zero for unsupported Laue classes was indistinguishable from "genuine zero misorientation" (a real possibility for aligned grains) — a serious correctness ambiguity that the new explicit handling resolves. Combined with D3 (NaN for invalid faces), users get unambiguous values for every triangle. + +--- + +## ComputeFeatureFaceMisorientations-D2 + +| Field | Value | +|------------------|----------------------------------------| +| **Deviation ID** | `ComputeFeatureFaceMisorientations-D2` | +| **Filter UUID** | `f3473af9-db77-43db-bd25-60df7230ea73` | +| **Status** | active | + +**Symptom:** SIMPLNX output is a 1-component `float32` array — the misorientation angle in degrees per triangle. Legacy DREAM3D 6.5.171 wrote a 3-component `float32` array — the rotation axis (3 components) multiplied componentwise by the angle in degrees, i.e., `(w·n1, w·n2, w·n3)`. The 3-component form encodes both the rotation magnitude AND the axis direction; the 1-component form keeps only the magnitude. + +**Root cause:** **Algorithmic choice** during the rewrite. Decision attributed to Michael Jackson: "bring the output in line with other Misorientation filters in `simplnx`". Most modern misorientation filters in simplnx (KAM, Reference Misorientations, Feature Neighbor Misorientations) return a 1-component magnitude; the legacy 3-component axis·angle form was the outlier. The new form also halves the output memory footprint and simplifies most downstream uses (binning, histogram, threshold). + +**Affected users:** Any user porting a 6.5.171 pipeline that consumed the 3-component output. Common downstream uses: +- **Magnitude-only consumers** (binning for histograms, comparison against a threshold): trivial migration — read component 0 (which used to be `w·n1`) → must change to read the new 1-component (now just `w`). Many pipelines did this by extracting component 0 anyway, which would have been mathematically incorrect for the old format. +- **Axis-and-angle consumers** (visualization with the axis direction encoded in color, downstream filters that need the misorientation rotation axis): not a clean migration — they need the axis explicitly. Workaround: regenerate the axis-angle by running `LaueOps::calculateMisorientation` directly in a custom filter; or back-out the axis from the (legacy) 3-component output by dividing by the angle magnitude. + +**Recommendation:** **Either acceptable per use case.** Neither output is wrong, just different representations of the same calculation. For users needing the rotation axis: open an issue requesting a separate axis output array (could be added as a v3 of the filter via the version-bump mechanism). + +--- + +## ComputeFeatureFaceMisorientations-D3 + +| Field | Value | +|------------------|----------------------------------------| +| **Deviation ID** | `ComputeFeatureFaceMisorientations-D3` | +| **Filter UUID** | `f3473af9-db77-43db-bd25-60df7230ea73` | +| **Status** | active | + +**Symptom:** For faces where the algorithm cannot compute a meaningful misorientation — `frontFeature == 0` (background voxel on the front side), `backFeature == 0` (background on the back side), `frontPhase != backPhase` (mixed-phase boundary), or the shared phase's Laue class is out of EbsdLib's supported range — SIMPLNX writes **`NaN`**. Legacy DREAM3D 6.5.171 wrote `0` for all these cases (in the legacy code: explicit zeros for the mismatched-phase case at lines 143–145; implicit zeros for the unsupported-Laue case via fall-through). + +**Root cause:** **Bug in 6.5.171** (loose categorization), fixed by **algorithmic choice** in SIMPLNX. In 6.5.171 there is no way to differentiate between a true zero misorientation (two grains in perfect crystallographic alignment, mathematically possible) and an unprocessed face. SIMPLNX's explicit NaN makes the distinction unambiguous: + +| Output value | Meaning | +|--------------|--------------------------------------------------------------------------| +| `0.0` | Genuine zero misorientation — the two features are crystallographically aligned (or the misorientation lies on a symmetry op of the shared Laue class) | +| Positive | Computed misorientation angle in degrees | +| `NaN` | The algorithm did not compute a value for this face (any of the four reasons above) | + +**Affected users:** Anyone running pipelines that aggregated misorientation values across all faces (mean, median, histogram). In the legacy filter, the implicit zeros from unprocessed faces silently dragged the aggregate toward zero. SIMPLNX's NaN propagates correctly under standard aggregation rules (NaN-aware aggregations skip NaN; non-aware aggregations propagate to NaN). + +**Recommendation:** **Trust SIMPLNX.** The clear distinction between genuine zero and "no value" is a significant correctness improvement. Downstream consumers should use NaN-aware aggregation (`std::isnan`, `numpy.nanmean`, etc.) or pre-filter faces with NaN before aggregating. + +--- + +## ComputeFeatureFaceMisorientations-D4 + +| Field | Value | +|------------------|----------------------------------------| +| **Deviation ID** | `ComputeFeatureFaceMisorientations-D4` | +| **Filter UUID** | `f3473af9-db77-43db-bd25-60df7230ea73` | +| **Status** | active | + +**Symptom:** For cubic-phase boundaries whose true misorientation lies exactly on a cubic symmetry operator (e.g., 90° rotation about the c-axis is a 4-fold sym op of m-3m), legacy DREAM3D 6.5.171 and pre-fix SIMPLNX returned a small residual misorientation (~0.02°) instead of exactly 0°. Post-fix SIMPLNX returns exactly 0° for these cases. + +The hand-built V&V dataset exposes this at F5↔F7 (Cubic_High features with φ1=0° and φ1=90° about c): expected exactly 0° by symmetry; pre-fix observed 0.0212°; post-fix observed 0.0°. + +**Root cause:** **Precision** in EbsdLib's `CubicOps::calculateMisorientationInternal` (NOT in this filter's code). The algorithm computed `cos(half-angle)` candidates as: + +```cpp +double w_candidate_2 = (qco.z() + qco.w()) / sqrt(2); // type-2 sym op +``` + +then extracted the angle via `acos(w_candidate)`. When `AvgQuats` are stored as **float32** in the input data (the standard SIMPLNX/EbsdLib convention) and promoted to double inside the calculation: + +1. The float32 truncation of `sqrt(2)/2` is `0.7071068f` ≈ `0.70710676908...` as double (off from true `sqrt(2)/2` by ~6e-8). +2. After the symmetry-op reduction, `qco.z` and `qco.w` end up at this float32 value. +3. `(qco.z + qco.w) / sqrt(2)` (where the divisor is the precise double `sqrt(2)`) computes to `1.0 − ~1.71e-8`, NOT exactly 1.0. +4. `acos(1.0 − 1.71e-8)` is in the catastrophic-cancellation regime: the derivative `−1/sqrt(1−x²)` is unbounded as `x → 1`, so a 1.71e-8 perturbation amplifies to ~`1.85e-4` rad in the result. +5. Doubled to full angle and converted to degrees: `~0.0212°` ≈ the observed residual. + +**Fix:** Patch in EbsdLib (`Source/EbsdLib/LaueOps/CubicOps.cpp`). Compute the explicit reduced-quaternion vector components for each of the three sym-op candidates ("type 1/2/3"); pick the candidate with the largest `w`; extract the angle as `2 · atan2(|v|, w)` using `|v|` computed from the explicit reduced-quaternion components, NOT from `sqrt(1 − w²)`. The cancellation that loses precision in the legacy form is *recovered* in the new form because the explicit `v` components include subtractions of identical floats (e.g., `qco.z − qco.w` when `qco.z == qco.w`), which yield exactly 0 in IEEE 754 — regardless of the underlying float32 precision. + +The fix is mathematically equivalent for non-sym-op-aligned misorientations (both forms compute the same angle within ULP for inputs far from the cancellation boundary). It strictly improves precision for inputs on or near a sym op. + +**Affected users:** Anyone who computes misorientations on cubic phases where some grain boundaries land on or near a 4-fold (90° about c), 3-fold (120° about [111]), 2-fold (180° about face), or other cubic symmetry op. These are not pathological — many real-world cubic textures have these as systematic features (e.g., {100} fiber textures align all grains' [001] directions, so 90° about [001] is a frequent boundary). The 0.02° pre-fix residual would have caused: +- Misorientation histograms to have a spurious peak at ~0.02° that should be at 0°. +- Threshold-based grain boundary classification (e.g., "low-angle boundaries < 5°") to silently misclassify some sym-op-aligned boundaries. +- Downstream KAM and GOS calculations to include the residual. + +**Recommendation:** **Trust the fixed SIMPLNX (post-2026-05-28).** The fix is a strict improvement; all 306 EbsdLib unit tests pass post-fix; 181/189 OrientationAnalysis unit tests pass post-fix (the 8 failures are exemplar-based regression tests against pre-fix-generated exemplars, with diffs in the `1e-4` to `1e-3` range — see the V&V doc's "Downstream impact note"). Exemplar files for the 8 affected downstream tests will be regenerated at the engineer's discretion to lock in the post-fix values as the new reference. + +--- + +## Comparison build & library nuance + +This filter's V&V did NOT run a direct A/B comparison against legacy DREAM3D 6.5.171's `GenerateFaceMisorientationColoring`. The output structure is incompatible by design (D2: 3-component axis·angle vs 1-component angle), making a per-array comparison meaningless. The Class 1 oracle (hand-derived from symmetry-group analysis) serves as the verification floor; the deviation entries above document the design choices that distinguish the new filter from the legacy. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md b/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md new file mode 100644 index 0000000000..1e82528caf --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/provenance/BadDataNeighborOrientationCheckFilter.md @@ -0,0 +1,89 @@ +# Exemplar Archive Provenance: `7_bad_data_neighbor_orientation_check.tar.gz` + +This sidecar records how the input data archive used by `BadDataNeighborOrientationCheckFilter`'s unit tests was generated. + +**Important:** Unlike most simplnx test archives, this archive contains **INPUT** `.dream3d` files only — the test "exemplar" outputs are hard-coded `std::array` arrays inline in `test/BadDataNeighborOrientationCheckTest.cpp`, NOT stored as `.dream3d` arrays in the archive. No archive re-upload was needed during this V&V cycle; only inline test-source updates and the SIMPLNX algorithm fix were necessary. + +--- + +## Archive identity + +| Field | Value | +|-------------------|--------------------------------------------------------------------------------------------------------| +| **Archive** | `7_bad_data_neighbor_orientation_check.tar.gz` | +| **SHA512** | `60089eecfe679466f63ef46839f194f83185a5987f51a0e23b9670e50d967ae49451bcfa43c0d44d6fb12cd55b73d208b36825251842d2b2568ffe521be12fbe` | +| **Used by tests** | `OrientationAnalysis::BadDataNeighborOrientationCheckFilter: Case 1.X.Y` (27 algorithmic cases) + `Class 4 Invariants Sweep` (DYNAMIC_SECTIONs over 18 fixtures) + `Class 4 Idempotence` (uses Case 4 input). The `2D Image Fixture` test does NOT consume the archive (inline construction). | +| **Generated by** | Nathan Young (algorithm rewrite + initial dataset, PR #1499, merged 2026-02-02). Engineer's design notes are in `bad_data_neighbor_orientation_check_v2/README.md` and `test_design.md` (bundled with the local archive copy). | +| **Generated on** | 2026-01-28 (per the `README.md` last-modified date in the local archive copy) | + +## How it was generated + +The archive contains a hierarchy of **input-only** `.dream3d` files, one per test case, organized by parameter combination: + +``` +bad_data_neighbor_orientation_check/ +├── case_1/case_1_X/case_1_X_Y/case_1_X_Y_input.dream3d (18 cases, 3×3×3 each) +├── case_2/case_2_X/case_2_X_input.dream3d (6 cases, 5×5×5 each) +├── case_3/case_3_X/case_3_X_input.dream3d (2 cases, 5×5×5 each) +└── case_4/case_4_input.dream3d (1 case, 5×5×5) +``` + +Each `case_*_input.dream3d` encodes: +- An ImageGeom (3×3×3 for Case 1, 5×5×5 for Cases 2/3/4) +- A `Quats` `float32` array (4 components per voxel) with hand-chosen orientations using pure φ1-rotations in degrees (Bunge ZXZ Euler `(φ1, 0, 0)`). The misorientation between any two voxels then equals `|Δφ1|` modulo the c-axis-aligned symmetry of the relevant Laue group — a closed-form derivation independent of any DREAM3D version. +- A `Phases` `int32` array (one entry per voxel) with hand-chosen phase indices (1, 2, or 3) +- A `Mask` `uint8` array (one entry per voxel) marking which voxels are initially "good" (`true`) vs "bad" (`false`) +- A `CrystalStructures` `uint32` ensemble array (typically 2 entries: `[UnknownCrystalStructure=999, Cubic_High=1]`) + +The choice of orientations and initial mask is designed so that, given specific `(MisorientationTolerance, NumberOfNeighbors)` parameter values, the algorithm's iterative-flipping convergence has a predictable, hand-derivable output. The expected output mask is therefore hand-coded in the test source rather than loaded from the archive. Each Case 1.X.Y has a hand-derived `expectedMask` visualization in the test docstring above the literal array, so the analytical derivation is auditable from the test source alone. + +## Canonical oracle output + +| DataPath | Source of expected values | +|---------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `/Image Geometry/Cell Data/Mask` (post-filter) | **Class 1 (Analytical)** — hand-derived from input orientations + tolerance comparison + iterative-flipping convergence. Encoded as `std::array expectedMask = {...}` literals inline in each `TEST_CASE` block, with the docstring above each array depicting the same mask in 3×3 grid form. | +| (no specific path) | **Class 4 (Invariant)** — monotonicity + no-degrade invariants, asserted via `ClassFourInvariants::AssertInvariants()` in `Class 4 Invariants Sweep` and `Class 4 Idempotence` tests. | + +The Class 1 expected values are asserted via `REQUIRE(maskStore.getValue(i) == expectedMask[i])` checks (one per voxel × 27 cases = ~729 assertions for the base cases, plus 125-cell checks for Case 4 and Cases 2/3 = several thousand additional assertions). + +## Oracle provenance + +### Class 1 — Analytical + +The Class 1 oracle lives in `test_design.md` (in the local archive copy at `bad_data_neighbor_orientation_check_v2/test_design.md`) and is mirrored inline as `expectedMask` literals in `test/BadDataNeighborOrientationCheckTest.cpp`. The engineer's derivation for each case is the input-mask + Quats + Phases configuration, the misorientation tolerance check (strict `<`), and the iterative-flipping convergence rule. Closed-form derivable on paper for every case. + +### Class 4 — Invariant + +Two invariants the filter must satisfy for any input configuration: +- **Monotonicity**: the count of `Mask == true` voxels is non-decreasing across one filter run. +- **No-degrade**: no voxel goes from `true` (good) to `false` (bad). + +Both invariants are encoded as a single helper (`namespace ClassFourInvariants` in `BadDataNeighborOrientationCheckTest.cpp`) and asserted from: +- `Class 4 Invariants Sweep` (DYNAMIC_SECTIONs over 18 Case 1.X.Y fixtures, each running the filter and asserting invariants on the resulting mask). +- `Class 4 Idempotence` (runs Case 4 input through the filter twice; second run must produce the same mask as the first). + +### Class 2, 3, 5 + +N/A. This filter delegates misorientation math to `ebsdlib::LaueOps::calculateMisorientation`; the Rowenhorst 2015 paper-based verification of that math is part of EbsdLib's own V&V, not this filter's. This filter consumes that math transitively at the pinned EbsdLib version. + +**EbsdLib version pin:** EbsdLib 2.4.1 (commit `5c8c993` on `/Users/mjackson/Workspace9/EbsdLib`). Recorded here for traceability since SIMPLNX's misorientation results are coupled to this pin. + +## Second-engineer oracle review + +- **Reviewer:** *Pending — recommend Joey Kleingers or another OA-domain engineer review:* + - *The Class 1 hand-derivations in `test_design.md` for plausibility (the 27 cases are small enough to walk through in ~1 hour).* + - *The Class 4 invariant set for completeness — are there other properties this algorithm must satisfy?* + - *The Phase 9 deviation narrative (D1 loop bound + D2 stale `w`) and the determination that the EbsdLib 2.4.1 CubicOps precision improvement is non-observable in this filter's test data.* +- **Date:** *YYYY-MM-DD (pending)* + +## Regenerated to fix a circular-oracle situation? + +**Partially yes** — but the situation was resolved by fixing SIMPLNX, not by regenerating the archive. + +**What happened:** On 2026-05-29 the test-source `expectedMask[13]` values for the 4 Case 1.X.3 fixtures were updated from `0` (matching the engineer's hand-derived oracle in `test_design.md`) to `1` (matching the then-current SIMPLNX output). This was a **circular oracle** — the test was being made to confirm whatever the code produced, regardless of correctness. + +**Root cause:** A SIMPLNX-side bug in the misorientation tolerance computation (float-π precision amplification) caused the 4 boundary-exact Case 1.X.3 fixtures to produce `mask[13] = 1` where the analytical oracle says `0`. The bug was not in the algorithm logic; it was in the tolerance-radians conversion: `numbers::pi_v` is slightly larger than true π, so the float-radian tolerance is ~5e-9 rad larger than `5° × π_true / 180`. Boundary-exact misorientations (those landing at *exactly* the user-supplied tolerance) were inappropriately counted as within-tolerance. + +**Resolution (Phase 6, 2026-05-29):** SIMPLNX's `Algorithms/BadDataNeighborOrientationCheck.cpp` line 41 was changed from `numbers::pi_v` to `numbers::pi_v` (with the variable type also upgraded `float` → `double` so the conversion isn't truncated back to float at assignment). The 4 inline `expectedMask[13]` literals were reverted from `1` back to `0` to match the engineer's analytical oracle. With both changes applied, all 31 tests pass and SIMPLNX is bit-identical to the engineer's `test_design.md` oracle. + +The archive itself was NOT regenerated. The archive's input data was always correct; the issue was a precision bug in SIMPLNX's consumption of that data and a test that had been bent to track the bug. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientations.md b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientations.md new file mode 100644 index 0000000000..3a9b912a93 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/provenance/ComputeFeatureFaceMisorientations.md @@ -0,0 +1,59 @@ +# Exemplar Archive Provenance: Inlined in Test + +This sidecar records how the test data used by `ComputeFeatureFaceMisorientationFilter`'s Class 1 unit test 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. + +--- + +## Archive identity + +| Field | Value | +|-------------------|----------------------------------------------------------------------------------------------------------------| +| **Archive** | Inlined (no separate archive) | +| **SHA512** | N/A | +| **Used by tests** | `OrientationAnalysis::ComputeFeatureFaceMisorientationFilter: Curated Data` | +| **Generated by** | Michael Jackson (dataset structure + first 10 Laue classes) — Claude (Trigonal_High extension, 2026-05-28) | +| **Generated on** | 2026-05-01 (initial dataset); 2026-05-28 (Trigonal_High added) | + +## How it was generated + +The dataset is a hand-rolled triangulated mesh designed as a **Class 1 (Analytical) oracle** that systematically covers all 11 EbsdLib Laue classes plus all four invalid-face scenarios: + +1. **Mesh structure:** 102 + 9 = 111 vertices, arranged in y-axis-stacked blocks of 9 vertices per "row" (one row per Laue class block); 34 + 3 = 37 triangles, each with its own 3 dedicated vertices (no shared edges or vertices between triangles). The algorithm reads only `FaceLabels`, not vertex coordinates, so the geometric layout is arbitrary; the row-stacked structure is purely for human readability. + +2. **Feature structure:** 41 + 4 = 45 features (indices 0–44). Feature 0 is a sentinel/unused (phase `999`). Features 1–4 are phase 1 (Hex_High), 5–8 phase 2 (Cubic_High), ..., 37–40 phase 10 (Trigonal_Low), 41–44 phase 11 (Trigonal_High, added during this V&V cycle). + +3. **Ensemble structure:** 12 + 1 = 13 phase entries (index 0 = sentinel `999`; indices 1–11 = Laue-class indices 0–10). Phase `i` has `CrystalStructure[i] = i - 1` for `i = 1..11`, so phase 1 → CrystalStructure 0 (Hex_High), phase 2 → CrystalStructure 1 (Cubic_High), ..., phase 11 → CrystalStructure 10 (Trigonal_High). + +4. **Orientations (`AvgEulerAngles`):** Each Laue class block uses four orientations: `(φ1, Φ, φ2) = (0°, 0°, 0°)`, `(45°, 0°, 0°)`, `(90°, 0°, 0°)`, `(180°, 0°, 0°)`. With `Φ = φ2 = 0`, these are pure rotations about the c-axis. The choice is deliberate: pure c-axis rotations allow closed-form symmetry-group reduction (the c-axis is the principal symmetry axis of each Laue class), so the expected misorientation is just `|Δφ1|` modulo the c-axis n-fold rotation. + +5. **Face labels:** 34 + 3 = 37 entries. The first 30 entries are organized as Laue-class blocks of 3 boundary types: A↔B (φ1 0° vs 45°), A↔C (0° vs 90°), A↔D (0° vs 180°). Entries 30–33 are the four invalid-face edge cases: (0, 1) background-front, (1, 0) background-back, (1, 5) different-phase forward, (5, 1) different-phase reverse. Entries 34–36 are the Trigonal_High boundaries added during this V&V cycle. + +6. **Expected values:** Derived in closed form from the c-axis n-fold rotation of each Laue class. See the V&V report's Oracle table for the full per-class derivation. The Trigonal_High values (45°, 30°, 60°) match Trigonal_Low — the additional mirror planes in -3m do not reduce pure c-axis rotation magnitudes further. + +7. **Conversion path in the test:** The Euler angles are stored in degrees as `AvgEulerAngles`. The test first runs `ChangeAngleRepresentationFilter` (degrees → radians), then `ConvertOrientationsFilter` (Euler → quaternion, writing `AvgQuats`), then `ComputeFeatureFaceMisorientationFilter` itself. This conversion chain mirrors the standard SIMPLNX EBSD pipeline, so the test exercises the real production code path for converting hand-specified Euler angles into the quaternion format the algorithm consumes. + +The dataset is inlined in `src/Plugins/OrientationAnalysis/test/ComputeFeatureFaceMisorientationTest.cpp` namespace `curated` (function `CreateTestDataStructure()`). + +## Canonical oracle output + +| DataPath | Source of expected values | +|----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `/triangle_geom/FaceData/NXFaceMisorientationColors` | Class 1 analytical (closed-form symmetry-group reduction). Hand-derived per Laue class; see the V&V report's "Applied (Class 1)" table for the full per-class values and per-face expected misorientation. | + +The expected values are hard-coded into the test as `REQUIRE(::CompareFloats(faceMisorientations[i], f))` checks (37 such asserts + `std::isnan` checks for the 4 edge cases). + +## Oracle provenance (Classes 2, 3, 5 only) + +N/A — Class 1 oracle. + +## Second-engineer oracle review + +- **Reviewer:** *Pending — recommend Joey Kleingers or another OA-domain engineer review the symmetry-group hand-calculations for all 11 Laue classes (especially the Trigonal_High → Trigonal_Low equivalence claim and the Cubic_High F5↔F7 → 0° claim post-EbsdLib-precision-fix).* +- **Date:** *YYYY-MM-DD (pending)* +- **Skip reason** (if skipped): *N/A — second-engineer review is recommended; not yet performed.* + +## 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-rewrite test used the unrelated `6_6_Small_IN100_GBCD.tar.gz` archive as a regression-against-reference exemplar; that test was retired when the algorithm was rewritten to a 1-component output that's structurally incompatible with the old archive's 3-component reference.) diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index 8a698edfcd..3f783b444f 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -8,7 +8,7 @@ { "kind": "git", "repository": "https://github.com/bluequartzsoftware/simplnx-registry", - "baseline": "ca7046ad28b4885b018e4ab5fcf43333460d82b2", + "baseline": "b9f4d4c072f0ffc3291378dc03c4f6b38f0b4743", "packages": [ "benchmark", "blosc", diff --git a/vcpkg.json b/vcpkg.json index a8576133a1..d843331ef4 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -83,7 +83,7 @@ "dependencies": [ { "name": "ebsdlib", - "version>=": "2.4.0" + "version>=": "2.4.1" } ] }, diff --git a/wrapping/python/examples/pipelines/OrientationAnalysis/03_Small_IN100_Mesh_Statistics.py b/wrapping/python/examples/pipelines/OrientationAnalysis/03_Small_IN100_Mesh_Statistics.py index 4d208e05b7..4e7b544b64 100644 --- a/wrapping/python/examples/pipelines/OrientationAnalysis/03_Small_IN100_Mesh_Statistics.py +++ b/wrapping/python/examples/pipelines/OrientationAnalysis/03_Small_IN100_Mesh_Statistics.py @@ -78,7 +78,7 @@ crystal_structures_array_path=nx.DataPath("DataContainer/Cell Ensemble Data/CrystalStructures"), feature_phases_array_path=nx.DataPath("DataContainer/Cell Feature Data/Phases"), surface_mesh_face_labels_array_path=nx.DataPath("TriangleDataContainer/Face Data/FaceLabels"), - surface_mesh_face_misorientation_colors_array_name="FaceMisorientationColors" + misorientation_array_name="FaceMisorientationColors" ) nxtest.check_filter_result(nx_filter, result)