Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ Processing (Crystallography)

This **Filter** generates a pair of colors for each **Triangle** in a **Triangle Geometry** based on the inverse pole figure (IPF) color scheme for the present crystal structure. Each **Triangle** has 2 colors since any **Face** sits at a boundary between 2 **Features** for a well-connected set of **Features** that represent _grains_. The reference direction used for the IPF color generation is the _normal_ of the **Triangle**.

Each side of a **Face** is colored using **that side's own** feature: the first color uses the first feature's orientation, its phase's crystal symmetry (Laue group), and the face normal; the second color uses the second feature's orientation, **its own** phase's crystal symmetry, and the negated (inward) face normal. This matters for multi-phase microstructures where the two features adjacent to a **Face** belong to phases with different Laue groups (for example a cubic phase against a hexagonal phase) — each side is colored with the correct symmetry for its phase. A side whose feature is invalid (a boundary/exterior face, feature id ≤ 0) is colored black.

> **Note on migration from DREAM3D 6.5.171:** earlier DREAM3D 6.5.171 colored the *second* side of every **Face** using the *first* feature's crystal symmetry, which produced incorrect colors on mixed-Laue-group faces and left the second color black on faces whose first side was the exterior. DREAM3D-NX (and DREAM3D 6.5.172) compute each side with its own phase's symmetry. See V&V deviation `ComputeFaceIPFColoringFilter-D1`.

------------

