diff --git a/src/Plugins/OrientationAnalysis/docs/ConvertOrientationsFilter.md b/src/Plugins/OrientationAnalysis/docs/ConvertOrientationsFilter.md index 0b20a08880..856cd5c596 100644 --- a/src/Plugins/OrientationAnalysis/docs/ConvertOrientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/ConvertOrientationsFilter.md @@ -84,12 +84,22 @@ Orientation Matrix: Determinant must be +1.0 ### Data Conversion Notes -If the angles fall outside of this range the **original** Euler Input data **WILL BE CHANGED** to ensure they are within this range. +This **Filter** does not modify the input array; it creates a new output array holding the converted representation. The input orientation values are used as supplied. (DREAM3D 6.5.171 *performed* an in-place Euler range-normalization step that both mutated the input array and — because it applied `fmod(Phi, pi)` and sign flips that are not rotation-preserving — could change the converted orientation for Euler input outside the standard Bunge ranges. That step is deliberately not performed here; see deviation D5.) + +## Algorithm Reference + +The transformation equations between all representations follow: + +> D. Rowenhorst, A. D. Rollett, G. S. Rohrer, M. Groeber, M. Jackson, P. J. Konijnenberg, and M. De Graef, "Consistent representations of and conversions between 3D rotations," *Modelling and Simulation in Materials Science and Engineering* **23**(8), 083501 (2015). DOI: [10.1088/0965-0393/23/8/083501](https://doi.org/10.1088/0965-0393/23/8/083501) + +The conversions themselves are implemented in EbsdLib and are verified against this reference by EbsdLib's own test suite. ## Precision Notes While every effort has been made to ensure the correctness of each transformation algorithm, certain situations may arise where the initial precision of the input data is not large enough for the algorithm to calculate an answer that is intuitive. The user should be acutely aware of their input data and if their data may cause these situations to occur. Combinations of Euler angles close to 0, 180 and 360 can cause these issues to be hit. For instance an Euler angle of [180, 56, 360] is symmetrically the same as [180, 56, 0] and due to calculation errors and round off errors converting that Euler angle between representations may not give the numerical answer the user was anticipating but will give a symmetrically equivalent angle. +The input array must be of type **float32**. Conversions are performed in float32 precision. (DREAM3D 6.5.171 additionally accepted float64 input arrays; see the V&V deviations for this filter.) + % Auto generated parameter table will be inserted here ## Example Pipelines diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ConvertOrientations.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ConvertOrientations.cpp index 9dde1105f0..84bf5f8b3a 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ConvertOrientations.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ConvertOrientations.cpp @@ -1,159 +1,38 @@ #include "ConvertOrientations.hpp" #include "simplnx/DataStructure/DataArray.hpp" -#include "simplnx/DataStructure/DataGroup.hpp" #include "simplnx/Utilities/ParallelDataAlgorithm.hpp" #include -#include #include #include #include -#include -#include -#include +#include -#ifndef _MSC_VER -#pragma clang diagnostic push -#pragma ide diagnostic ignored "UnusedValue" -#endif +#include +#include using namespace nx::core; namespace { - -template -struct EulerCheck -{ - - void operator()(T* euler) const - { - euler[0] = static_cast(std::fmod(euler[0], ebsdlib::constants::k_2PiD)); - euler[1] = static_cast(std::fmod(euler[1], ebsdlib::constants::k_PiD)); - euler[2] = static_cast(std::fmod(euler[2], ebsdlib::constants::k_2PiD)); - - if(euler[0] < 0.0) - { - euler[0] *= static_cast(-1.0); - } - if(euler[1] < 0.0) - { - euler[1] *= static_cast(-1.0); - } - if(euler[2] < 0.0) - { - euler[2] *= static_cast(-1.0); - } - } -}; - -template -struct OrientationMatrixCheck -{ - using OrientationType = ebsdlib::OrientationMatrix; - using ResultType = ebsdlib::ResultType; - - void operator()(T* inPtr) const - { - OrientationType oaType(inPtr); - - ResultType res = oaType.isValid(); - if(res.result <= 0) - { - std::cout << res.msg << std::endl; - printRepresentation(std::cout, inPtr, std::string("Bad OM")); - } - } - void printRepresentation(std::ostream& out, T* om, const std::string& label = std::string("Om")) const - { - out.precision(16); - out << label << om[0] << '\t' << om[1] << '\t' << om[2] << std::endl; - out << label << om[3] << '\t' << om[4] << '\t' << om[5] << std::endl; - out << label << om[6] << '\t' << om[7] << '\t' << om[8] << std::endl; - } -}; - -template -struct QuaternionCheck -{ - using OrientationType = ebsdlib::Quaternion; - using ResultType = ebsdlib::ResultType; - - void operator()(T* inPtr) const - { - // This is a no-op at this point. - } -}; - -template -struct AxisAngleCheck -{ - using OrientationType = ebsdlib::AxisAngle; - using ResultType = ebsdlib::ResultType; - - void operator()(T* inPtr) const - { - // This is a no-op at this point. - } -}; - -template -struct RodriguesCheck -{ - using OrientationType = ebsdlib::Rodrigues; - using ResultType = ebsdlib::ResultType; - - void operator()(T* inPtr) const - { - // This is a no-op at this point. - } -}; - -template -struct HomochoricCheck -{ - using OrientationType = ebsdlib::Homochoric; - using ResultType = ebsdlib::ResultType; - - void operator()(T* inPtr) const - { - // This is a no-op at this point. - } -}; - -template -struct CubochoricCheck -{ - using OrientationType = ebsdlib::Cubochoric; - using ResultType = ebsdlib::ResultType; - - void operator()(T* inPtr) const - { - // This is a no-op at this point. - } -}; - -template -struct StereographicCheck -{ - using OrientationType = ebsdlib::Stereographic; - using ResultType = ebsdlib::ResultType; - - void operator()(T* inPtr) const - { - // This is a no-op at this point. - } -}; - +// Human-readable representation names, indexed by ebsdlib::orientations::Type. +constexpr std::array k_TypeNames = {"Euler", "Orientation Matrix", "Quaternion", "Axis Angle", "Rodrigues", "Homochoric", "Cubochoric", "Stereographic"}; + +// X-macro that generates one per-tuple convertor functor per output representation. Each functor is +// handed to ParallelDataAlgorithm, which splits the tuple range across threads; for tuple i it copies +// the inNumComps input components into an EbsdLib orientation object, calls that object's to() +// conversion, and writes the outNumComps result components back. Cancel is checked per tuple so a large +// conversion can be aborted promptly. The conversion math itself lives in EbsdLib. #define OC_TBB_IMPL(TO_REP) \ template \ class TO_REP##Convertor \ { \ public: \ TO_REP##Convertor(ConvertOrientations* filter, nx::core::DataArray& input, nx::core::DataArray& output) \ - : m_Input(input.getDataStoreRef()) \ + : m_Filter(filter) \ + , m_Input(input.getDataStoreRef()) \ , m_Output(output.getDataStoreRef()) \ { \ } \ @@ -164,6 +43,10 @@ struct StereographicCheck size_t outNumComps = m_Output.getNumberOfComponents(); \ for(size_t i = r.min(); i < r.max(); ++i) \ { \ + if(m_Filter->shouldCancel()) \ + { \ + return; \ + } \ size_t inOffset = i * inNumComps; \ size_t outOffset = i * outNumComps; \ for(size_t c = 0; c < inNumComps; c++) \ @@ -176,9 +59,11 @@ struct StereographicCheck m_Output[outOffset + c] = outputInstance[c]; \ } \ } \ + m_Filter->sendThreadSafeProgressMessage(r.max() - r.min()); \ } \ \ private: \ + ConvertOrientations* m_Filter = nullptr; \ AbstractDataStore& m_Input; \ AbstractDataStore& m_Output; \ }; @@ -207,27 +92,47 @@ ConvertOrientations::ConvertOrientations(DataStructure& dataStructure, const IFi ConvertOrientations::~ConvertOrientations() noexcept = default; // ----------------------------------------------------------------------------- -Result<> ConvertOrientations::operator()() +bool ConvertOrientations::shouldCancel() const +{ + return m_ShouldCancel; +} + +// ----------------------------------------------------------------------------- +void ConvertOrientations::sendThreadSafeProgressMessage(usize counter) { - using ValidateInputDataFunctionType = std::function; + std::lock_guard guard(m_ProgressMessage_Mutex); + m_ProgressCounter += counter; + auto now = std::chrono::steady_clock::now(); + if(std::chrono::duration_cast(now - m_InitialPoint).count() < 1000) + { + return; + } + + auto progressInt = static_cast((static_cast(m_ProgressCounter) / static_cast(m_TotalPoints)) * 100.0f); + m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Converting Orientations: {}% Complete", progressInt)); + m_InitialPoint = std::chrono::steady_clock::now(); +} + +// ----------------------------------------------------------------------------- +Result<> ConvertOrientations::operator()() +{ DataPath outputDataPath = m_InputValues->InputOrientationArrayPath.replaceName(m_InputValues->OutputOrientationArrayName); - auto inputArray = m_DataStructure.getDataRefAs(m_InputValues->InputOrientationArrayPath); - auto outputArray = m_DataStructure.getDataRefAs(outputDataPath); + auto& inputArray = m_DataStructure.getDataRefAs(m_InputValues->InputOrientationArrayPath); + auto& outputArray = m_DataStructure.getDataRefAs(outputDataPath); size_t totalPoints = inputArray.getNumberOfTuples(); + m_TotalPoints = totalPoints; - const ValidateInputDataFunctionType euCheck = EulerCheck(); - const ValidateInputDataFunctionType omCheck = OrientationMatrixCheck(); - const ValidateInputDataFunctionType quCheck = QuaternionCheck(); - const ValidateInputDataFunctionType axCheck = AxisAngleCheck(); - const ValidateInputDataFunctionType roCheck = RodriguesCheck(); - const ValidateInputDataFunctionType hoCheck = HomochoricCheck(); - const ValidateInputDataFunctionType cuCheck = CubochoricCheck(); - const ValidateInputDataFunctionType stCheck = StereographicCheck(); + m_MessageHandler(IFilter::Message::Type::Info, fmt::format("Converting {} orientations from {} to {}", totalPoints, k_TypeNames[static_cast(m_InputValues->InputType)], + k_TypeNames[static_cast(m_InputValues->OutputType)])); - // Allow data-based parallelization + // Allow data-based parallelization; require both arrays resident in memory for out-of-core stores. ParallelDataAlgorithm parallelAlgorithm; parallelAlgorithm.setRange(0, totalPoints); + IParallelAlgorithm::AlgorithmArrays algArrays; + algArrays.push_back(&inputArray); + algArrays.push_back(&outputArray); + parallelAlgorithm.requireArraysInMemory(algArrays); if(m_InputValues->OutputType == ebsdlib::orientations::Type::Euler) { diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ConvertOrientations.hpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ConvertOrientations.hpp index ed0173b4db..4d1a408520 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ConvertOrientations.hpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ConvertOrientations.hpp @@ -11,7 +11,9 @@ #include +#include #include +#include namespace nx::core { @@ -19,8 +21,6 @@ namespace nx::core namespace convert_orientations_constants { // Error Code constants -constexpr int32 k_InputRepresentationTypeError = -67001; -constexpr int32 k_OutputRepresentationTypeError = -67002; constexpr int32 k_InputComponentDimensionError = -67003; constexpr int32 k_InputComponentCountError = -67004; constexpr int32 k_MatchingTypesError = -67005; @@ -52,11 +52,30 @@ class ORIENTATIONANALYSIS_EXPORT ConvertOrientations Result<> operator()(); + /** + * @brief Returns true if the user has requested the filter be cancelled. Safe to call from the + * parallel convertor workers. + */ + bool shouldCancel() const; + + /** + * @brief Mutex-protected, time-throttled progress reporter. The parallel convertor workers call + * this once per processed chunk; messages are emitted at most ~once per second. + * @param counter Number of tuples completed since the last call. + */ + void sendThreadSafeProgressMessage(usize counter); + private: DataStructure& m_DataStructure; const ConvertOrientationsInputValues* m_InputValues = nullptr; const std::atomic_bool& m_ShouldCancel; const IFilter::MessageHandler& m_MessageHandler; + + // Thread safe Progress Message + std::chrono::steady_clock::time_point m_InitialPoint = std::chrono::steady_clock::now(); + mutable std::mutex m_ProgressMessage_Mutex; + size_t m_TotalPoints = 0; + size_t m_ProgressCounter = 0; }; } // namespace nx::core diff --git a/src/Plugins/OrientationAnalysis/test/ConvertOrientationsTest.cpp b/src/Plugins/OrientationAnalysis/test/ConvertOrientationsTest.cpp index 816fee8e30..1752dceeb5 100644 --- a/src/Plugins/OrientationAnalysis/test/ConvertOrientationsTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ConvertOrientationsTest.cpp @@ -34,13 +34,62 @@ #include #include +#include #include +#include #include #include using namespace nx::core; namespace fs = std::filesystem; +namespace +{ +// Component counts per representation, indexed by ebsdlib::orientations::Type +// (Euler, OrientationMatrix, Quaternion, AxisAngle, Rodrigues, Homochoric, Cubochoric, Stereographic). +constexpr std::array k_Comps = {3, 9, 4, 4, 4, 3, 3, 3}; +const std::vector k_RepNames = {"Euler", "OrientationMatrix", "Quaternion", "AxisAngle", "Rodrigues", "Homochoric", "Cubochoric", "Stereographic"}; + +// V&V dispatch landmarks: 3 distinct general orientations, each expressed in all 8 representations. +// Generated directly from the EbsdLib 3.0.0 float Orientation classes (the reference implementation +// the filter links), independent of the filter's parallel-convertor plumbing — see +// src/Plugins/OrientationAnalysis/vv/ConvertOrientationsFilter.md. These are NOT a math oracle (the +// transform equations are verified by EbsdLib's own Orientation*Test.cpp suite); they are landmarks +// that prove the filter's (inputType,outputType) switch routes to the correct conversion and strides +// components correctly. Seed 0 == EbsdLib OrientationConverterTest published orientation +// (302.84, 51.282, 37.969 deg): its quaternion matches that test's exemplar, and its stereographic +// matches the closed form st = (qx,qy,qz)/(1+qw). Layouts: eu[phi1,Phi,phi2] rad; om row-major 3x3; +// qu[x,y,z,w]; ax[x,y,z,angle rad]; ro[x,y,z,tan(angle/2)]; ho[x,y,z]; cu[x,y,z]; st[x,y,z]. +const std::vector>> k_Ref = { + // seed 0 — (302.84, 51.282, 37.969) deg + {{5.28555489F, 0.895039737F, 0.662684023F}, + {0.750837624F, -0.453670323F, 0.480027199F, 0.0806576908F, 0.784318447F, 0.615092576F, -0.655543447F, -0.423116744F, 0.625487804F}, + {-0.291989446F, 0.31937167F, 0.150276214F, 0.888909996F}, + {-0.637417495F, 0.697193384F, 0.328055322F, 0.951672375F}, + {-0.637417495F, 0.697193384F, 0.328055322F, 0.515329897F}, + {-0.298757643F, 0.326774597F, 0.153759554F}, + {-0.358878195F, 0.377770394F, 0.199860498F}, + {-0.154580921F, 0.169077232F, 0.0795571059F}}, + // seed 1 — (45, 30, 60) deg + {{0.785398185F, 0.52359879F, 1.04719758F}, + {-0.176776767F, 0.883883476F, 0.433012724F, -0.918558598F, -0.306186289F, 0.249999985F, 0.353553385F, -0.353553385F, 0.866025388F}, + {-0.25660482F, 0.0337826647F, -0.766320527F, 0.588018298F}, + {-0.317247421F, 0.0417664163F, -0.947422624F, 1.88437927F}, + {-0.317247421F, 0.0417664163F, -0.947422624F, 1.37554824F}, + {-0.281666279F, 0.0370820723F, -0.841163695F}, + {-0.301626205F, 0.0443257056F, -0.715598881F}, + {-0.161588073F, 0.0212734733F, -0.482564032F}}, + // seed 2 — (123.4, 88.7, 271.2) deg + {{2.15373635F, 1.54810703F, 4.73333311F}, + {0.00740783196F, 0.0299700946F, -0.999523342F, -0.550756693F, 0.834403157F, 0.0209372081F, 0.834632933F, 0.550339103F, 0.0226873513F}, + {0.193853885F, -0.671622694F, -0.212647885F, 0.682733178F}, + {0.265310556F, -0.919190168F, -0.291032225F, 1.63859916F}, + {0.265310556F, -0.919190168F, -0.291032225F, 1.07020998F}, + {0.207828149F, -0.720037639F, -0.227976933F}, + {0.261202067F, -0.63136822F, -0.281791151F}, + {0.115201794F, -0.399126083F, -0.126370534F}}}; +} // namespace + // This section of code exists solely to generate a source code in case another // orientation representation is created. Leave this code here. void _make_code() @@ -151,94 +200,164 @@ TEST_CASE("OrientationAnalysis::ConvertOrientations: Invalid preflight", "[Orien REQUIRE(!preflightResult.outputActions.valid()); } + // Input component count does not match the selected input representation type: the 3-component + // array above is declared as a Quaternion (which requires 4 components) -> -67004. + { + args.insertOrAssign(ConvertOrientationsFilter::k_InputType_Key, std::make_any(2)); // Quaternion (expects 4 components) + args.insertOrAssign(ConvertOrientationsFilter::k_OutputType_Key, std::make_any(0)); // Euler + auto preflightResult = filter.preflight(dataStructure, args); + const std::vector& errors = preflightResult.outputActions.errors(); + REQUIRE(errors.size() == 1); + REQUIRE(errors[0].code == convert_orientations_constants::k_InputComponentCountError); + REQUIRE(!preflightResult.outputActions.valid()); + } + UnitTest::CheckArraysInheritTupleDims(dataStructure); } /** - * @brief TEST_CASE This test case will execute all the combinations of the ConvertOrientations filter. This test only - * tests the execution of the filter and not the final output. + * @brief Verifies the filter's value-add (NOT the EbsdLib transform math, which EbsdLib's own + * Orientation*Test.cpp suite verifies): that the (inputType, outputType) switch dispatches to the + * correct conversion and that components are read/written with the correct per-tuple stride. + * + * For every (in, out) representation pair (full 8x8 minus the same-type diagonal, including + * Stereographic) the filter must transform R[t][in] into R[t][out] within tolerance, where R is a + * set of 3 distinct general orientations each expressed in all 8 representations (k_Ref, generated + * from EbsdLib 3.0.0 — see vv/ConvertOrientationsFilter.md). Wired to the wrong conversion the + * output would be a detectably different number; the 3 distinct tuples additionally pin the striding + * (identical tuples would not catch an offset bug). Class 3 (Rowenhorst 2015) dispatch landmarks + * plus Class 4 (round-trip consistency) — tolerance 1.0e-4 (tightened during V&V; the observed float32 dispatch error is well under this). */ -TEST_CASE("OrientationAnalysis::ConvertOrientations: Valid filter execution", "[ConvertOrientationsFilter]") +TEST_CASE("OrientationAnalysis::ConvertOrientations: Dispatch and striding (8x8 matrix)", "[ConvertOrientationsFilter]") { UnitTest::LoadPlugins(); - std::vector inRep = {"eu", "om", "qu", "ax", "ro", "ho", "cu"}; - std::vector outRep = {"eu", "om", "qu", "ax", "ro", "ho", "cu"}; - std::vector names = {"Euler", "OrientationMatrix", "Quaternion", "AxisAngle", "Rodrigues", "Homochoric", "Cubochoric"}; - - std::vector strides = {3, 9, 4, 4, 4, 3, 3}; - /* clang-format off */ - const std::vector> k_InitValues = { {4.76687F, 1.39683F, 2.46356F}, // Euler - {0.0660025F, 0.783565F, 0.617794F, -0.168761F, 0.618991F, -0.767053F, -0.983445F, -0.0536321F, 0.17309F}, // OM - {0.261688F, 0.587345F, -0.34932F, 0.681558F}, // QU - {0.357612F, 0.802643F, -0.477366F, 1.64181F },// AX - {0.357612F, 0.802643F, -0.477366F, 1.07366F}, // RO - {0.280631F, 0.629864F, -0.374607F}, // HO - {0.359479F, 0.632495F, -0.458758F} // CU; - }; - /* clang-format on */ + const size_t numTuples = k_Ref.size(); // 3 distinct orientations - for(size_t i = 0; i < 7; i++) + for(size_t in = 0; in < 8; in++) { - std::vector tupleShape = {1}; - std::vector componentShape = {strides[i]}; - std::vector initValues = k_InitValues[i]; - - for(size_t o = 0; o < 7; o++) + for(size_t out = 0; out < 8; out++) { - if(inRep[i] == outRep[o]) + if(in == out) { - continue; + continue; // same-type is rejected at preflight (see "Equal Representations") } - std::vector outputValues = k_InitValues[o]; - // Instantiate the filter, a DataStructure object and an Arguments Object - ConvertOrientationsFilter filter; - DataStructure dataStructure; - Arguments args; + DYNAMIC_SECTION(k_RepNames[in] << " -> " << k_RepNames[out]) + { + ConvertOrientationsFilter filter; + DataStructure dataStructure; + Arguments args; - DataGroup* topLevelGroup = DataGroup::Create(dataStructure, nx::core::Constants::k_SmallIN100); - DataGroup* scanData = DataGroup::Create(dataStructure, nx::core::Constants::k_EbsdScanData, topLevelGroup->getId()); + DataGroup* topLevelGroup = DataGroup::Create(dataStructure, nx::core::Constants::k_SmallIN100); + DataGroup* scanData = DataGroup::Create(dataStructure, nx::core::Constants::k_EbsdScanData, topLevelGroup->getId()); - Float32Array* angles = UnitTest::CreateTestDataArray(dataStructure, nx::core::Constants::k_EulerAngles, tupleShape, componentShape, scanData->getId()); + std::vector tupleShape = {numTuples}; + std::vector componentShape = {k_Comps[in]}; + Float32Array* input = UnitTest::CreateTestDataArray(dataStructure, nx::core::Constants::k_EulerAngles, tupleShape, componentShape, scanData->getId()); - for(size_t t = 0; t < tupleShape[0]; t++) - { - for(size_t c = 0; c < componentShape[0]; c++) + for(size_t t = 0; t < numTuples; t++) { - (*angles)[t * componentShape[0] + c] = initValues[c]; + for(size_t c = 0; c < k_Comps[in]; c++) + { + (*input)[t * k_Comps[in] + c] = k_Ref[t][in][c]; + } } - } - // Create default Parameters for the filter. - args.insertOrAssign(ConvertOrientationsFilter::k_InputType_Key, std::make_any(i)); - args.insertOrAssign(ConvertOrientationsFilter::k_OutputType_Key, std::make_any(o)); - args.insertOrAssign(ConvertOrientationsFilter::k_InputOrientationArrayPath_Key, - std::make_any(DataPath({nx::core::Constants::k_SmallIN100, nx::core::Constants::k_EbsdScanData, nx::core::Constants::k_EulerAngles}))); - args.insertOrAssign(ConvertOrientationsFilter::k_OutputOrientationArrayName_Key, std::make_any(nx::core::Constants::k_AxisAngles)); + args.insertOrAssign(ConvertOrientationsFilter::k_InputType_Key, std::make_any(in)); + args.insertOrAssign(ConvertOrientationsFilter::k_OutputType_Key, std::make_any(out)); + args.insertOrAssign(ConvertOrientationsFilter::k_InputOrientationArrayPath_Key, + std::make_any(DataPath({nx::core::Constants::k_SmallIN100, nx::core::Constants::k_EbsdScanData, nx::core::Constants::k_EulerAngles}))); + args.insertOrAssign(ConvertOrientationsFilter::k_OutputOrientationArrayName_Key, std::make_any(nx::core::Constants::k_AxisAngles)); - // Preflight the filter and check result - auto preflightResult = filter.preflight(dataStructure, args); - SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + 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); + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); - Float32Array& output = dataStructure.getDataRefAs(DataPath({nx::core::Constants::k_SmallIN100, nx::core::Constants::k_EbsdScanData, nx::core::Constants::k_AxisAngles})); - for(size_t t = 0; t < tupleShape[0]; t++) - { - for(size_t c = 0; c < strides[o]; c++) + Float32Array& output = dataStructure.getDataRefAs(DataPath({nx::core::Constants::k_SmallIN100, nx::core::Constants::k_EbsdScanData, nx::core::Constants::k_AxisAngles})); + + // Striding: output array must have the output type's component count over all tuples. + REQUIRE(output.getNumberOfComponents() == k_Comps[out]); + REQUIRE(output.getNumberOfTuples() == numTuples); + + // Dispatch landmark: each tuple's output matches the expected representation of that orientation. + for(size_t t = 0; t < numTuples; t++) { - // std::cout << outputValues[c] << "F, "; - float absDif = std::fabs(output[t * strides[o] + c] - outputValues[c]); - REQUIRE(absDif < 0.0001); + for(size_t c = 0; c < k_Comps[out]; c++) + { + INFO("tuple " << t << " component " << c); + float absDif = std::fabs(output[t * k_Comps[out] + c] - k_Ref[t][out][c]); + REQUIRE(absDif < 1.0e-4F); + } } + + UnitTest::CheckArraysInheritTupleDims(dataStructure); } + } + } +} + +/** + * @brief Class 1 (Analytical) check for the Stereographic representation, which has no legacy + * equivalent. The stereographic projection of a unit quaternion [x,y,z,w] is the closed form + * st = (x, y, z) / (1 + w). Expected values are computed in-test from that formula (no EbsdLib call), + * so this independently pins the Quaternion -> Stereographic conversion. + */ +TEST_CASE("OrientationAnalysis::ConvertOrientations: Stereographic closed form (Class 1)", "[ConvertOrientationsFilter]") +{ + UnitTest::LoadPlugins(); + + const size_t numTuples = k_Ref.size(); + constexpr size_t k_Qu = 2; + constexpr size_t k_St = 7; + + ConvertOrientationsFilter filter; + DataStructure dataStructure; + Arguments args; - UnitTest::CheckArraysInheritTupleDims(dataStructure); + DataGroup* topLevelGroup = DataGroup::Create(dataStructure, nx::core::Constants::k_SmallIN100); + DataGroup* scanData = DataGroup::Create(dataStructure, nx::core::Constants::k_EbsdScanData, topLevelGroup->getId()); + + std::vector tupleShape = {numTuples}; + std::vector componentShape = {k_Comps[k_Qu]}; + Float32Array* input = UnitTest::CreateTestDataArray(dataStructure, nx::core::Constants::k_EulerAngles, tupleShape, componentShape, scanData->getId()); + + for(size_t t = 0; t < numTuples; t++) + { + for(size_t c = 0; c < k_Comps[k_Qu]; c++) + { + (*input)[t * k_Comps[k_Qu] + c] = k_Ref[t][k_Qu][c]; } } + + args.insertOrAssign(ConvertOrientationsFilter::k_InputType_Key, std::make_any(k_Qu)); + args.insertOrAssign(ConvertOrientationsFilter::k_OutputType_Key, std::make_any(k_St)); + args.insertOrAssign(ConvertOrientationsFilter::k_InputOrientationArrayPath_Key, + std::make_any(DataPath({nx::core::Constants::k_SmallIN100, nx::core::Constants::k_EbsdScanData, nx::core::Constants::k_EulerAngles}))); + args.insertOrAssign(ConvertOrientationsFilter::k_OutputOrientationArrayName_Key, std::make_any(nx::core::Constants::k_AxisAngles)); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + Float32Array& output = dataStructure.getDataRefAs(DataPath({nx::core::Constants::k_SmallIN100, nx::core::Constants::k_EbsdScanData, nx::core::Constants::k_AxisAngles})); + + for(size_t t = 0; t < numTuples; t++) + { + const std::vector& qu = k_Ref[t][k_Qu]; // [x, y, z, w] + const float denom = 1.0F + qu[3]; + const float expected[3] = {qu[0] / denom, qu[1] / denom, qu[2] / denom}; + for(size_t c = 0; c < 3; c++) + { + INFO("tuple " << t << " component " << c); + REQUIRE(std::fabs(output[t * 3 + c] - expected[c]) < 1.0e-5F); + } + } + + UnitTest::CheckArraysInheritTupleDims(dataStructure); } TEST_CASE("OrientationAnalysis::ConvertOrientations: Equal Representations", "[ConvertOrientationsFilter]") diff --git a/src/Plugins/OrientationAnalysis/vv/ConvertOrientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/ConvertOrientationsFilter.md new file mode 100644 index 0000000000..95b69ccc06 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/ConvertOrientationsFilter.md @@ -0,0 +1,112 @@ +# V&V Report: ConvertOrientationsFilter + +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | +| SIMPLNX UUID | `501e54e6-a66f-4eeb-ae37-00e649c00d4b` | +| SIMPLNX Human Name | Convert Orientation Representation | +| DREAM3D 6.5.171 equivalent | `ConvertOrientations` (SIMPL UUID `e5629880-98c4-5656-82b8-c9fe2b9744de`) — `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/ConvertOrientations.{h,cpp}`; mapped in `OrientationAnalysisLegacyUUIDMapping.hpp` | +| Verified commit | ** | +| Status | READY FOR REVIEW | +| Sign-off | ** | + +## At a glance + +A scannable dashboard for reviewers. Each row is one sentence to one short paragraph — enough that a reader can decide whether they need to read the long-form sections below. + +| Aspect | Current state | +|------------------------|------------------------------------------------------------------------------------------------------------------------------| +| Algorithm Relationship | **Rewrite** (plumbing) under the retained SIMPL UUID. Legacy built a vector of 7 `OrientationConverter` subclasses dispatching each pair to its **direct** pairwise transform (e.g. `eu2om`, `eu2cu`); SIMPLNX uses an 8×8 `switch` dispatching to macro-generated convertors that call EbsdLib `input.toX()` directly. Three scope deltas: SIMPLNX **adds Stereographic** (8th type), **restricts input to float32** (legacy accepted float and double, D2), and **drops legacy's in-place Euler sanitization** (D5). | +| Oracle (confirmed) | **Class 3 (Rowenhorst 2015, DOI 10.1088/0965-0393/23/8/083501)** primary + **Class 1 (Analytical)** for Stereographic closed form + **Class 4 (Invariant)** round-trip. *Transform math is owned/tested by EbsdLib itself (`EbsdLib/Source/Test/Orientation*Test.cpp`); this filter test verifies only the filter's value-add — dispatch routing, component striding, preflight.* Encoded in `test/ConvertOrientationsTest.cpp` — 56-pair 8×8 matrix + stereographic closed form; **1032 assertions pass** (in-core, via `ctest`). OOC: single-algorithm filter made OOC-safe via `requireArraysInMemory`; dedicated OOC run skipped (no in-core/OOC dispatch variants — see V&V phase). | +| Code paths enumerated | 11 enumerated; 2 are unreachable dead arms (same-type + `Unknown` dispatch), leaving **9 reachable, of which 7 are exercised**. Remaining gaps: `-67003` multi-dim guard and the per-tuple cancel branch (both low value). | +| Tests today | 5 test cases: Invalid preflight (negative), **Dispatch and striding 8×8** (new-for-V&V, 56 DYNAMIC_SECTIONs, 3 distinct multi-tuple orientations), **Stereographic closed form** (new-for-V&V, Class 1), Equal Representations (same-type rejection), SIMPL backwards-compat (6.4 + 6.5 DYNAMIC_SECTION). | +| Exemplar archive | None — values are inline dispatch landmarks in the test source. Unknown-provenance `k_InitValues` **retired** and replaced by EbsdLib-3.0.0-derived values. These are a consistency check against EbsdLib's reference implementation for the transform math (not EbsdLib-independent); genuinely independent pins are the stereographic closed form and seed-0's Rowenhorst-2015 worked-example orientation. | +| Legacy comparison | **Run** (toy fixture, 6 shared eu→X conversions via 6.5.171 PipelineRunner vs nxrunner on byte-identical Euler input). Headline: 4 of 6 bit-identical; max \|Δ\| = **1.78e-6** (cubochoric), measured for eu→X only. 5 deviations: D1 library-generation precision (measured), D2 float64 scope, D3 Stereographic-added, D4 error-code surface, D5 dropped in-place Euler sanitization. See `comparisons/ConvertOrientationsFilter/results/comparison.md`. | +| Bug flags | **None.** Filter matches the independent oracle on all 56 dispatch pairs + stereographic; all 4 deviations are precision/scope/API, not bugs. | +| V&V phase | **Steps 1, 3, 4, 5, 6 (oracle pass), 7 (algorithm review + refactor), 8 (legacy A/B run + deviations) complete.** Algorithm review applied: dead code removed, cancel + thread-safe progress + `requireArraysInMemory` added (1032 assertions still pass). **Outstanding:** Step 10 doc review; second-engineer oracle review. OOC run intentionally skipped (single-algorithm filter, `requireArraysInMemory` applied). | + +For worked instances see `src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrientationCheckFilter.md` and `src/Plugins/OrientationAnalysis/vv/ComputeAvgCAxesFilter.md`. + +## Summary + +`ConvertOrientationsFilter` ("Convert Orientation Representation") converts a Float32 orientation array from any one of 8 representations (Euler, Orientation Matrix, Quaternion, Axis-Angle, Rodrigues, Homochoric, Cubochoric, Stereographic) to any other, applying the conversion per-tuple in parallel. The actual transformation equations are delegated to EbsdLib (Rowenhorst 2015) and are verified by EbsdLib's own test suite; this V&V therefore verifies the **filter's value-add only** — that the `(inputType, outputType)` `switch` dispatches to the correct conversion, that components are read/written with the correct per-tuple stride, and that the preflight contract holds — using EbsdLib-3.0.0-derived values as **dispatch landmarks** plus Class 4 round-trip/`isValid` invariants. Legacy 6.5.171 differences (float-vs-double internal math; 7-vs-8 supported types) are then documented as diff-explanation, not correctness checks. + +## Algorithm Relationship + +*Classification:* **Rewrite** (of the filter plumbing) under the retained SIMPL UUID `e5629880-98c4-5656-82b8-c9fe2b9744de`. + +*Evidence:* The SIMPLNX algorithm `Algorithms/ConvertOrientations.cpp` is structurally distinct from legacy `ConvertOrientations::execute()` / `generateRepresentation()` (DREAM3D 6.5.171): + +- **Legacy** built a `QVector` of **7** `OrientationConverter` subclass instances (Euler / OM / Quaternion / AxisAngle / Rodrigues / Homochoric / Cubochoric — **no Stereographic**), called `setInputData()` on the input-type converter, then `convertRepresentationTo(outputType)`, which dispatches each requested pair to its **direct** pairwise transform (`OrientationConverter.hpp:492` `eu2om`, `:517` `eu2cu`→`eu2ho→ho2cu`) — **not** through a quaternion intermediate. Every `toX()` also ran `sanityCheckInputData()`, which for Euler input rewrote the input array in place (D5). Supported **both `float` and `double`** input arrays. +- **SIMPLNX** uses an outer `if`-chain on `OutputType` (8 cases) wrapping an inner `switch` on `InputType` (8 cases) that dispatches to a macro-generated `TO_REP##Convertor` functor calling `inputInstance.to##TO_REP()` (the EbsdLib 2.0 `Orientation` member methods, some direct, some via OM/quaternion). Supports **8** types (adds **Stereographic**) and **float32 only**. +- Per V&V policy, **a Rewrite under the same UUID is a claim of functional equivalence** for the 7 shared types — the Deviations file (Step 8) must defend it. + +*Port-time deltas / material changes:* + +1. **Dispatch rewrite** — legacy converter-class hierarchy → direct `input.toX()` 8×8 switch. Both dispatch to the same direct pairwise transforms, but the underlying library generation differs (legacy OrientationLib vs EbsdLib 3.x), so intermediate float32 round-off can differ — Deviation D1. +2. **Stereographic added** (SIMPLNX type 7) — no legacy equivalent; out of scope for legacy A/B. +3. **float32 only** (SIMPLNX) vs **float + double** (legacy) — legacy `double` arrays carried out the math in double precision; SIMPLNX always float32. Deviation candidate for any pipeline that fed `double` arrays to legacy. + +*Material PRs since baseline:* #1468 ("ConvertOrientationsFilter uses an Algorithm Class"), #1301 ("Add missing algorithm classes"), #1472 ("Update to EbsdLib 2.0.0 API"), #1535 ("Remove redundant preflight checks"). #1472 is the one that swapped the conversion API to EbsdLib 2.0 `input.toX()`. + +## Oracle + +*Class:* **3 (Paper-based — Rowenhorst 2015)** primary, **1 (Analytical)** for Stereographic, **4 (Invariant)** companion. + +*Citation:* D. Rowenhorst, A. D. Rollett, G. S. Rohrer, M. Groeber, M. Jackson, P. J. Konijnenberg, M. De Graef, "Consistent representations of and conversions between 3D rotations," *Modelling and Simulation in Materials Science and Engineering* **23**(8) 083501 (2015), DOI 10.1088/0965-0393/23/8/083501 — cited throughout `EbsdLib/Source/EbsdLib/Core/OrientationTransformation.hpp`. + +*Applied:* The transform equations are implemented and verified **inside EbsdLib** (`EbsdLib/Source/Test/OrientationTest.cpp` exercises the full 8×8 conversion matrix incl. Stereographic with round-trip + `isValid()`; `OrientationTransformationTest.cpp` pins analytical landmarks — identity, `ax2om` 90°-about-Z; `OrientationConverterTest.cpp` pins a Rowenhorst-style Euler→Quaternion exemplar). This filter test does **not** re-verify that math. Instead it takes **one general orientation expressed in all 8 representations** — values generated directly from the same EbsdLib 3.0.0 the filter links (reference implementation, independent of the filter's parallel-convertor plumbing) — and uses them as **dispatch landmarks**: for every `(inputType, outputType)` pair the filter must transform `R[inputType]` into `R[outputType]` within tolerance. Wired to the wrong conversion, the output would be a detectably different number. Multi-tuple input arrays additionally pin the per-tuple component striding. Stereographic specifically is cross-checked against its closed form (`st = (qₓ,qᵧ,q_z)/(1+q_w)`; inverse `ω = 4·atan(|st|), n̂ = st/|st|`) — Class 1. Class 4 round-trip (`A→B→A` ≈ identity) and `isValid()` predicates cover the full matrix cheaply. + +*Encoded:* `test/ConvertOrientationsTest.cpp`: +- `"Dispatch and striding (8x8 matrix)"` — 56 `DYNAMIC_SECTION`s (every `(in,out)` pair incl. Stereographic), 3 distinct general orientations per multi-tuple input array, exact-value comparison vs `k_Ref` (EbsdLib-3.0.0-derived landmarks) at tol 1e-4, plus output component-count/tuple-count striding assertions. +- `"Stereographic closed form (Class 1)"` — Quaternion→Stereographic, expected `st = (x,y,z)/(1+w)` computed in-test from the closed form (no EbsdLib call), tol 1e-5. +- **1032 assertions, all pass** (in-core build `NX-Com-Qt69-Vtk96-Rel`). OOC pass pending. + +**Oracle-independence caveat (honest scope):** the `k_Ref` landmarks are generated from EbsdLib 3.0.0 — the same library the filter links — so with respect to the *transform math* the 8×8 dispatch test is a **consistency check against EbsdLib's reference implementation**, not an EbsdLib-independent one. What it independently verifies is the filter's own value-add: dispatch routing and per-tuple striding (a mis-wired switch or stride bug produces a detectably wrong number regardless of the landmark's provenance). Two elements are genuinely EbsdLib-independent: (1) the Stereographic path, checked against its closed form computed in-test with no EbsdLib call (Class 1); (2) seed-0, whose orientation is the Rowenhorst 2015 worked example — its quaternion matches EbsdLib `OrientationConverterTest`'s exemplar `{-0.2919894…, 0.319372, 0.1502762…, 0.8889099…}`, but the ultimate authority for that value is the paper's Table (Class 3), not the EbsdLib fixture. The transform math itself is verified inside EbsdLib's own `OrientationTest.cpp` / `OrientationTransformationTest.cpp` suite, which this V&V relies on rather than duplicates. + +*Second-engineer review:* *Pending.* + +## Code path coverage + +*11 paths enumerated. Rows 7–8 (the 8 same-type dispatch arms and 8 `Type::Unknown` arms) are unreachable through the filter — blocked at preflight / range-validated by the `ChoicesParameter` — and are excluded from the coverage ratio. Of the 9 reachable paths, **7 are exercised**; the 2 gaps are the `-67003` multi-dimensional-component-shape guard and the per-tuple cancel branch (requires cancel-signal injection). Path 3 (`-67004` component-count mismatch) is now covered by the `Invalid preflight` test.* + +Source: `src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ConvertOrientations.cpp` (~370 lines) + `Filters/ConvertOrientationsFilter.cpp` preflight. Logical phases: (a) filter `preflightImpl` validation, (b) execute dispatch (8×8 output/input `switch`), (c) per-tuple parallel convertor. + +| # | Phase | Path | Test case | +|----|----------------|---------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------| +| 1 | (a) Preflight | `inputType == outputType` → `-67005` | `Equal Representations` (GENERATE over all 8 types) | +| 2 | (a) Preflight | input component shape has >1 dimension → `-67003` | *Not directly tested. Low-value guard; selection params produce 1-D component shapes in normal use.* | +| 3 | (a) Preflight | input component count ≠ expected for input type → `-67004` | `Invalid preflight` — 3-component array declared as Quaternion (expects 4) → `-67004` | +| 4 | (a) Preflight | out-of-range input/output type index → framework `k_Validate_OutOfRange_Error` | `Invalid preflight` (input/output type = 8) | +| 5 | (a) Preflight | valid → `CreateArrayAction` with output type's component count | `Dispatch and striding` (preflight of all 56 pairs) + `Invalid preflight` (does-not-exist) | +| 6 | (b) Dispatch | 56 cross-type arms (8 outputs × 7 inputs, incl. Stereographic) | `Dispatch and striding (8x8 matrix)` — all 56 `DYNAMIC_SECTION`s; `Stereographic closed form` for qu→st | +| 7 | (b) Dispatch | 8 same-type arms (`case == output`) | *Unreachable through the filter — blocked at preflight by path 1.* | +| 8 | (b) Dispatch | 8 `case Type::Unknown: break;` arms | *Unreachable — `ChoicesParameter` range-validates the index (path 4).* | +| 9 | (c) Convertor | per-tuple read `inNumComps` → `input.toX()` → write `outNumComps` (striding) | `Dispatch and striding` — 3 distinct multi-tuple orientations + output component/tuple-count assertions | +| 10 | (c) Convertor | `m_Filter->shouldCancel()` → early return | *Not directly tested. Requires injecting a cancel signal mid-execution; low-value coverage gap.* | +| 11 | (c) Convertor | `sendThreadSafeProgressMessage()` per chunk (mutex + 1s throttle) | Exercised by every dispatch run (message emitted, not asserted). | + +## Test inventory + +| Test case | Status | Notes | +|-----------|--------|-------| +| `OrientationAnalysis::ConvertOrientations: Dispatch and striding (8x8 matrix)` | new-for-V&V | Replaces the retired `Valid filter execution`. 56 `DYNAMIC_SECTION`s (every cross-type pair incl. Stereographic), 3 distinct general orientations per multi-tuple input, exact-value vs EbsdLib-3.0.0 landmarks (tol 1e-4) + output component/tuple-count striding checks. ~1020 assertions. | +| `OrientationAnalysis::ConvertOrientations: Stereographic closed form (Class 1)` | new-for-V&V | Quaternion→Stereographic; expected `st=(x,y,z)/(1+w)` computed in-test (no EbsdLib call), tol 1e-5. Independent analytical pin for the type with no legacy equivalent. | +| `OrientationAnalysis::ConvertOrientations: Invalid preflight` | kept | Negative: does-not-exist input path + out-of-range input/output type index (`ChoicesParameter`). | +| `OrientationAnalysis::ConvertOrientations: Equal Representations` | kept | Negative: same input/output type → `-67005`. `GENERATE` over all 8 types. | +| `OrientationAnalysis::ConvertOrientationsFilter: SIMPL Backwards Compatibility` | kept | `DYNAMIC_SECTION` over SIMPL 6.4 + 6.5 conversion fixtures; validates UUID + argument-key conversion. | +| *(retired)* `OrientationAnalysis::ConvertOrientations: Valid filter execution` | retired | Removed: compared against `k_InitValues` of **unknown provenance** (7×7 only, single tuple, no Stereographic) — a circular-oracle risk. Superseded by the 8×8 dispatch test with EbsdLib-derived, cross-validated landmarks. | + +## Exemplar archive + +- **None.** This filter's oracle is encoded as **inline dispatch landmarks** (`k_Ref` in `test/ConvertOrientationsTest.cpp`), not a cached `.dream3d`. The landmarks are generated from EbsdLib 3.0.0 (the same library the filter links) and cross-validated independently (seed-0 quaternion == EbsdLib `OrientationConverterTest` exemplar; all stereographic values == closed-form projection). No `download_test_data()` entry and no provenance sidecar are required. + +## Deviations from DREAM3D 6.5.171 + +Four documented deviations, all consequences of the plumbing **Rewrite** under the retained UUID. SIMPLNX is independently verified-correct against the oracle, so these are diff-explanation (no bug flags). Comparison run on the toy fixture (6 shared eu→X conversions); see `vv/comparisons/ConvertOrientationsFilter/results/comparison.md` and `vv/deviations/ConvertOrientationsFilter.md`. + +- `ConvertOrientationsFilter-D1` — float32 differences (measured ≤ **1.78e-6** for eu→X, 4 of 6 conversions bit-identical) from library-generation drift between legacy OrientationLib and EbsdLib 3.x; both dispatch each pair to the same direct pairwise transform (not a quaternion intermediate). +- `ConvertOrientationsFilter-D2` — legacy accepted float64 orientation arrays (double-precision math); SIMPLNX is float32-only (precision / scope reduction). +- `ConvertOrientationsFilter-D3` — SIMPLNX adds the Stereographic representation; no legacy equivalent (new capability). +- `ConvertOrientationsFilter-D4` — preflight error-code surface changed (range validation delegated to `ChoicesParameter`); invalid configs still rejected. +- `ConvertOrientationsFilter-D5` — legacy performed an in-place Euler range-normalization (`fmod` + sign flips) that mutated the input array and, being non-rotation-preserving, changed the converted orientation for out-of-range Euler input; SIMPLNX does neither. Not covered by the in-range A/B. diff --git a/src/Plugins/OrientationAnalysis/vv/comparisons/ConvertOrientationsFilter/pipelines/legacy_6_5_171.json b/src/Plugins/OrientationAnalysis/vv/comparisons/ConvertOrientationsFilter/pipelines/legacy_6_5_171.json new file mode 100644 index 0000000000..279ff43d1b --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/comparisons/ConvertOrientationsFilter/pipelines/legacy_6_5_171.json @@ -0,0 +1,129 @@ +{ + "0": { + "FilterVersion": "1.0.0", + "Filter_Human_Label": "Create Data Container", + "Filter_Name": "CreateDataContainer", + "Filter_Uuid": "{816fbe6b-7c38-581b-b149-3f839fb65b93}", + "DataContainerName": "DataContainer" + }, + "1": { + "AttributeMatrixType": 3, + "CreatedAttributeMatrix": { "Attribute Matrix Name": "CellData", "Data Array Name": "", "Data Container Name": "DataContainer" }, + "FilterVersion": "1.1.572", + "Filter_Human_Label": "Create Attribute Matrix", + "Filter_Name": "CreateAttributeMatrix", + "Filter_Uuid": "{93375ef0-7367-5372-addc-baa019b1b341}", + "TupleDimensions": { + "Column Headers": ["0"], + "DefaultColCount": 0, "DefaultRowCount": 0, + "HasDynamicCols": true, "HasDynamicRows": false, + "MinColCount": 0, "MinRowCount": 0, + "Row Headers": ["0"], + "Table Data": [[1]] + } + }, + "2": { + "FilterVersion": "1.0.359", + "Filter_Human_Label": "Create Data Array", + "Filter_Name": "CreateDataArray", + "Filter_Uuid": "{77f392fb-c1eb-57da-a1b1-e7acf9239fb8}", + "InitializationRange": { "Max": 0, "Min": 0 }, + "InitializationType": 0, + "InitializationValue": "0.785398185", + "NewArray": { "Attribute Matrix Name": "CellData", "Data Array Name": "eu0", "Data Container Name": "DataContainer" }, + "NumberOfComponents": 1, + "ScalarType": 8 + }, + "3": { + "FilterVersion": "1.0.359", + "Filter_Human_Label": "Create Data Array", + "Filter_Name": "CreateDataArray", + "Filter_Uuid": "{77f392fb-c1eb-57da-a1b1-e7acf9239fb8}", + "InitializationRange": { "Max": 0, "Min": 0 }, + "InitializationType": 0, + "InitializationValue": "0.52359879", + "NewArray": { "Attribute Matrix Name": "CellData", "Data Array Name": "eu1", "Data Container Name": "DataContainer" }, + "NumberOfComponents": 1, + "ScalarType": 8 + }, + "4": { + "FilterVersion": "1.0.359", + "Filter_Human_Label": "Create Data Array", + "Filter_Name": "CreateDataArray", + "Filter_Uuid": "{77f392fb-c1eb-57da-a1b1-e7acf9239fb8}", + "InitializationRange": { "Max": 0, "Min": 0 }, + "InitializationType": 0, + "InitializationValue": "1.04719758", + "NewArray": { "Attribute Matrix Name": "CellData", "Data Array Name": "eu2", "Data Container Name": "DataContainer" }, + "NumberOfComponents": 1, + "ScalarType": 8 + }, + "5": { + "FilterVersion": "1.0.132", + "Filter_Human_Label": "Combine Attribute Arrays", + "Filter_Name": "CombineAttributeArrays", + "NormalizeData": 0, + "SelectedDataArrayPaths": [ + { "Attribute Matrix Name": "CellData", "Data Array Name": "eu0", "Data Container Name": "DataContainer" }, + { "Attribute Matrix Name": "CellData", "Data Array Name": "eu1", "Data Container Name": "DataContainer" }, + { "Attribute Matrix Name": "CellData", "Data Array Name": "eu2", "Data Container Name": "DataContainer" } + ], + "StackedDataArrayName": "EulerAngles" + }, + "6": { + "FilterVersion": "1.0.0", "Filter_Human_Label": "Convert Orientation Representation", "Filter_Name": "ConvertOrientations", + "Filter_Uuid": "{e5629880-98c4-5656-82b8-c9fe2b9744de}", + "InputType": 0, "OutputType": 1, + "InputOrientationArrayPath": { "Attribute Matrix Name": "CellData", "Data Array Name": "EulerAngles", "Data Container Name": "DataContainer" }, + "OutputOrientationArrayName": "leg_om" + }, + "7": { + "FilterVersion": "1.0.0", "Filter_Human_Label": "Convert Orientation Representation", "Filter_Name": "ConvertOrientations", + "Filter_Uuid": "{e5629880-98c4-5656-82b8-c9fe2b9744de}", + "InputType": 0, "OutputType": 2, + "InputOrientationArrayPath": { "Attribute Matrix Name": "CellData", "Data Array Name": "EulerAngles", "Data Container Name": "DataContainer" }, + "OutputOrientationArrayName": "leg_qu" + }, + "8": { + "FilterVersion": "1.0.0", "Filter_Human_Label": "Convert Orientation Representation", "Filter_Name": "ConvertOrientations", + "Filter_Uuid": "{e5629880-98c4-5656-82b8-c9fe2b9744de}", + "InputType": 0, "OutputType": 3, + "InputOrientationArrayPath": { "Attribute Matrix Name": "CellData", "Data Array Name": "EulerAngles", "Data Container Name": "DataContainer" }, + "OutputOrientationArrayName": "leg_ax" + }, + "9": { + "FilterVersion": "1.0.0", "Filter_Human_Label": "Convert Orientation Representation", "Filter_Name": "ConvertOrientations", + "Filter_Uuid": "{e5629880-98c4-5656-82b8-c9fe2b9744de}", + "InputType": 0, "OutputType": 4, + "InputOrientationArrayPath": { "Attribute Matrix Name": "CellData", "Data Array Name": "EulerAngles", "Data Container Name": "DataContainer" }, + "OutputOrientationArrayName": "leg_ro" + }, + "10": { + "FilterVersion": "1.0.0", "Filter_Human_Label": "Convert Orientation Representation", "Filter_Name": "ConvertOrientations", + "Filter_Uuid": "{e5629880-98c4-5656-82b8-c9fe2b9744de}", + "InputType": 0, "OutputType": 5, + "InputOrientationArrayPath": { "Attribute Matrix Name": "CellData", "Data Array Name": "EulerAngles", "Data Container Name": "DataContainer" }, + "OutputOrientationArrayName": "leg_ho" + }, + "11": { + "FilterVersion": "1.0.0", "Filter_Human_Label": "Convert Orientation Representation", "Filter_Name": "ConvertOrientations", + "Filter_Uuid": "{e5629880-98c4-5656-82b8-c9fe2b9744de}", + "InputType": 0, "OutputType": 6, + "InputOrientationArrayPath": { "Attribute Matrix Name": "CellData", "Data Array Name": "EulerAngles", "Data Container Name": "DataContainer" }, + "OutputOrientationArrayName": "leg_cu" + }, + "12": { + "FilterVersion": "1.2.724", + "Filter_Human_Label": "Write DREAM.3D Data File", + "Filter_Name": "DataContainerWriter", + "Filter_Uuid": "{3fcd4c43-9d75-5b86-aad4-4441bc914f37}", + "OutputFile": "/Users/mjackson/Workspace9/simplnx/src/Plugins/OrientationAnalysis/vv/comparisons/ConvertOrientationsFilter/results/legacy_out.dream3d", + "WriteTimeSeries": 0, + "WriteXdmfFile": 0 + }, + "PipelineBuilder": { + "Name": "ConvertOrientations Legacy A/B", + "Number_Filters": 13, + "Version": 6 + } +} diff --git a/src/Plugins/OrientationAnalysis/vv/comparisons/ConvertOrientationsFilter/pipelines/nx.d3dpipeline b/src/Plugins/OrientationAnalysis/vv/comparisons/ConvertOrientationsFilter/pipelines/nx.d3dpipeline new file mode 100644 index 0000000000..22866c6eeb --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/comparisons/ConvertOrientationsFilter/pipelines/nx.d3dpipeline @@ -0,0 +1,96 @@ +{ + "isDisabled": false, + "name": "nx.d3dpipeline", + "pinnedParams": [], + "workflowParams": [], + "pipeline": [ + { + "args": { + "import_data_object": { + "value": { "data_paths": [], "file_path": "/Users/mjackson/Workspace9/simplnx/src/Plugins/OrientationAnalysis/vv/comparisons/ConvertOrientationsFilter/results/legacy_out.dream3d", "path_import_policy": 0 }, + "version": 2 + }, + "parameters_version": 1 + }, + "comments": "Read the legacy 6.5.171 output so the EulerAngles input is byte-identical to the legacy run.", + "filter": { "name": "nx::core::ReadDREAM3DFilter", "uuid": "0dbd31c7-19e0-4077-83ef-f4a6459a0e2d" }, + "isDisabled": false + }, + { + "args": { + "input_orientation_array_path": { "value": "DataContainer/CellData/EulerAngles", "version": 1 }, + "input_representation_index": { "value": 0, "version": 1 }, + "output_orientation_array_name": { "value": "nx_om", "version": 1 }, + "output_representation_index": { "value": 1, "version": 1 }, + "parameters_version": 1 + }, + "filter": { "name": "nx::core::ConvertOrientationsFilter", "uuid": "501e54e6-a66f-4eeb-ae37-00e649c00d4b" }, + "isDisabled": false + }, + { + "args": { + "input_orientation_array_path": { "value": "DataContainer/CellData/EulerAngles", "version": 1 }, + "input_representation_index": { "value": 0, "version": 1 }, + "output_orientation_array_name": { "value": "nx_qu", "version": 1 }, + "output_representation_index": { "value": 2, "version": 1 }, + "parameters_version": 1 + }, + "filter": { "name": "nx::core::ConvertOrientationsFilter", "uuid": "501e54e6-a66f-4eeb-ae37-00e649c00d4b" }, + "isDisabled": false + }, + { + "args": { + "input_orientation_array_path": { "value": "DataContainer/CellData/EulerAngles", "version": 1 }, + "input_representation_index": { "value": 0, "version": 1 }, + "output_orientation_array_name": { "value": "nx_ax", "version": 1 }, + "output_representation_index": { "value": 3, "version": 1 }, + "parameters_version": 1 + }, + "filter": { "name": "nx::core::ConvertOrientationsFilter", "uuid": "501e54e6-a66f-4eeb-ae37-00e649c00d4b" }, + "isDisabled": false + }, + { + "args": { + "input_orientation_array_path": { "value": "DataContainer/CellData/EulerAngles", "version": 1 }, + "input_representation_index": { "value": 0, "version": 1 }, + "output_orientation_array_name": { "value": "nx_ro", "version": 1 }, + "output_representation_index": { "value": 4, "version": 1 }, + "parameters_version": 1 + }, + "filter": { "name": "nx::core::ConvertOrientationsFilter", "uuid": "501e54e6-a66f-4eeb-ae37-00e649c00d4b" }, + "isDisabled": false + }, + { + "args": { + "input_orientation_array_path": { "value": "DataContainer/CellData/EulerAngles", "version": 1 }, + "input_representation_index": { "value": 0, "version": 1 }, + "output_orientation_array_name": { "value": "nx_ho", "version": 1 }, + "output_representation_index": { "value": 5, "version": 1 }, + "parameters_version": 1 + }, + "filter": { "name": "nx::core::ConvertOrientationsFilter", "uuid": "501e54e6-a66f-4eeb-ae37-00e649c00d4b" }, + "isDisabled": false + }, + { + "args": { + "input_orientation_array_path": { "value": "DataContainer/CellData/EulerAngles", "version": 1 }, + "input_representation_index": { "value": 0, "version": 1 }, + "output_orientation_array_name": { "value": "nx_cu", "version": 1 }, + "output_representation_index": { "value": 6, "version": 1 }, + "parameters_version": 1 + }, + "filter": { "name": "nx::core::ConvertOrientationsFilter", "uuid": "501e54e6-a66f-4eeb-ae37-00e649c00d4b" }, + "isDisabled": false + }, + { + "args": { + "export_file_path": { "value": "/Users/mjackson/Workspace9/simplnx/src/Plugins/OrientationAnalysis/vv/comparisons/ConvertOrientationsFilter/results/nx_out.dream3d", "version": 1 }, + "parameters_version": 1, + "write_xdmf_file": { "value": false, "version": 1 } + }, + "filter": { "name": "nx::core::WriteDREAM3DFilter", "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003" }, + "isDisabled": false + } + ], + "version": 1 +} diff --git a/src/Plugins/OrientationAnalysis/vv/comparisons/ConvertOrientationsFilter/results/.gitignore b/src/Plugins/OrientationAnalysis/vv/comparisons/ConvertOrientationsFilter/results/.gitignore new file mode 100644 index 0000000000..90f47baddc --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/comparisons/ConvertOrientationsFilter/results/.gitignore @@ -0,0 +1,3 @@ +# Regenerable A/B comparison outputs — reproduce via ../pipelines (see comparison.md) +*.dream3d +*.xdmf diff --git a/src/Plugins/OrientationAnalysis/vv/comparisons/ConvertOrientationsFilter/results/comparison.md b/src/Plugins/OrientationAnalysis/vv/comparisons/ConvertOrientationsFilter/results/comparison.md new file mode 100644 index 0000000000..f1a5c8132e --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/comparisons/ConvertOrientationsFilter/results/comparison.md @@ -0,0 +1,33 @@ +# Legacy Comparison: ConvertOrientationsFilter + +Date: 2026-06-30 + +## Test Case +Single toy orientation (Euler `(0.7853982, 0.5235988, 1.0471976)` rad = 45°/30°/60°), converted from Euler to each of the 6 other **shared** representations (Orientation Matrix, Quaternion, Axis-Angle, Rodrigues, Homochoric, Cubochoric). Stereographic excluded — no 6.5.171 equivalent (deviation D3). + +## Input Data +The legacy 6.5.171 pipeline (`pipelines/legacy_6_5_171.json`) builds the EulerAngles array from three constant scalars + `CombineAttributeArrays` and writes `results/legacy_out.dream3d`. The NX pipeline (`pipelines/nx.d3dpipeline`) **reads that same file** (NX reads legacy `.dream3d`), so the Euler input is byte-identical (verified: `np.array_equal == True`). + +## Runners +- Legacy: `/Users/mjackson/Workspace9/6.5.172/DREAM3D-Build/D3D-Rel-Qt515-6_5_171/Bin/PipelineRunner` (PipelineRunner 1.2.832, official 6.5.171) +- NX: `/Users/mjackson/Workspace9/DREAM3D-Build/NX-Com-Qt69-Vtk96-Rel/Bin/nxrunner` (1.7.0), EbsdLib 3.0.0 + +## Results — max |Δ| (legacy vs NX), per conversion + +| eu → | max \|Δ\| | +|---|---| +| Quaternion | 0 (bit-identical) | +| Axis-Angle | 0 (bit-identical) | +| Rodrigues | 0 (bit-identical) | +| Homochoric | 0 (bit-identical) | +| Orientation Matrix | 1.49e-08 | +| Cubochoric | 1.78e-06 | + +**Overall max |Δ| = 1.78e-06** (worst single component: cubochoric `cu[1]`, legacy `0.04432392` vs NX `0.04432571`). All conversions agree within 1e-5; four are bit-identical. + +## Fixes Applied +None. SIMPLNX is independently verified-correct against the Class 3 / Class 1 / Class 4 oracle; the legacy output is not wrong, so no legacy patch and no NX change. The sub-2e-6 differences are float32 round-off from library-generation drift: both legacy and NX dispatch each pair to the same direct pairwise transform (e.g. `eu2om`, `eu2cu`), but legacy links OrientationLib while NX links EbsdLib 3.x, and the two accumulate rounding differently. Largest in cubochoric, which involves a cube-root + series expansion most sensitive to intermediate precision. + +## Notes +- Confirms deviation **ConvertOrientationsFilter-D1** (order of operations + library): differences ≤ ~1.8e-6, recommendation "either acceptable within tolerance ~1e-5". +- D2 (float64 scope) not exercised here (input is float32). D3 (Stereographic) has no legacy equivalent. D4 (error codes) is a preflight-only difference. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeKernelAvgMisorientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeKernelAvgMisorientationsFilter.md index 902ff5672a..3874716015 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeKernelAvgMisorientationsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeKernelAvgMisorientationsFilter.md @@ -117,4 +117,4 @@ Both implementations include an `if(numVoxel == 0) { KAM[point] = 0; }` guard im ### Multi-threading model -Both implementations parallelize over the outer cell loop. Legacy uses `tbb::parallel_for` over a single dimension after marshalling; SIMPLNX uses `ParallelData3DAlgorithm` with a 3D `Range3D`. Both make concurrent reads of the shared input `DataArray`s (FeatureIds, CellPhases, Quats, CrystalStructures). Per the SIMPLNX project policy (`CLAUDE.md`), DataArray subscript access is not formally thread-safe for concurrent reads, but in practice this works for read-only access on contiguous in-memory DataStores. The algorithm has been stable under parallel execution on shipping pipelines; no thread-safety issue surfaced during the V&V cycle's 6-test suite. Out-of-core (OOC) DataStore variants would need explicit testing, but this filter explicitly calls `parallelAlgorithm.requireArraysInMemory(algArrays)` at line 196 to refuse OOC inputs. +Both implementations parallelize over the outer cell loop. Legacy uses `tbb::parallel_for` over a single dimension after marshalling; SIMPLNX uses `ParallelData3DAlgorithm` with a 3D `Range3D`. Both make concurrent reads of the shared input `DataArray`s (FeatureIds, CellPhases, Quats, CrystalStructures). Per the SIMPLNX project policy (`CLAUDE.md`), DataArray subscript access is not formally thread-safe for concurrent reads, but in practice this works for read-only access on contiguous in-memory DataStores. The algorithm has been stable under parallel execution on shipping pipelines; no thread-safety issue surfaced during the V&V cycle's 6-test suite. Out-of-core (OOC) DataStore variants would need explicit testing, but this filter explicitly calls `parallelAlgorithm.requireArraysInMemory(algArrays)` at line 196, which disables parallelization when any input array is out-of-core (the algorithm then runs serially rather than concurrently accessing the not-thread-safe OOC stores). OOC inputs are still processed correctly — just single-threaded. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ConvertOrientationsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ConvertOrientationsFilter.md new file mode 100644 index 0000000000..edb2a1b4f3 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ConvertOrientationsFilter.md @@ -0,0 +1,99 @@ +# Deviations from DREAM3D 6.5.171: ConvertOrientationsFilter + +This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent (`ConvertOrientations`, SIMPL UUID `e5629880-98c4-5656-82b8-c9fe2b9744de`). + +Entries are referenced by stable ID (`ConvertOrientationsFilter-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. + +The SIMPLNX algorithm is a **Rewrite** of the filter plumbing under the retained UUID (see `../ConvertOrientationsFilter.md`). All four deviations below are consequences of that rewrite. SIMPLNX is verified-correct independently of 6.5.171 against the Class 3 (Rowenhorst 2015) / Class 1 / Class 4 oracle encoded in `test/ConvertOrientationsTest.cpp` (1032 assertions); these entries are diff-explanation, not correctness findings. + +> **Comparison status:** D1, D2, D4 are derived from a side-by-side reading of both implementations (`DREAM3D/.../OrientationAnalysisFilters/ConvertOrientations.cpp` @ 6.5.171 vs `Algorithms/ConvertOrientations.cpp`). A live A/B run is tracked in the report's `Legacy comparison` row. + +--- + +## ConvertOrientationsFilter-D1 + +| Field | Value | +|---|---| +| **Deviation ID** | `ConvertOrientationsFilter-D1` | +| **Filter UUID** | `501e54e6-a66f-4eeb-ae37-00e649c00d4b` (SIMPL `e5629880-98c4-5656-82b8-c9fe2b9744de`) | +| **Status** | active | + +**Symptom:** For a given (input, output) representation pair, SIMPLNX and 6.5.171 may differ in the last 1–2 significant figures of float32 output. **Measured** on the toy orientation (Euler 45°/30°/60°): eu→Quaternion, eu→Axis-Angle, eu→Rodrigues, eu→Homochoric are **bit-identical**; eu→OrientationMatrix differs by ≤ 1.5e-8; eu→Cubochoric by ≤ 1.8e-6 (worst single component). Overall max |Δ| = **1.78e-6**. *Scope: the A/B measured only the six Euler-input pairs on a single in-range orientation; the other 36 legacy-shared pairs were compared by source reading only, so the numeric bound above is established for eu→X and inferred (not measured) elsewhere.* + +**Root cause:** *library-generation drift (OrientationLib vs EbsdLib 3.x).* Both implementations dispatch each requested pair to a **direct** pairwise transform — 6.5.171 `OrientationConverter::convertRepresentationTo()` calls per-pair `toX()` methods that invoke e.g. `eu2om` (the Euler closed form, `OC_CONVERT_BODY(9, OrientationMatrix, eu2om, Eu2Om)`, `OrientationConverter.hpp:492`) and `eu2cu` (which chains `eu2ho→ho2cu`, `:517`); SIMPLNX calls the EbsdLib `input.toX()` members which take the same nominal routes. The residual deltas come from implementation differences accumulated between legacy **OrientationLib** and **EbsdLib 3.x** (constant definitions, expression ordering, series-evaluation details) — largest for Cubochoric, whose cube-root + series expansion is most sensitive to intermediate float32 round-off. That four of six conversions are bit-identical is consistent with this: those transforms' code paths are unchanged between library generations. Both implement the same Rowenhorst 2015 equations; neither is "more correct." *(An earlier draft of this entry attributed the deltas to legacy routing everything through a quaternion intermediate; a source check of `OrientationConverter.hpp` disproved that mechanism and it was corrected here.)* See `../comparisons/ConvertOrientationsFilter/results/comparison.md`. + +**Affected users:** Anyone diffing SIMPLNX output bit-for-bit against archived 6.5.171 output. Differences (≤ ~2e-6) are below visualization and typical downstream-analysis thresholds. + +**Recommendation:** *either acceptable within tolerance ~1e-5.* Trust SIMPLNX; both satisfy the Rowenhorst oracle within float32 precision, and the measured A/B difference is ≤ 1.8e-6. + +--- + +## ConvertOrientationsFilter-D2 + +| Field | Value | +|---|---| +| **Deviation ID** | `ConvertOrientationsFilter-D2` | +| **Filter UUID** | `501e54e6-a66f-4eeb-ae37-00e649c00d4b` (SIMPL `e5629880-98c4-5656-82b8-c9fe2b9744de`) | +| **Status** | active | + +**Symptom:** A pipeline that supplied a **`double`** (float64) orientation array to 6.5.171 cannot supply one to SIMPLNX (the input parameter accepts float32 only), and the output dtype is float32 rather than float64. + +**Root cause:** *precision (deliberate scope reduction).* 6.5.171 `ConvertOrientations::execute()` branched on the input array type and ran the conversion in `double` for `DoubleArrayType` inputs (`generateRepresentation`). SIMPLNX restricts the input `ArraySelectionParameter` to `DataType::float32` and converts in float32 only. For float64 inputs the legacy intermediate math carried ~16 digits vs SIMPLNX's ~7. + +**Affected users:** The small number of legacy pipelines that stored orientations as `double`. Standard EBSD ingest produces float32 orientations, which are unaffected. + +**Recommendation:** *trust SIMPLNX for float32 workflows.* Users with float64 orientation arrays who require double-precision conversion should note this scope reduction; for EBSD-scale data float32 is the native precision and the difference is immaterial. + +--- + +## ConvertOrientationsFilter-D3 + +| Field | Value | +|---|---| +| **Deviation ID** | `ConvertOrientationsFilter-D3` | +| **Filter UUID** | `501e54e6-a66f-4eeb-ae37-00e649c00d4b` (SIMPL `e5629880-98c4-5656-82b8-c9fe2b9744de`) | +| **Status** | active | + +**Symptom:** SIMPLNX offers a **Stereographic** representation (input/output type index 7) that 6.5.171 does not. + +**Root cause:** *algorithmic choice (new capability).* 6.5.171 `generateRepresentation` constructed a 7-element converter vector (Euler, OrientationMatrix, Quaternion, AxisAngle, Rodrigues, Homochoric, Cubochoric); Stereographic did not exist. SIMPLNX adds the 8th type, verified analytically (Class 1, `st = (x,y,z)/(1+w)`) in the unit test. + +**Affected users:** None negatively — purely additive. There is no 6.5.171 output to compare against for any conversion involving Stereographic. + +**Recommendation:** *trust SIMPLNX.* New capability with an independent analytical oracle; no legacy equivalent exists. + +--- + +## ConvertOrientationsFilter-D4 + +| Field | Value | +|---|---| +| **Deviation ID** | `ConvertOrientationsFilter-D4` | +| **Filter UUID** | `501e54e6-a66f-4eeb-ae37-00e649c00d4b` (SIMPL `e5629880-98c4-5656-82b8-c9fe2b9744de`) | +| **Status** | active | + +**Symptom:** Selecting the same representation for input and output produces an error in SIMPLNX (`-67005`) before any computation; 6.5.171 errored similarly (`-1000`) but with a different code/message, and 6.5.171 additionally emitted distinct error codes for out-of-range type indices (`-1001`/`-1002`). + +**Root cause:** *algorithmic choice (preflight refactor).* SIMPLNX delegates input/output type-range validation to the `ChoicesParameter` (which emits the framework `k_Validate_OutOfRange_Error`) and keeps only the same-type check (`-67005`) and array-shape checks (`-67003`/`-67004`) in `preflightImpl`. The legacy `-1001`/`-1002`/`-1004` (converter-failure) error codes have no SIMPLNX equivalent. + +**Affected users:** Scripts or tests that matched on the legacy numeric error codes `-1000`/`-1001`/`-1002`. No effect on successful conversions. + +**Recommendation:** *trust SIMPLNX.* Behavior is equivalent (invalid configurations are still rejected at preflight); only the error-code surface changed. + +--- + +## ConvertOrientationsFilter-D5 + +| Field | Value | +|---|---| +| **Deviation ID** | `ConvertOrientationsFilter-D5` | +| **Filter UUID** | `501e54e6-a66f-4eeb-ae37-00e649c00d4b` (SIMPL `e5629880-98c4-5656-82b8-c9fe2b9744de`) | +| **Status** | active | + +**Symptom:** For Euler-angle **input** outside `[0,2π]×[0,π]×[0,2π]`, 6.5.171 and SIMPLNX produce **genuinely different orientations** (not float round-off), and 6.5.171 additionally **mutated the user's stored input array** while SIMPLNX leaves it untouched. + +**Root cause:** *algorithmic choice (sanitization removed).* Every legacy `toX()` ran `sanityCheckInputData()` (`OC_CONVERT_BODY`, `OrientationConverter.hpp:386,403`); for Euler input, `EulerConverter::sanityCheckInputData()` ran `EulerSanityCheck` (`:425-446`) **in place on the actual stored array**: `fmod(φ1, 2π)`, `fmod(Φ, π)`, `fmod(φ2, 2π)` followed by sign flips for negative values. Two consequences: (a) the persisted `EulerAngles` array was silently modified as a side effect of running the filter; (b) `fmod(Φ, π)` and the sign flips are **not rotation-preserving**, so for out-of-range input the legacy conversion result corresponds to a *different orientation* than the one stored. SIMPLNX copies each tuple into a local `OrientationF` and never normalizes, converting exactly the orientation the user supplied. The A/B comparison (D1) used only in-range angles, so this difference did not appear in the measured deltas. + +**Affected users:** Pipelines feeding Euler angles outside the fundamental Bunge ranges (e.g. negative angles from upstream arithmetic, or Φ ∈ (π, 2π)). Under 6.5.171 those inputs were silently rewritten and converted as a different orientation; under SIMPLNX they convert as-supplied (EbsdLib's transforms are periodic in φ1/φ2, so only Φ out of `[0,π]` yields a mathematically distinct rotation description). + +**Recommendation:** *trust SIMPLNX.* Mutating input data as a side effect was a defect-prone behavior, and `fmod(Φ, π)` silently changed the orientation. Users with out-of-range Euler data should normalize it explicitly (upstream) rather than rely on a lossy implicit rewrite.