![Face IPF Coloring](Images/ComputeFaceIPFColoring.png)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,12 +106,8 @@ class CalculateFaceIPFColorsImpl
// Now compute for Phase 2
if(phase2 > 0)
{
// KNOWN BUG — tracked at https://github.com/BlueQuartzSoftware/simplnx/issues/1635.
// Both the validity guard and the IPF operator lookup below use phase1 instead of phase2,
// assigning Phase 1's symmetry operator to Phase 2's Euler angles on mixed-phase faces.
// The 2-line fix (phase1→phase2) is held pending a V&V cycle that will regenerate the
// test exemplar (which currently encodes the bug — a circular oracle).
if(m_CrystalStructures[phase1] < ebsdlib::CrystalStructure::LaueGroupEnd)
// Make sure we are using a valid Euler Angles with valid crystal symmetry
if(m_CrystalStructures[phase2] < ebsdlib::CrystalStructure::LaueGroupEnd)
{
dEuler[0] = m_Eulers[3 * feature2 + 0];
dEuler[1] = m_Eulers[3 * feature2 + 1];
Expand All @@ -120,7 +116,7 @@ class CalculateFaceIPFColorsImpl
refDir[1] = -m_Normals[3 * i + 1];
refDir[2] = -m_Normals[3 * i + 2];

argb = ops[m_CrystalStructures[phase1]]->generateIPFColor(dEuler, refDir, false, m_ColorKey);
argb = ops[m_CrystalStructures[phase2]]->generateIPFColor(dEuler, refDir, false, m_ColorKey);
m_SecondColors[3 * i + 0] = RgbColor::dRed(argb);
m_SecondColors[3 * i + 1] = RgbColor::dGreen(argb);
m_SecondColors[3 * i + 2] = RgbColor::dBlue(argb);
Expand Down Expand Up @@ -184,6 +180,10 @@ Result<> ComputeFaceIPFColoring::operator()()
ParallelDataAlgorithm parallelTask;
parallelTask.setRange(0, numTriangles);
parallelTask.requireArraysInMemory(algArrays);
// Per the project thread-safety policy, DataArray/DataStore access is not thread-safe even when each
// thread writes distinct indices, so parallelization stays disabled (same disposition as the
// ComputeFeatureFaceMisorientations V&V).
parallelTask.setParallelizationEnabled(false);
parallelTask.execute(CalculateFaceIPFColorsImpl(faceLabels, phases, faceNormals, eulerAngles, crystalStructures, firstIpfColors, secondIpfColors, m_InputValues->ColorKey));

return {};
Expand Down
168 changes: 134 additions & 34 deletions src/Plugins/OrientationAnalysis/test/ComputeFaceIPFColoringTest.cpp
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
#include "OrientationAnalysis/Filters/ComputeFaceIPFColoringFilter.hpp"
#include "OrientationAnalysis/OrientationAnalysis_test_dirs.hpp"

#include "simplnx/Common/Constants.hpp"
#include "simplnx/Core/Application.hpp"
#include "simplnx/DataStructure/DataArray.hpp"
#include "simplnx/DataStructure/DataGroup.hpp"
#include "simplnx/DataStructure/DataStructure.hpp"
#include "simplnx/Parameters/ArrayCreationParameter.hpp"
#include "simplnx/Parameters/ChoicesParameter.hpp"
#include "simplnx/Pipeline/Pipeline.hpp"
Expand All @@ -11,6 +15,7 @@
#include <catch2/catch.hpp>

#include <algorithm>
#include <array>
#include <filesystem>
#include <fstream>

Expand All @@ -20,7 +25,6 @@ namespace fs = std::filesystem;

namespace
{
constexpr StringLiteral k_FaceIPFColors("SurfaceMeshFaceIPFColors");
constexpr StringLiteral k_FirstNXFaceIPFColors("NXFaceIPFColors 0");
constexpr StringLiteral k_SecondNXFaceIPFColors("NXFaceIPFColors 1");

Expand All @@ -39,57 +43,153 @@ DataPath faceNormals = faceDataGroup.createChildPath(nx::core::Constants::k_Face
DataPath faceAreas = faceDataGroup.createChildPath(nx::core::Constants::k_FaceAreas);
} // namespace

TEST_CASE("OrientationAnalysis::ComputeFaceIPFColoringFilter: Valid filter execution", "[OrientationAnalysis][ComputeFaceIPFColoringFilter]")
// =============================================================================
// Class 1 (Analytical) oracle for issue #1635.
//
// The filter colors each side of a surface-mesh face with the IPF color of the
// adjacent feature: the FIRST color uses feature1's orientation + the face
// normal; the SECOND color uses feature2's orientation + the *negated* face
// normal. Each side must use ITS OWN phase's Laue symmetry operator.
//
// Oracle: IPF colors are a closed-form function of (orientation, reference
// direction, Laue symmetry). At the standard-triangle corners the color is a
// pure primary, independent of any implementation detail (EbsdLib 3.0.0,
// TSL color key):
// - Cubic-high <100> -> (255, 0, 0) (red corner)
// - Cubic-high <111> -> ( 0, 0,255) (blue corner)
// - Hex-high c-axis -> (255, 0, 0) (red corner)
// - Hex-high basal -> ( 0,255, 0) (green corner; RED channel is exactly
// 0 because a basal direction sits at
// chi = chiMax, so r = 1 - chi/chiMax = 0)
// The crystal-structure enum values that index ops[] are Hexagonal_High = 0,
// Cubic_High = 1 (EbsdLib/Core/EbsdLibConstants.h).
//
// Every feature carries a DISTINCT orientation so a wrong-Euler-index bug (the
// Euler analogue of the #1635 wrong-phase-index bug) changes the output. The
// orientations are chosen so the expected corner colors remain hand-derivable:
// Feature 1 (cubic): Euler (pi/2, 0, 0) -- a 90-degree rotation about Z. It maps
// <100>-family reference directions to other <100> directions and <111> to
// <111>, so the cubic corner colors are IDENTICAL to the identity's. The
// derivation is insensitive to the active/passive convention (both give a
// <100>/<111> family member).
// Feature 2 (hex): Euler (pi/3, 0, 0) -- a 60-degree rotation about c, which
// is a 6/mmm symmetry operation, so every IPF color is IDENTICAL to the
// identity's (again convention-insensitive).
// Feature 3 (cubic): identity (0, 0, 0), distinct from feature 1.
// If the algorithm ever reads the wrong feature's Euler angles (e.g. feature1's
// on the phase-2 side), the hex side sees a 90-degree Z rotation instead of its
// own 60-degree one: reference (1,0,0) then lands ~30 degrees away in eta -- the
// opposite corner of the hex wedge -- and the exact-value assertions fail.
//
// The fixture builds five faces:
// Face 0 labels (1,2) n=(-1,0,0) mixed cubic/hex -- the headline discriminator
// Face 1 labels (-1,2) n=(-1,0,0) feature1 invalid -- boundary discriminator
// Face 2 labels (1,-1) n=(-1,0,0) feature2 invalid -- control (second black)
// Face 3 labels (1,3) n=(1,1,1) same-phase cubic -- control (bug cannot manifest)
// Face 4 labels (2,1) n=(0,0,1) hex first side -- hex c-axis corner (red)
//
// Why each side's expected color is what it is (FIX = correct behavior):
// F0 first = cubic Rz(90)(-1,0,0) = <100> red = (255, 0, 0)
// F0 second = hex Rz(60)(-(-1,0,0)=(1,0,0)) basal = ( 0,255, 0) [BUG -> cubic ops = red]
// F1 first = black (feature1 = -1 -> phase1 = 0) = ( 0, 0, 0)
// F1 second = hex Rz(60)(1,0,0) basal = ( 0,255, 0) [BUG -> guard reads CrystalStructures[0]
// sentinel, fails, leaves black (0,0,0)]
// F2 first = cubic Rz(90)(-1,0,0) = <100> red = (255, 0, 0)
// F2 second = black (feature2 = -1 -> phase2 = 0) = ( 0, 0, 0)
// F3 first = cubic Rz(90)(1,1,1) = <111> blue = ( 0, 0,255)
// F3 second = cubic identity(-(1,1,1)) = <111> blue = ( 0, 0,255) [phase1==phase2, bug cannot manifest]
// F4 first = hex Rz(60)(0,0,1) = c-axis = (255, 0, 0) [chi = 0 -> pure red corner]
// F4 second = cubic Rz(90)(-(0,0,1)) = <100> red = (255, 0, 0)
//
// Faces 0 and 1 are the discriminators: under the pre-fix bug both their SECOND
// colors are wrong (F0 -> red, F1 -> black); after the fix they are green.
TEST_CASE("OrientationAnalysis::ComputeFaceIPFColoringFilter: Class 1 Oracle - mixed-phase analytical", "[OrientationAnalysis][ComputeFaceIPFColoringFilter]")
{
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");
// EbsdLib crystal-structure enum values used as ops[] indices.
constexpr uint32 k_HexHigh = 0; // Hexagonal_High (6/mmm)
constexpr uint32 k_CubicHigh = 1; // Cubic_High (m-3m)
constexpr uint32 k_Unknown = 999; // UnknownCrystalStructure sentinel

DataStructure dataStructure;
auto* topGroup = DataGroup::Create(dataStructure, "Data");

const DataPath faceLabelsPath({"Data", "FaceLabels"});
const DataPath faceNormalsPath({"Data", "FaceNormals"});
const DataPath eulerAnglesPath({"Data", "FeatureEulerAngles"});
const DataPath phasesPath({"Data", "FeaturePhases"});
const DataPath crystalStructuresPath({"Data", "CrystalStructures"});

// 5 faces, 2-component (feature1, feature2)
auto* faceLabelsArray = UnitTest::CreateTestDataArray<int32>(dataStructure, "FaceLabels", {5}, {2}, topGroup->getId());
const std::vector<int32> faceLabelValues = {1, 2, -1, 2, 1, -1, 1, 3, 2, 1};
std::copy(faceLabelValues.begin(), faceLabelValues.end(), faceLabelsArray->begin());

// 5 faces, 3-component face normals (not normalized; EbsdLib normalizes internally)
auto* faceNormalsArray = UnitTest::CreateTestDataArray<float64>(dataStructure, "FaceNormals", {5}, {3}, topGroup->getId());
const std::vector<float64> faceNormalValues = {-1.0, 0.0, 0.0, -1.0, 0.0, 0.0, -1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0};
std::copy(faceNormalValues.begin(), faceNormalValues.end(), faceNormalsArray->begin());

// 4 features (index 0 unused). Distinct orientations per feature (see the derivation above):
// feature 1 (cubic): (pi/2, 0, 0) feature 2 (hex): (pi/3, 0, 0) feature 3 (cubic): identity
auto* eulerAngles = UnitTest::CreateTestDataArray<float32>(dataStructure, "FeatureEulerAngles", {4}, {3}, topGroup->getId());
std::fill(eulerAngles->begin(), eulerAngles->end(), 0.0F);
(*eulerAngles)[3 * 1 + 0] = nx::core::Constants::k_PiOver2F; // feature 1: phi1 = 90 degrees
(*eulerAngles)[3 * 2 + 0] = nx::core::Constants::k_PiOver3F; // feature 2: phi1 = 60 degrees (hex symmetry op)

// Feature phases: feature 1 -> phase 1 (cubic), feature 2 -> phase 2 (hex), feature 3 -> phase 1 (cubic)
auto* phases = UnitTest::CreateTestDataArray<int32>(dataStructure, "FeaturePhases", {4}, {1}, topGroup->getId());
const std::vector<int32> phaseValues = {0, 1, 2, 1};
std::copy(phaseValues.begin(), phaseValues.end(), phases->begin());

// Crystal structures per phase: phase 0 -> unknown, phase 1 -> cubic-high, phase 2 -> hex-high
auto* crystalStructures = UnitTest::CreateTestDataArray<uint32>(dataStructure, "CrystalStructures", {3}, {1}, topGroup->getId());
(*crystalStructures)[0] = k_Unknown;
(*crystalStructures)[1] = k_CubicHigh;
(*crystalStructures)[2] = k_HexHigh;

// 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
ComputeFaceIPFColoringFilter filter;
Arguments args;

// Create default Parameters for the filter.
args.insertOrAssign(ComputeFaceIPFColoringFilter::k_SurfaceMeshFaceLabelsArrayPath_Key, std::make_any<DataPath>(faceLabels));
args.insertOrAssign(ComputeFaceIPFColoringFilter::k_SurfaceMeshFaceNormalsArrayPath_Key, std::make_any<DataPath>(faceNormals));
args.insertOrAssign(ComputeFaceIPFColoringFilter::k_FeatureEulerAnglesArrayPath_Key, std::make_any<DataPath>(avgEulerAnglesPath));
args.insertOrAssign(ComputeFaceIPFColoringFilter::k_FeaturePhasesArrayPath_Key, std::make_any<DataPath>(featurePhasesPath));
args.insertOrAssign(ComputeFaceIPFColoringFilter::k_CrystalStructuresArrayPath_Key, std::make_any<DataPath>(crystalStructurePath));
args.insertOrAssign(ComputeFaceIPFColoringFilter::k_SurfaceMeshFaceLabelsArrayPath_Key, std::make_any<DataPath>(faceLabelsPath));
args.insertOrAssign(ComputeFaceIPFColoringFilter::k_SurfaceMeshFaceNormalsArrayPath_Key, std::make_any<DataPath>(faceNormalsPath));
args.insertOrAssign(ComputeFaceIPFColoringFilter::k_FeatureEulerAnglesArrayPath_Key, std::make_any<DataPath>(eulerAnglesPath));
args.insertOrAssign(ComputeFaceIPFColoringFilter::k_FeaturePhasesArrayPath_Key, std::make_any<DataPath>(phasesPath));
args.insertOrAssign(ComputeFaceIPFColoringFilter::k_CrystalStructuresArrayPath_Key, std::make_any<DataPath>(crystalStructuresPath));
args.insertOrAssign(ComputeFaceIPFColoringFilter::k_FirstFaceIPFColorsArrayName_Key, std::make_any<std::string>(::k_FirstNXFaceIPFColors));
args.insertOrAssign(ComputeFaceIPFColoringFilter::k_SecondFaceIPFColorsArrayName_Key, std::make_any<std::string>(::k_SecondNXFaceIPFColors));
args.insertOrAssign(ComputeFaceIPFColoringFilter::k_ColorKey_Key, std::make_any<ChoicesParameter::ValueType>(0)); // TSL

// Preflight the filter and check result
auto preflightResult = filter.preflight(dataStructure, args);
SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions);

// Execute the filter and check the result
auto executeResult = filter.execute(dataStructure, args);
SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result);

// compare the resulting face IPF Colors array
DataPath exemplarPath = faceDataGroup.createChildPath(::k_FaceIPFColors);
const auto& exemplarArray = dataStructure.getDataRefAs<UInt8Array>(exemplarPath);
DataPath firstGeneratedPath = faceDataGroup.createChildPath(::k_FirstNXFaceIPFColors);
const auto& firstArray = dataStructure.getDataRefAs<UInt8Array>(firstGeneratedPath);
DataPath secondGeneratedPath = faceDataGroup.createChildPath(::k_SecondNXFaceIPFColors);
const auto& secondArray = dataStructure.getDataRefAs<UInt8Array>(secondGeneratedPath);
const auto& first = dataStructure.getDataRefAs<UInt8Array>(faceLabelsPath.replaceName(::k_FirstNXFaceIPFColors));
const auto& second = dataStructure.getDataRefAs<UInt8Array>(faceLabelsPath.replaceName(::k_SecondNXFaceIPFColors));

// Done this way because original output was segmented into separate arrays for vis
for(usize i = 0; i < exemplarArray.getNumberOfTuples(); i++)
{
REQUIRE(exemplarArray[(i * 6) + 0] == firstArray[(i * 3) + 0]);
REQUIRE(exemplarArray[(i * 6) + 1] == firstArray[(i * 3) + 1]);
REQUIRE(exemplarArray[(i * 6) + 2] == firstArray[(i * 3) + 2]);
// Expected colors (the FIX = correct behavior). Each row is one face.
const std::array<std::array<uint8, 3>, 5> expectedFirst = {{{255, 0, 0}, {0, 0, 0}, {255, 0, 0}, {0, 0, 255}, {255, 0, 0}}};
const std::array<std::array<uint8, 3>, 5> expectedSecond = {{{0, 255, 0}, {0, 255, 0}, {0, 0, 0}, {0, 0, 255}, {255, 0, 0}}};

REQUIRE(exemplarArray[(i * 6) + 3] == secondArray[(i * 3) + 0]);
REQUIRE(exemplarArray[(i * 6) + 4] == secondArray[(i * 3) + 1]);
REQUIRE(exemplarArray[(i * 6) + 5] == secondArray[(i * 3) + 2]);
for(usize face = 0; face < 5; face++)
{
INFO(fmt::format("face {}", face));
REQUIRE(first[face * 3 + 0] == expectedFirst[face][0]);
REQUIRE(first[face * 3 + 1] == expectedFirst[face][1]);
REQUIRE(first[face * 3 + 2] == expectedFirst[face][2]);

REQUIRE(second[face * 3 + 0] == expectedSecond[face][0]);
REQUIRE(second[face * 3 + 1] == expectedSecond[face][1]);
REQUIRE(second[face * 3 + 2] == expectedSecond[face][2]);
}

// Class 4 companion invariant: the Phase-2 (hex) side of a basal-plane face has
// a RED channel of exactly 0. The pre-fix bug colored it with cubic <100> ops,
// which produces (255,0,0) -- a non-zero red channel. This single byte is the
// crispest signature of the wrong-Laue-operator bug.
REQUIRE(second[0 * 3 + 0] == 0); // Face 0 hex second color, red channel

UnitTest::CheckArraysInheritTupleDims(dataStructure);
}

Expand Down
Loading
Loading