From 26160afb68f32a2ea15abba94105d5198fe66024 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Thu, 20 Aug 2026 20:59:44 -0400 Subject: [PATCH 1/3] VV: Compute Schmids fully V&V'ed Summary: - Found and fixed 4 bugs (two in EbsdLib on topic/3_1_1_staging: CubicOps normalized the Schmid factor with the float literals 1.732f/1.414f instead of sqrt(3)/sqrt(2), inflating every value by +0.0180353% and letting it exceed the physical maximum of 0.5; and HexagonalLowOps read schmidfactor uninitialized while the seven no-slip-system Laue classes left angleComps untouched, leaking the previous Feature's angles. Two in simplnx: all five output arrays were created with no fill value where legacy passed initValue 0, so the filter stated no requirement about the rows it never writes -- a latent port regression, since the in-core store factory supplies a zero initValue of its own, established by experiment and recorded as such; and crystalStructures[featurePhases[i]] was an unbounded index); - documented 5 deviations from DREAM3D 6.5.171 (D1 +0.018% normalizer bias, D2 -301 sentinel in the legacy angle arrays, D3 undefined EbsdLib Schmid outputs plus twelve known-open SC-2-class hexagonal divisors and the new slipsys=0 sentinel, D4 the Phis/Lambdas cosines-vs-radians unit flip and renamed arrays, D5 the table-relative override-path symmetry-operator index); - retired 0 tests (both pre-existing tests kept; the archive-consuming test's three float comparisons were converted from equality to the exact per-component EbsdLib bias relationship, which is strictly stronger and keeps all 214 regression checks); - augmented existing tests with 12 value-asserting *Class 1 (Analytical) + Class 4 (Invariant)* fixtures plus 5 option/guard fixtures across 4 new test cases, 601 assertions total (measured); - added 3 V&V source-tree deliverables (report, deviations, provenance); - documented the loading-direction normalization, the tie-break rule, both Slip Systems numbering schemes and their two "no slip system found" zero cases, the Phis/Lambdas unit flip, and corrected the claim that Poles is a Miller index plus the (05) -> (04) example-pipeline citation in the user-facing doc. MERGE DEPENDENCY: this PR is blocked until EbsdLib 3.1.1 is released. Two of the four fixes live in EbsdLib and the Class 1 oracle asserts the post-fix values to 1e-6, so the branch carries a vcpkg.json bump of ebsdlib from "version>=" 3.1.0 to 3.1.1. Until that version is published, vcpkg rejects the manifest at configure time in EVERY preset -- including the local SIMPLNX_USE_LOCAL_EBSD_LIB=ON preset, because manifest version constraints are resolved regardless of which features are enabled. All verification was performed with the bump not applied, against the local EbsdLib source through the NX-Com-Qt69-Vtk96-Rel-EbsdLib preset; the bump was applied last as a manifest-only change. Do not reconfigure a build directory on this branch until 3.1.1 exists. This consequence was accepted by the requester on 2026-08-20. Signed-off-by: Michael Jackson --- .../docs/ComputeSchmidsFilter.md | 59 +- .../Filters/Algorithms/ComputeSchmids.cpp | 37 +- .../Filters/ComputeSchmidsFilter.cpp | 20 +- .../test/ComputeSchmidsTest.cpp | 738 +++++++++++++++++- .../vv/ComputeSchmidsFilter.md | 211 +++++ .../vv/deviations/ComputeSchmidsFilter.md | 159 ++++ .../vv/provenance/6_6_stats_test_v2.md | 47 ++ vcpkg-configuration.json | 2 +- vcpkg.json | 2 +- 9 files changed, 1230 insertions(+), 45 deletions(-) create mode 100644 src/Plugins/OrientationAnalysis/vv/ComputeSchmidsFilter.md create mode 100644 src/Plugins/OrientationAnalysis/vv/deviations/ComputeSchmidsFilter.md create mode 100644 src/Plugins/OrientationAnalysis/vv/provenance/6_6_stats_test_v2.md diff --git a/src/Plugins/OrientationAnalysis/docs/ComputeSchmidsFilter.md b/src/Plugins/OrientationAnalysis/docs/ComputeSchmidsFilter.md index dee10769d3..0e109e9861 100644 --- a/src/Plugins/OrientationAnalysis/docs/ComputeSchmidsFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/ComputeSchmidsFilter.md @@ -6,47 +6,62 @@ Statistics (Crystallography) ## Description -This **Filter** calculates the Schmid factor for each **Feature** (grain), which measures how favorably oriented that grain is for plastic deformation under a given loading direction. A higher Schmid factor means the grain is more likely to deform (slip) under the applied load. +This **Filter** calculates the Schmid factor of each **Feature** given its average orientation and a user defined loading axis. The Schmid Factor is the combination of the component of the axial force *F* that lies parallel to the slip direction and the component that lies perpendicular to the slip plane. The equation for the Schmid Factor is given as: -### What is the Schmid Factor? +Schmid Factor = (cos φ cos λ) -When a force is applied to a polycrystalline material, each grain responds differently depending on how its internal crystal planes are oriented relative to the loading direction. Deformation occurs by *slip* -- atoms sliding along specific crystal planes in specific directions. Each combination of a slip plane and a slip direction is called a *slip system*. +*The angle φ is the angle between the tensile axis and the slip plane normal, and λ is the angle between the tensile axis and the slip direction in the slip plane.* -The Schmid factor quantifies how much of the applied force is resolved onto a given slip system: +The **Filter** determines the Schmid factor for each **Feature** by using the above equation for all possible slip systems (given the **Feature's** crystal structure). The largest Schmid factor from all of the slip systems is stored for the **Feature**. Only the Schmid factor is used in determining which slip system's Schmid factor to report. The critical resolved shear stress for the different slip systems is not considered. -**Schmid Factor = cos(φ) × cos(λ)** +The user-supplied *Loading Direction* is normalized before use, so only its direction matters — `[1, 2, 3]` and `[3, 6, 9]` give identical results. It is a **sample-frame** direction; the **Filter** rotates it into each **Feature's** crystal frame using that **Feature's** average orientation before evaluating the slip systems. -where: +**Feature 0** is the conventional "unassigned" **Feature** and is never computed; all of its output values are zero. A **Feature** whose phase maps to a crystal structure for which no slip systems are enumerated is skipped, and all of its output values are likewise zero. -- **φ** is the angle between the loading axis and the *slip plane normal* (the direction perpendicular to the slip plane) -- **λ** is the angle between the loading axis and the *slip direction* (the direction atoms slide along within the slip plane) +### Ties Between Slip Systems -The Schmid factor ranges from 0 to 0.5. A value of 0.5 means the slip system is optimally aligned with the applied load. A value near 0 means the slip system is poorly oriented for deformation. +Candidate slip systems are compared with a strict greater-than, in the fixed order in which they are enumerated for the crystal structure. When two or more slip systems share the maximum Schmid factor — which happens for high-symmetry loading directions such as `[0, 0, 1]` or `[1, 1, 1]` in a cubic crystal — the **lowest-numbered** of the tied systems is reported. This is a property of the enumeration order, not a physical preference: the tied systems are equally favoured. -![Fig. 1: The geometric relationship between the tensile axis, slip plane, and slip direction that defines the Schmid factor.](Images/ComputeSchmids_SchmidFactor.png) +### Slip Systems Numbering Depends on Override Default Slip System -### How This Filter Works +The meaning of the *Slip Systems* output changes with the *Override Default Slip System* toggle: -1. The user specifies a **Loading Direction** as a unit vector in the sample reference frame (e.g., [0, 0, 1] for loading along the Z-axis). This is a dimensionless direction, not a magnitude. -2. For each **Feature**, the filter rotates the loading direction from the sample frame into the grain's crystal frame using the grain's average orientation. -3. The filter evaluates the Schmid factor for every slip system available for that grain's crystal structure. -4. By default, the slip system with the **largest** Schmid factor is reported. Alternatively, the user can specify a particular slip plane and slip direction to evaluate. +| Override Default Slip System | *Slip Systems* value | +|---|---| +| Off (default) | Index into the crystal structure's built-in slip system list — numbered from **0** (0–11) for Cubic-High and Cubic-Low, and from **1** (1–6) for Hexagonal-High and Hexagonal-Low | +| On | Index of the **symmetry operator** (0–23 for Cubic-High) that maps the user-supplied slip plane and slip direction onto the winning variant | -### Note +The two numbering schemes are not comparable, and neither is the base of the *off* numbering comparable between the cubic and hexagonal classes. -Only the geometric Schmid factor is considered. The critical resolved shear stress (CRSS), which varies between slip systems in real materials, is not taken into account. This means the reported "most favorable" slip system is based purely on geometric alignment, not on the actual stress required to activate slip. +A *Slip Systems* value of `0` means **"no slip system found"** in two situations, and in neither of them is it a slip-system number: -### Required Input Sources +- With *Override Default Slip System* **on** and no symmetry-operator variant producing a non-zero Schmid factor — for example a loading direction parallel to the slip plane normal, which puts the loading axis perpendicular to every slip direction. +- With the toggle **off** on a **Hexagonal-High or Hexagonal-Low** phase, where the built-in systems are numbered 1–6 and no candidate exceeds the initial Schmid factor of `0`. Because that numbering starts at 1, `0` falls outside the valid range and is unambiguous. (For the cubic classes, whose numbering starts at 0, the reported `0` in this situation is indistinguishable from a genuine win on system 0; check the reported Schmid factor, which is `0` in the degenerate case.) -- **Average Quaternions** -- the per-feature average orientation produced by [Compute Average Orientations](ComputeAvgOrientationsFilter.md). -- **Feature Phases** -- produced by [Compute Feature Phases](../SimplnxCore/ComputeFeaturePhasesFilter.md). -- **Crystal Structures** -- ensemble-level array read from EBSD data or created by [Create Ensemble Info](CreateEnsembleInfoFilter.md). +In every one of these cases the reported Schmid factor is `0`. + +With *Override Default Slip System* on, the reported index is **relative to the symmetry-operator table** of the orientation library, and several operators typically tie at the maximum Schmid factor — for a `(001)[100]` system under `[1,2,3]` loading, six of the twenty-four do. The reported index is simply the first of those in table order. It identifies *a* maximizing variant, not a uniquely determined one, and the same input run through DREAM3D 6.5.x reports a different index for the same physical answer because that version's table is ordered differently. Treat this value as a handle into the current library's table, not as a portable label. + +### Phis and Lambdas Units Depend on Override Default Slip System + +The *Phis* and *Lambdas* arrays change **units** with the same toggle, which is easy to miss because the parameter descriptions and array names do not: + +| Override Default Slip System | *Phis* / *Lambdas* contents | +|---|---| +| Off (default) | The direction **cosines** cos φ and cos λ (dimensionless, in [0, 1]) | +| On | The **angles** φ and λ themselves, in **radians** | + +With the toggle off, the reported Schmid factor is the product of the two stored values. With the toggle on, it is the product of their cosines. If you need consistent units across both modes, convert explicitly rather than assuming. + +### Poles Is Not a Miller Index + +Despite its name, the *Poles* array does **not** contain a crystallographic index. It is the unit loading direction expressed in the **Feature's** crystal frame, multiplied by 100 and **truncated toward zero** to an integer — a compact fixed-point encoding of that unit vector, retaining two decimal places. Components can be negative, and the sum of squares is approximately 10 000 rather than 1. Because the conversion truncates rather than rounds, a component whose scaled value falls near an integer can differ by one from the value you would get by rounding. Reducing the triplet to a Miller index requires dividing by the greatest common divisor yourself, and the truncation means the result is only approximate. % Auto generated parameter table will be inserted here ## Example Pipelines -+ `(05) SmallIN100 Crystallographic Statistics` ++ (04) Small IN100 Crystallographic Statistics ## License & Copyright diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.cpp index 72a0040533..ec72abbf01 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.cpp @@ -40,6 +40,10 @@ Result<> ComputeSchmids::operator()() auto* phiArray = m_DataStructure.getDataAs(m_InputValues->PhisArrayName); auto* lambdaArray = m_DataStructure.getDataAs(m_InputValues->LambdasArrayName); + // Feature 0 is the conventional unassigned-Feature sentinel and is never computed; the loop below + // starts at 1. Preflight now creates all five arrays with a "0" fill value so every tuple is + // defined regardless, but these explicit writes are kept because they are what documents the + // sentinel row's meaning at the point of use. if(m_InputValues->StoreAngleComponents) { (*phiArray)[0] = 0.0F; @@ -52,15 +56,11 @@ Result<> ComputeSchmids::operator()() slipSystems[0] = 0; size_t totalFeatures = avgQuatPtr.getNumberOfTuples(); - - int32_t slipSystem = 0; + const usize numEnsembles = crystalStructures.getNumberOfTuples(); Eigen::Vector3d sampleLoading = {m_InputValues->LoadingDirection[0], m_InputValues->LoadingDirection[1], m_InputValues->LoadingDirection[2]}; sampleLoading.normalize(); - double angleComps[2] = {0.0f, 0.0f}; - double schmid = 0.0f; - Eigen::Vector3d plane; Eigen::Vector3d direction; if(m_InputValues->OverrideSystem) @@ -79,11 +79,36 @@ Result<> ComputeSchmids::operator()() return {}; } - uint32_t laueClass = crystalStructures[featurePhases[i]]; + // The phase id indexes the Crystal Structures array directly, so it has to be validated before + // it is used. An id at or beyond the ensemble count, or a negative one, would read outside the + // array and dispatch on whatever Laue value that read produced. + const int32 phaseId = featurePhases[i]; + if(phaseId < 0) + { + return MakeErrorResult(-13502, fmt::format("Feature {} has a negative phase value of {}. Phase values index the Crystal Structures array '{}' and must be zero or greater.", i, phaseId, + m_InputValues->CrystalStructuresArrayPath.toString())); + } + if(static_cast(phaseId) >= numEnsembles) + { + return MakeErrorResult(-13501, fmt::format("Feature {} has a phase value of {} but the Crystal Structures array '{}' only has {} entries (valid phase values are 0 through {}).", i, phaseId, + m_InputValues->CrystalStructuresArrayPath.toString(), numEnsembles, numEnsembles - 1)); + } + + uint32_t laueClass = crystalStructures[phaseId]; if(laueClass >= ebsdlib::CrystalStructure::LaueGroupEnd) { continue; } + + // Re-initialized every iteration on purpose. EbsdLib Laue ops that enumerate no slip systems + // for their class return without writing every output, so a buffer hoisted out of the loop + // would carry the previous Feature's values into the current Feature's row. Fixed in EbsdLib + // 3.1.1-staging as well; keeping the locals loop-scoped means this filter is correct against + // any EbsdLib. + double schmid = 0.0; + double angleComps[2] = {0.0, 0.0}; + int32_t slipSystem = 0; + auto om = ebsdlib::QuaternionDType(avgQuatPtr[i * 4 + 0], avgQuatPtr[i * 4 + 1], avgQuatPtr[i * 4 + 2], avgQuatPtr[i * 4 + 3]).toOrientationMatrix(); Eigen::Vector3d crystalLoading = om * sampleLoading; diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeSchmidsFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeSchmidsFilter.cpp index d3f1be9c07..f50e296ae8 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeSchmidsFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeSchmidsFilter.cpp @@ -135,31 +135,37 @@ IFilter::PreflightResult ComputeSchmidsFilter::preflightImpl(const DataStructure const Int32Array& phases = dataStructure.getDataRefAs(pFeaturePhasesArrayPathValue); auto tupleShape = phases.getIDataStore()->getTupleShape(); + // All five output arrays are created with an explicit "0" fill value. The algorithm skips any + // Feature whose Laue class has no slip systems enumerated for it, and Feature 0 is a sentinel + // slot that is never computed, so without the fill those tuples would be handed to the user as + // whatever the allocator happened to hold. Legacy SIMPL FindSchmids allocated with initValue 0. + const std::string k_ZeroFill = "0"; + // Create output Schmids Array { - auto createArrayAction = std::make_unique(DataType::float32, tupleShape, std::vector{1}, pSchmidsArrayNameValue); + auto createArrayAction = std::make_unique(DataType::float32, tupleShape, std::vector{1}, pSchmidsArrayNameValue, "", k_ZeroFill); resultOutputActions.value().appendAction(std::move(createArrayAction)); } // Create output SlipSystems Array { - auto createArrayAction = std::make_unique(DataType::int32, tupleShape, std::vector{1}, pSlipSystemsArrayNameValue); + auto createArrayAction = std::make_unique(DataType::int32, tupleShape, std::vector{1}, pSlipSystemsArrayNameValue, "", k_ZeroFill); resultOutputActions.value().appendAction(std::move(createArrayAction)); } - // Create output SlipSystems Array + // Create output Poles Array { - auto createArrayAction = std::make_unique(DataType::int32, tupleShape, std::vector{3}, pPolesArrayNameValue); + auto createArrayAction = std::make_unique(DataType::int32, tupleShape, std::vector{3}, pPolesArrayNameValue, "", k_ZeroFill); resultOutputActions.value().appendAction(std::move(createArrayAction)); } - // Create output SlipSystems Array + // Create output Phis Array if(pStoreAngleComponentsValue) { - auto createArrayAction = std::make_unique(DataType::float32, tupleShape, std::vector{1}, pPhisArrayNameValue); + auto createArrayAction = std::make_unique(DataType::float32, tupleShape, std::vector{1}, pPhisArrayNameValue, "", k_ZeroFill); resultOutputActions.value().appendAction(std::move(createArrayAction)); } // Create output Lambdas Array if(pStoreAngleComponentsValue) { - auto createArrayAction = std::make_unique(DataType::float32, tupleShape, std::vector{1}, pLambdasArrayNameValue); + auto createArrayAction = std::make_unique(DataType::float32, tupleShape, std::vector{1}, pLambdasArrayNameValue, "", k_ZeroFill); resultOutputActions.value().appendAction(std::move(createArrayAction)); } diff --git a/src/Plugins/OrientationAnalysis/test/ComputeSchmidsTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeSchmidsTest.cpp index 7258f3cc55..3ea56a7b38 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeSchmidsTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeSchmidsTest.cpp @@ -2,6 +2,8 @@ #include "OrientationAnalysis/OrientationAnalysis_test_dirs.hpp" #include "simplnx/Core/Application.hpp" +#include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Parameters/ArrayCreationParameter.hpp" #include "simplnx/Parameters/BoolParameter.hpp" #include "simplnx/Parameters/VectorParameter.hpp" @@ -9,10 +11,15 @@ #include "simplnx/Pipeline/PipelineFilter.hpp" #include "simplnx/UnitTest/UnitTestCommon.hpp" +#include + #include +#include +#include #include #include +#include namespace fs = std::filesystem; using namespace nx::core; @@ -29,6 +36,177 @@ const std::string k_LambdasArrayName("Schmid_Lambdas"); const std::string k_CalculatedArrayPrefix("Calculated_"); } // namespace +// ===================================================================================== +// V&V hand-built fixtures (Class 1 Analytical + Class 4 Invariant) +// ===================================================================================== +// +// ORACLE PROVENANCE +// ----------------- +// Every expected number below was derived by hand from the ALGORITHM SOURCE, before +// the filter was ever run, and independently cross-checked at 80 significant digits by +// ww_work/ComputeSchmids/oracle.py (mpmath). No value here was read back from a filter +// run or from a legacy DREAM3D run. +// +// The chain being modelled is: +// +// 1. simplnx ComputeSchmids.cpp: +// sampleLoading = normalize(LoadingDirection) +// om = QuaternionD(qx, qy, qz, qw).toOrientationMatrix() +// crystalLoading = om * sampleLoading +// Poles[3i+k] = static_cast(crystalLoading[k] * 100.0) // TRUNCATES toward zero +// +// 2. EbsdLib Quaternion::toOrientationMatrix() (row-major, epsijk = +1 so NO transpose; +// OrientationFwd.hpp defines DREAM3D_PASSIVE_ROTATION => epsijkd == 1.0): +// qq = w*w - (x*x + y*y + z*z) +// om = [ qq+2x*x, 2(xy - wz), 2(xz + wy) ] +// [ 2(yx + wz), qq+2y*y, 2(yz - wx) ] +// [ 2(zx - wy), 2(zy + wx), qq+2z*z ] +// +// 3. EbsdLib CubicOps::getSchmidFactorAndSS(load, m, angleComps, slipsys) -- the AUTO path. +// With L = crystalLoading and mag = |L|: +// theta1..4 = |Lx+Ly+Lz|, |Lx+Ly-Lz|, |Lx-Ly+Lz|, |-Lx+Ly+Lz| all / (mag * sqrt(3)) +// lambda1..6 = |Lx+Ly|, |Lx+Lz|, |Lx-Ly|, |Lx-Lz|, |Ly+Lz|, |Ly-Lz| all / (mag * sqrt(2)) +// theta_i is cos(phi) against a {111} plane normal; lambda_j is cos(lambda) against a +// <110> slip direction. The twelve FCC systems are enumerated, in source order, as +// (theta, lambda) index pairs: +// idx: 0 1 2 3 4 5 +// t1*l6 t1*l4 t1*l3 t2*l3 t2*l2 t2*l5 +// idx: 6 7 8 9 10 11 +// t3*l1 t3*l5 t3*l4 t4*l1 t4*l2 t4*l6 +// slipsys is seeded to 0 with schmidfactor = idx0 and each later index is taken only on a +// STRICT '>' -- so ties resolve to the LOWEST index. angleComps carries the (theta, lambda) +// of the winner, i.e. COSINES on this path (see the SC-3 note on the override path). +// +// SC-2 NOTE (EbsdLib <= 3.1.0): the sqrt(3)/sqrt(2) normalizers above were the float literals +// 1.732f / 1.414f, which inflate every Schmid factor by the uniform factor +// sqrt(6) / (1.732f * 1.414f) = 1.00018035284 (+0.0180353 %) +// and let m exceed the physical cubic maximum of 0.5 (0.500090176 at the maximizing +// direction). Fixed on EbsdLib topic/3_1_1_staging; the values asserted below are the +// EXACT-arithmetic post-fix values and will fail by ~7.4e-5 against EbsdLib <= 3.1.0. +namespace +{ +constexpr float64 k_Tol = 1.0e-6; + +const std::string k_GeomName("DataContainer"); +const std::string k_FeatureAMName("Cell Feature Data"); +const std::string k_EnsembleAMName("Cell Ensemble Data"); +const std::string k_PhasesName("Phases"); +const std::string k_AvgQuatsName("AvgQuats"); +const std::string k_CrystalStructuresName("CrystalStructures"); + +const DataPath k_FeatureAMPath({k_GeomName, k_FeatureAMName}); +const DataPath k_EnsembleAMPath({k_GeomName, k_EnsembleAMName}); + +/** + * @brief One hand-built fixture: N features (feature 0 is the conventional sentinel) and + * numEnsembles ensemble slots. Ensemble 0 is the 999 "unknown" sentinel, ensemble 1 is + * Cubic_High. Every feature defaults to phase 1 with the identity quaternion. + */ +struct SchmidFixture +{ + DataStructure ds; + Int32Array* featurePhases = nullptr; + Float32Array* avgQuats = nullptr; + UInt32Array* crystalStructures = nullptr; +}; + +SchmidFixture MakeFixture(usize numFeatures, usize numEnsembles = 2) +{ + SchmidFixture td; + auto* imageGeom = ImageGeom::Create(td.ds, k_GeomName); + imageGeom->setDimensions({1, 1, 1}); + imageGeom->setSpacing({1.0F, 1.0F, 1.0F}); + imageGeom->setOrigin({0.0F, 0.0F, 0.0F}); + + auto* featureAM = AttributeMatrix::Create(td.ds, k_FeatureAMName, ShapeType{numFeatures}, imageGeom->getId()); + auto* ensembleAM = AttributeMatrix::Create(td.ds, k_EnsembleAMName, ShapeType{numEnsembles}, imageGeom->getId()); + + td.featurePhases = UnitTest::CreateTestDataArray(td.ds, k_PhasesName, {numFeatures}, {1}, featureAM->getId()); + td.avgQuats = UnitTest::CreateTestDataArray(td.ds, k_AvgQuatsName, {numFeatures}, {4}, featureAM->getId()); + td.crystalStructures = UnitTest::CreateTestDataArray(td.ds, k_CrystalStructuresName, {numEnsembles}, {1}, ensembleAM->getId()); + + for(usize i = 0; i < numFeatures; ++i) + { + (*td.featurePhases)[i] = 1; + (*td.avgQuats)[i * 4 + 0] = 0.0F; // x + (*td.avgQuats)[i * 4 + 1] = 0.0F; // y + (*td.avgQuats)[i * 4 + 2] = 0.0F; // z + (*td.avgQuats)[i * 4 + 3] = 1.0F; // w -> identity + } + (*td.featurePhases)[0] = 0; // feature 0 is the sentinel; the algorithm starts its loop at 1 + for(usize e = 0; e < numEnsembles; ++e) + { + (*td.crystalStructures)[e] = ebsdlib::CrystalStructure::UnknownCrystalStructure; + } + if(numEnsembles > 1) + { + (*td.crystalStructures)[1] = ebsdlib::CrystalStructure::Cubic_High; // EbsdLib LaueOps index 1 + } + return td; +} + +void SetQuat(SchmidFixture& td, usize featureIdx, const std::array& q) +{ + (*td.avgQuats)[featureIdx * 4 + 0] = q[0]; + (*td.avgQuats)[featureIdx * 4 + 1] = q[1]; + (*td.avgQuats)[featureIdx * 4 + 2] = q[2]; + (*td.avgQuats)[featureIdx * 4 + 3] = q[3]; +} + +Arguments MakeArgs(const std::vector& loading, bool storeAngleComps, bool overrideSystem, const std::vector& plane = {0.0F, 0.0F, 1.0F}, + const std::vector& direction = {1.0F, 0.0F, 0.0F}) +{ + Arguments args; + args.insertOrAssign(ComputeSchmidsFilter::k_LoadingDirection_Key, std::make_any(loading)); + args.insertOrAssign(ComputeSchmidsFilter::k_StoreAngleComponents_Key, std::make_any(storeAngleComps)); + args.insertOrAssign(ComputeSchmidsFilter::k_OverrideSystem_Key, std::make_any(overrideSystem)); + args.insertOrAssign(ComputeSchmidsFilter::k_SlipPlane_Key, std::make_any(plane)); + args.insertOrAssign(ComputeSchmidsFilter::k_SlipDirection_Key, std::make_any(direction)); + args.insertOrAssign(ComputeSchmidsFilter::k_FeaturePhasesArrayPath_Key, std::make_any(k_FeatureAMPath.createChildPath(k_PhasesName))); + args.insertOrAssign(ComputeSchmidsFilter::k_AvgQuatsArrayPath_Key, std::make_any(k_FeatureAMPath.createChildPath(k_AvgQuatsName))); + args.insertOrAssign(ComputeSchmidsFilter::k_CrystalStructuresArrayPath_Key, std::make_any(k_EnsembleAMPath.createChildPath(k_CrystalStructuresName))); + args.insertOrAssign(ComputeSchmidsFilter::k_SchmidsArrayName_Key, std::make_any(k_SchmidsArrayName)); + args.insertOrAssign(ComputeSchmidsFilter::k_SlipSystemsArrayName_Key, std::make_any(k_SlipSystemsArrayName)); + args.insertOrAssign(ComputeSchmidsFilter::k_PolesArrayName_Key, std::make_any(k_PolesArrayName)); + args.insertOrAssign(ComputeSchmidsFilter::k_PhisArrayName_Key, std::make_any(k_PhisArrayName)); + args.insertOrAssign(ComputeSchmidsFilter::k_LambdasArrayName_Key, std::make_any(k_LambdasArrayName)); + return args; +} + +/** + * @brief Asserts the five outputs of one feature against the oracle. Angle components are + * checked only when phis/lambdas are >= 0 (a negative sentinel means "do not check"). + */ +void CheckFeature(const DataStructure& ds, usize featureIdx, float64 expectedM, int32 expectedSlipSystem, const std::array& expectedPoles, float64 expectedPhi, float64 expectedLambda) +{ + REQUIRE_NOTHROW(ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SchmidsArrayName))); + REQUIRE_NOTHROW(ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SlipSystemsArrayName))); + REQUIRE_NOTHROW(ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PolesArrayName))); + const auto& schmids = ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SchmidsArrayName)); + const auto& slipSystems = ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SlipSystemsArrayName)); + const auto& poles = ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PolesArrayName)); + + CAPTURE(featureIdx); + CHECK(static_cast(schmids[featureIdx]) == Approx(expectedM).margin(k_Tol)); + CHECK(slipSystems[featureIdx] == expectedSlipSystem); + for(usize k = 0; k < 3; ++k) + { + CAPTURE(k); + CHECK(poles[featureIdx * 3 + k] == expectedPoles[k]); + } + + if(expectedPhi >= 0.0) + { + REQUIRE_NOTHROW(ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PhisArrayName))); + REQUIRE_NOTHROW(ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_LambdasArrayName))); + const auto& phis = ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PhisArrayName)); + const auto& lambdas = ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_LambdasArrayName)); + CHECK(static_cast(phis[featureIdx]) == Approx(expectedPhi).margin(k_Tol)); + CHECK(static_cast(lambdas[featureIdx]) == Approx(expectedLambda).margin(k_Tol)); + } +} +} // namespace + TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter", "[OrientationAnalysis][ComputeSchmidsFilter]") { UnitTest::LoadPlugins(); @@ -75,26 +253,77 @@ TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter", "[OrientationAnalysis][Co SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); } - // Compare the float output arrays with those precalculated from the file + // The two INTEGER outputs must still match the archive EXACTLY. Both are provably invariant + // under the EbsdLib 3.1.1 normalizer fix: + // * SlipSystems is an argmax over twelve candidates that the fix rescales by a single positive + // factor, so the ranking -- and therefore the winner -- cannot move. + // * Poles is derived from crystalLoading, which the fix does not touch at all. + // Keeping these exact is the check that the fix changed only what it was supposed to. { - std::vector comparisonNames = {k_SchmidsArrayName, k_PhisArrayName, k_LambdasArrayName}; + const std::vector comparisonNames = {k_SlipSystemsArrayName, k_PolesArrayName}; for(const auto& comparisonName : comparisonNames) { const DataPath exemplarPath({k_DataContainer, k_CellFeatureData, comparisonName}); const DataPath calculatedPath({k_DataContainer, k_CellFeatureData, k_CalculatedArrayPrefix + comparisonName}); const auto& exemplarData = dataStructure.getDataRefAs(exemplarPath); const auto& calculatedData = dataStructure.getDataRefAs(calculatedPath); - UnitTest::CompareDataArrays(exemplarData, calculatedData, 1); + UnitTest::CompareDataArrays(exemplarData, calculatedData, 1); } + } - comparisonNames = {k_SlipSystemsArrayName, k_PolesArrayName}; - for(const auto& comparisonName : comparisonNames) + // The three FLOAT outputs no longer match the archive, and they are not supposed to. The archive + // was generated with EbsdLib <= 3.1.0, whose CubicOps::getSchmidFactorAndSS normalized with the + // float literals 1.732f and 1.414f instead of sqrt(3) and sqrt(2). Both literals differ from the + // constants they stand in for, so every archived value carries a KNOWN, UNIFORM, per-component + // inflation factor: + // + // Phis (cos phi, normalized by sqrt(3)): sqrt(3) / 1.732f = 1.0000293385 + // Lambdas (cos lambda, normalized by sqrt(2)): sqrt(2) / 1.414f = 1.0001510099 + // Schmids (their product) : sqrt(6) / (1.732f*1.414f) = 1.0001803528 + // + // Rather than retire the comparison -- which would throw away 214 real regression checks -- or + // loosen it into meaninglessness, assert the RELATIONSHIP. Every archived value must equal the + // freshly computed value times its predicted factor. That is a strictly STRONGER statement than + // the equality it replaces: it pins both the new values and the exact size and shape of the + // change, and it fails if the fix perturbed anything the bias analysis did not account for. + // + // Recompute these expectations (and regenerate the archive) if the exemplar is ever rebuilt + // against EbsdLib >= 3.1.1, at which point the factors all become 1.0. + { + const std::vector> biasedComparisons = { + {k_SchmidsArrayName, 1.0001803528351113}, + {k_PhisArrayName, 1.0000293384785181}, + {k_LambdasArrayName, 1.0001510099261918}, + }; + for(const auto& [comparisonName, biasFactor] : biasedComparisons) { const DataPath exemplarPath({k_DataContainer, k_CellFeatureData, comparisonName}); const DataPath calculatedPath({k_DataContainer, k_CellFeatureData, k_CalculatedArrayPrefix + comparisonName}); - const auto& exemplarData = dataStructure.getDataRefAs(exemplarPath); - const auto& calculatedData = dataStructure.getDataRefAs(calculatedPath); - UnitTest::CompareDataArrays(exemplarData, calculatedData, 1); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(exemplarPath)); + REQUIRE_NOTHROW(dataStructure.getDataRefAs(calculatedPath)); + const auto& exemplarData = dataStructure.getDataRefAs(exemplarPath); + const auto& calculatedData = dataStructure.getDataRefAs(calculatedPath); + REQUIRE(exemplarData.getSize() == calculatedData.getSize()); + + usize mismatches = 0; + for(usize i = 1; i < calculatedData.getSize(); i++) + { + const float64 expected = static_cast(calculatedData[i]) * biasFactor; + const float64 archived = static_cast(exemplarData[i]); + // Absolute margin covers the float32 quantization of both operands near zero; the relative + // epsilon covers it away from zero. + if(archived != Approx(expected).margin(2.0e-7).epsilon(1.0e-6)) + { + CAPTURE(comparisonName, i, archived, expected, biasFactor); + CHECK(archived == Approx(expected).margin(2.0e-7).epsilon(1.0e-6)); + if(++mismatches >= 5) + { + break; + } + } + } + CAPTURE(comparisonName); + CHECK(mismatches == 0); } } @@ -106,6 +335,499 @@ TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter", "[OrientationAnalysis][Co UnitTest::CheckArraysInheritTupleDims(dataStructure); } +// ----------------------------------------------------------------------------- +// Class 1 (Analytical): auto slip-system path, identity quaternion, Cubic_High. +// +// om == identity, so crystalLoading == normalize(LoadingDirection) EXACTLY (the quaternion +// (0,0,0,1) gives qq == 1 and every off-diagonal term is an exact zero). +// +// [0,0,1] L = (0,0,1) +// theta1..4 = 1/sqrt(3) each (|0+0+1| = |0+0-1| = |0-0+1| = |-0+0+1| = 1) +// lambda1 = 0, lambda2 = 1/sqrt(2), lambda3 = 0, lambda4..6 = 1/sqrt(2) +// schmid = [1/sqrt6, 1/sqrt6, 0, 0, 1/sqrt6, 1/sqrt6, 0, 1/sqrt6, 1/sqrt6, 0, 1/sqrt6, 1/sqrt6] +// EIGHT-way tie at 1/sqrt(6) -- indices 0, 1, 4, 5, 7, 8, 10, 11; STRICT '>' keeps index 0. +// m = 1/sqrt(6) = 0.4082482904638630, cos(phi) = theta1 = 1/sqrt(3), cos(lambda) = lambda6 = 1/sqrt(2) +// Poles = trunc(100 * (0,0,1)) = (0, 0, 100) +// +// [1,1,1] L = (1,1,1)/sqrt(3), so Lx == Ly == Lz == s +// theta1 = 3s/sqrt3 = 1 ; theta2 = theta3 = theta4 = s/sqrt3 = 1/3 (bit-identical: s+s-s == s) +// lambda1 = lambda2 = lambda5 = 2s/sqrt2 = 2/sqrt6 ; lambda3 = lambda4 = lambda6 = 0 +// schmid = [0,0,0,0, 2/(3 sqrt6) x6 at idx 4,5,6,7 and 9,10, 0 elsewhere] -- a SIX-way tie. +// first non-zero maximum is index 4 (theta2*lambda2). +// m = 2/(3*sqrt(6)) = 0.2721655269759087, cos(phi) = 1/3, cos(lambda) = 2/sqrt(6) = 0.8164965809277260 +// Poles = trunc(100 * 0.5773502691896258) = (57, 57, 57) +// +// [0,1,1] L = (0,1,1)/sqrt(2), Lx = 0, Ly == Lz == u +// theta1 = theta4 = 2u/sqrt3 = 2/sqrt6 ; theta2 = theta3 = 0 +// lambda1..4 = 0.5 ; lambda5 = 1 ; lambda6 = 0 +// schmid[0] = 0 (theta1*lambda6); schmid[1] = (2/sqrt6)(1/2) = 1/sqrt6 -> first strict win at index 1. +// indices 2, 9, 10 tie at 1/sqrt6 and are rejected by the strict '>'. +// m = 1/sqrt(6) = 0.4082482904638630, cos(phi) = 2/sqrt(6) = 0.8164965809277260, cos(lambda) = 0.5 +// This fixture is the one that SEPARATES Phis from Lambdas (0.8165 vs 0.5). +// Poles = trunc(100 * 0.7071067811865476) = (0, 70, 70) +// +// [1,2,3] L = (1,2,3)/sqrt(14) -- the enumeration-order pin: a UNIQUE maximum, no ties. +// theta1..4 = 6, 0, 2, 4 (all / sqrt(42)); lambda1..6 = 3, 4, 1, 2, 5, 1 (all / sqrt(28)) +// schmid * sqrt(1176) = [6, 12, 6, 0, 0, 0, 6, 10, 4, 12, 16, 4] -> unique max 16 at index 10. +// m = 16/sqrt(1176) = 8/sqrt(294) = 0.4665694748158435 +// cos(phi) = 4/sqrt(42) = 0.6172133998483701, cos(lambda) = 4/sqrt(28) = 0.7559289460184544 +// Poles = trunc(100 * (0.2672612419, 0.5345224838, 0.8017837257)) = (26, 53, 80) +// Every component is far from an integer boundary, so the truncation is exact, not marginal. +// +// [3,6,9] is [1,2,3] scaled by 3: the filter normalizes the loading direction, so every +// output must be identical (scale invariance, Class 4). +// ----------------------------------------------------------------------------- +TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: Class 1 analytical oracle, auto slip system", "[OrientationAnalysis][ComputeSchmidsFilter][VV]") +{ + UnitTest::LoadPlugins(); + + struct OracleRow + { + const char* label; + std::vector loading; + float64 m; + int32 slipSystem; + std::array poles; + float64 cosPhi; + float64 cosLambda; + }; + + // clang-format off + const std::vector rows = { + {"[0,0,1]", {0.0F, 0.0F, 1.0F}, 0.4082482904638630, 0, {0, 0, 100}, 0.5773502691896258, 0.7071067811865476}, + {"[1,1,1]", {1.0F, 1.0F, 1.0F}, 0.2721655269759087, 4, {57, 57, 57}, 0.3333333333333333, 0.8164965809277260}, + {"[0,1,1]", {0.0F, 1.0F, 1.0F}, 0.4082482904638630, 1, {0, 70, 70}, 0.8164965809277260, 0.5000000000000000}, + {"[1,2,3]", {1.0F, 2.0F, 3.0F}, 0.4665694748158435, 10, {26, 53, 80}, 0.6172133998483701, 0.7559289460184544}, + {"[3,6,9]", {3.0F, 6.0F, 9.0F}, 0.4665694748158435, 10, {26, 53, 80}, 0.6172133998483701, 0.7559289460184544}, + }; + // clang-format on + + for(const auto& row : rows) + { + DYNAMIC_SECTION("loading " << row.label) + { + SchmidFixture td = MakeFixture(2); + + ComputeSchmidsFilter filter; + Arguments args = MakeArgs(row.loading, true, false); + + auto preflightResult = filter.preflight(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + CheckFeature(td.ds, 1, row.m, row.slipSystem, row.poles, row.cosPhi, row.cosLambda); + + // Class 4 invariants. + const auto& schmids = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SchmidsArrayName)); + const auto& phis = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PhisArrayName)); + const auto& lambdas = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_LambdasArrayName)); + // (a) The Schmid factor of a cubic crystal cannot exceed 0.5 and cannot be negative. + // EbsdLib <= 3.1.0 violated the upper bound (max 0.500090176); this assertion is only + // satisfiable against the fixed normalizers. + CHECK(schmids[1] >= 0.0F); + CHECK(schmids[1] <= 0.5F); + // (b) On the auto path angleComps are COSINES, so m must be their product exactly. + CHECK(static_cast(schmids[1]) == Approx(static_cast(phis[1]) * static_cast(lambdas[1])).margin(k_Tol)); + // (c) Both cosines are direction cosines: in [0, 1]. + CHECK(phis[1] >= 0.0F); + CHECK(phis[1] <= 1.0F); + CHECK(lambdas[1] >= 0.0F); + CHECK(lambdas[1] <= 1.0F); + + // Feature 0 is the sentinel slot; the algorithm writes explicit zeros there. + CheckFeature(td.ds, 0, 0.0, 0, {0, 0, 0}, 0.0, 0.0); + + UnitTest::CheckArraysInheritTupleDims(td.ds); + } + } +} + +// ----------------------------------------------------------------------------- +// Class 1 + Class 4: the quaternion -> orientation-matrix convention, and equivariance. +// +// All three quaternions below are EXACTLY representable in float32 and produce an EXACT +// orientation matrix (every entry is 0 or +/-1), so there is no floating-point slack in the +// expected Poles. Loading is [1,2,3] for all three. +// +// q = (0,0,0,1) identity om = I +// crystalLoading = ( 1, 2, 3)/sqrt(14) -> SS 10, Poles ( 26, 53, 80) +// +// q = (0,0,1,0) 180 deg about Z qq = -1 -> om = diag(-1,-1,1) +// crystalLoading = (-1,-2, 3)/sqrt(14) +// theta1..4 = 0, 6, 4, 2 ; lambda1..6 = 3, 2, 1, 4, 1, 5 (over sqrt(28)) +// schmid*sqrt(1176) = [0,0,0, 6,12,6, 12,4,16, 6,4,10] -> unique max 16 at idx 8 +// -> SS 8, Poles (-26, -53, 80). NEGATIVE poles: this fixture is what +// pins truncation-toward-zero (a round() would give -27 and -53). +// +// q = (.5,.5,.5,.5) 120 deg about [111]; qq = -0.5 +// om = [[0,0,1],[1,0,0],[0,1,0]] (an ASYMMETRIC permutation matrix) +// crystalLoading = ( 3, 1, 2)/sqrt(14) +// theta1..4 = 6, 2, 4, 0 ; lambda1..6 = 4, 5, 2, 1, 3, 1 (over sqrt(28)) +// schmid*sqrt(1176) = [6,6,12, 4,10,6, 16,12,4, 0,0,0] -> unique max 16 at idx 6 +// -> SS 6, Poles (80, 26, 53). +// Transposing om would give crystalLoading = (2,3,1)/sqrt(14), hence +// SS 5 and Poles (53, 80, 26) -- so this row DISCRIMINATES the transpose. +// +// q = (1,0,0,0) 180 deg about X qq = -1 -> om = diag(1,-1,-1) +// crystalLoading = ( 1,-2,-3)/sqrt(14) +// theta1..4 = 4, 2, 0, 6 ; lambda1..6 = 1, 2, 3, 4, 5, 1 (over sqrt(28)) +// schmid*sqrt(1176) = [4,16,12, 6,4,10, 0,0,0, 6,12,6] -> unique max 16 at idx 1 +// -> SS 1, Poles (26, -53, -80). +// The three rows above all have quaternion x == y, so none of them notices a +// swapped x/y read -- a gap the mutation run found. This row closes it: swap +// x and y and the quaternion becomes (0,1,0,0), a 180 deg rotation about Y, +// giving om = diag(-1,1,-1), SS 4 and Poles (-26, 53, -80). +// +// All three rotations are cubic symmetry operations, so the Schmid factor and both angle +// components must be IDENTICAL across the three (Class 4 symmetry invariance) even though the +// winning slip-system index and the Poles differ. +// ----------------------------------------------------------------------------- +TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: orientation-matrix convention and equivariance", "[OrientationAnalysis][ComputeSchmidsFilter][VV]") +{ + UnitTest::LoadPlugins(); + + struct QuatRow + { + const char* label; + std::array quat; // (x, y, z, w) + int32 slipSystem; + std::array poles; + }; + + const float64 k_ExpectedM = 0.4665694748158435; // 8/sqrt(294) + const float64 k_ExpectedPhi = 0.6172133998483701; // 4/sqrt(42) + const float64 k_ExpectedLambda = 0.7559289460184544; // 4/sqrt(28) + + const std::vector rows = { + {"identity (0,0,0,1)", {0.0F, 0.0F, 0.0F, 1.0F}, 10, {26, 53, 80}}, + {"180 deg about Z (0,0,1,0)", {0.0F, 0.0F, 1.0F, 0.0F}, 8, {-26, -53, 80}}, + {"120 deg about [111] (.5,.5,.5,.5)", {0.5F, 0.5F, 0.5F, 0.5F}, 6, {80, 26, 53}}, + {"180 deg about X (1,0,0,0)", {1.0F, 0.0F, 0.0F, 0.0F}, 1, {26, -53, -80}}, + }; + + for(const auto& row : rows) + { + DYNAMIC_SECTION("quat " << row.label) + { + SchmidFixture td = MakeFixture(2); + SetQuat(td, 1, row.quat); + + ComputeSchmidsFilter filter; + Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, true, false); + + auto preflightResult = filter.preflight(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + CheckFeature(td.ds, 1, k_ExpectedM, row.slipSystem, row.poles, k_ExpectedPhi, k_ExpectedLambda); + + const auto& schmids = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SchmidsArrayName)); + CHECK(schmids[1] <= 0.5F); + + UnitTest::CheckArraysInheritTupleDims(td.ds); + } + } +} + +// ----------------------------------------------------------------------------- +// Class 1: the OVERRIDE slip-system path -- a DIFFERENT EbsdLib overload with different +// semantics for BOTH the slip-system index and the angle components. +// +// CubicOps::getSchmidFactorAndSS(load, plane, direction, m, angleComps, slipsys) loops over the +// 24 CubicHigh symmetry operators. For symmetry operator i: +// slipPlane = MatSym[i] * plane (== column 3 of MatSym[i] when plane == (0,0,1)) +// slipDirection = MatSym[i] * direction (== column 1 of MatSym[i] when direction == (1,0,0)) +// operators whose slipPlane[2] < 0 are skipped (duplicate-avoidance), then +// m_i = |L . slipPlane| * |L . slipDirection| +// and slipsys becomes the SYMMETRY-OPERATOR INDEX (0..23) of the strict maximum -- NOT the +// 0..11 FCC slip-system index of the auto path. +// +// SC-3 (documented deviation): angleComps on THIS path are acos() results, i.e. RADIANS, +// whereas the auto path writes raw COSINES into the same two output arrays. The Phis/Lambdas +// arrays therefore change UNITS with the OverrideSystem toggle. +// +// Every cubic symmetry operator is a signed permutation matrix, so with plane == (0,0,1) and +// direction == (1,0,0) the pair (slipPlane, slipDirection) is always a pair of DISTINCT signed +// basis vectors, and m_i = |L_a| * |L_b| for some a != b. +// +// L = (1,2,3)/sqrt(14): the maximum of |L_a||L_b| over a != b is (3/sqrt14)(2/sqrt14) = 6/14 = 3/7. +// SIX operators of the twenty-four attain that maximum -- {3, 6, 8, 9, 11, 14} -- so the +// asserted index is ENUMERATION-ORDER DEPENDENT: the strict '>' keeps the first, operator 3 = +// [[0,-1,0],[1,0,0],[0,0,1]], whose column 3 = (0,0,1) (z >= 0, kept) and column 1 = (0,1,0), +// giving cos(phi) = 3/sqrt(14) and cos(lambda) = 2/sqrt(14). The A/B run confirmed that +// DREAM3D 6.5.171, whose symmetry table holds the same group in a different order, reports 8 +// for the identical physical answer. This assertion therefore pins EbsdLib's table order, not +// a portable physical label. +// m = 3/7 = 0.4285714285714286 ; Phis = acos(3/sqrt14) = 0.6405223126794245 rad (36.699 deg) +// Lambdas = acos(2/sqrt14) = 1.0068536854342678 rad (57.688 deg) +// The two acos values are NOT closed-form rationals or surds; they were recomputed to 60 +// significant digits as atan(sqrt(5)/3) = 0.640522312679424574143... and +// atan(sqrt(10)/2) = 1.006853685434267776537... and rounded to the nearest double. They are +// asserted at k_Tol = 1e-6, which is far looser than that rounding. +// L = (1,1,1)/sqrt(3): all |L_a| equal, so every kept operator gives 1/3; the strict '>' keeps +// the FIRST, operator 0 (the identity). m = 1/3, both angles = acos(1/sqrt3) = 0.9553166181245093 rad. +// L = (0,0,1): slipPlane and slipDirection are distinct basis vectors, so at most one of them +// can be +/-Z; the product is 0 for ALL 24 operators. The strict '>' is never taken, so the +// function returns its initialized state: m = 0, slipsys = 0, angleComps = (0,0). This is the +// "no slip system found" degenerate case. +// ----------------------------------------------------------------------------- +TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: override slip system path", "[OrientationAnalysis][ComputeSchmidsFilter][VV]") +{ + UnitTest::LoadPlugins(); + + struct OverrideRow + { + const char* label; + std::vector loading; + float64 m; + int32 symOpIndex; + std::array poles; + float64 phiRadians; + float64 lambdaRadians; + }; + + // clang-format off + const std::vector rows = { + {"[1,2,3]", {1.0F, 2.0F, 3.0F}, 0.4285714285714286, 3, {26, 53, 80}, 0.6405223126794245, 1.0068536854342678}, + {"[1,1,1]", {1.0F, 1.0F, 1.0F}, 0.3333333333333333, 0, {57, 57, 57}, 0.9553166181245093, 0.9553166181245093}, + {"[0,0,1]", {0.0F, 0.0F, 1.0F}, 0.0000000000000000, 0, {0, 0, 100}, 0.0000000000000000, 0.0000000000000000}, + }; + // clang-format on + + for(const auto& row : rows) + { + DYNAMIC_SECTION("loading " << row.label) + { + SchmidFixture td = MakeFixture(2); + + ComputeSchmidsFilter filter; + Arguments args = MakeArgs(row.loading, true, true, {0.0F, 0.0F, 1.0F}, {1.0F, 0.0F, 0.0F}); + + auto preflightResult = filter.preflight(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + CheckFeature(td.ds, 1, row.m, row.symOpIndex, row.poles, row.phiRadians, row.lambdaRadians); + + // Class 4: on the override path m must equal cos(Phis) * cos(Lambdas) because the stored + // components are the ANGLES, not their cosines. This is the assertion that proves the SC-3 + // unit flip is real rather than a documentation misreading. + const auto& schmids = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SchmidsArrayName)); + const auto& phis = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PhisArrayName)); + const auto& lambdas = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_LambdasArrayName)); + if(row.m > 0.0) + { + CHECK(static_cast(schmids[1]) == Approx(std::cos(static_cast(phis[1])) * std::cos(static_cast(lambdas[1]))).margin(k_Tol)); + } + CHECK(schmids[1] <= 0.5F); + + UnitTest::CheckArraysInheritTupleDims(td.ds); + } + } + + SECTION("preflight rejects a slip direction that is not in the slip plane") + { + SchmidFixture td = MakeFixture(2); + ComputeSchmidsFilter filter; + // (0,0,1) . (1,0,1) == 1 != 0 + Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, true, true, {0.0F, 0.0F, 1.0F}, {1.0F, 0.0F, 1.0F}); + auto preflightResult = filter.preflight(td.ds, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + CHECK(preflightResult.outputActions.errors()[0].code == -13500); + } +} + +// ----------------------------------------------------------------------------- +// Option and guard coverage. +// +// SC-1: the five output arrays were created by CreateArrayAction with an EMPTY fill value, where +// legacy SIMPL FindSchmids passed initValue 0. Any feature the loop skips (`laueClass >= +// CrystalStructure::LaueGroupEnd`, ComputeSchmids.cpp:83) is never written, so those rows depend +// entirely on how the store was initialized. The fixture below is large and dirties the heap +// first, which is what settled how much of that exposure is real -- see the note inside it. +// +// SC-4: `crystalStructures[featurePhases[i]]` (ComputeSchmids.cpp:82) is unbounded. A phase id +// beyond the ensemble count, or a negative one, reads out of bounds. +// ----------------------------------------------------------------------------- +TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: options, skip path and phase guards", "[OrientationAnalysis][ComputeSchmidsFilter][VV]") +{ + UnitTest::LoadPlugins(); + + SECTION("StoreAngleComponents == false does not create Phis/Lambdas") + { + SchmidFixture td = MakeFixture(2); + ComputeSchmidsFilter filter; + Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, false, false); + + auto preflightResult = filter.preflight(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + // The three unconditional outputs are unchanged by the toggle. + CheckFeature(td.ds, 1, 0.4665694748158435, 10, {26, 53, 80}, -1.0, -1.0); + CHECK(td.ds.getDataAs(k_FeatureAMPath.createChildPath(k_PhisArrayName)) == nullptr); + CHECK(td.ds.getDataAs(k_FeatureAMPath.createChildPath(k_LambdasArrayName)) == nullptr); + + UnitTest::CheckArraysInheritTupleDims(td.ds); + } + + SECTION("features whose Laue class is beyond LaueGroupEnd get defined zeros, not stale memory") + { + // Ensemble slots: 0 = 999 sentinel, 1 = Cubic_High, 2 = 999 sentinel (>= LaueGroupEnd == 11). + // Feature 1 uses phase 1 (computed); every other feature uses phase 2 (skipped by the + // continue), so all of their output rows are ones the algorithm never writes. + // + // WHY THE FIXTURE IS LARGE, AND WHAT THAT SETTLED. The claim SC-1 originally rested on was + // that without an explicit fill the skipped rows hold indeterminate memory. Removing the + // explicit "0" fill from all five CreateArrayAction calls and running THIS fixture -- 20000 + // features, so every output array is 78 KB or more, with same-size blocks poisoned to 0xAB + // and freed immediately before the run -- still yields all zeros. That is not the allocator + // being kind: a standalone probe confirms that on this platform `new float[20000]` after a + // same-size dirty free does return 0xABABABAB, whereas the 8-to-36-byte allocations a + // three-feature fixture uses come from libmalloc's nano zone as freshly zeroed pages. So the + // zeros here are written by SIMPLNX, not inherited from the heap, and the mechanism is in the + // store factory rather than in the action: CoreDataIOManager::addDataStoreFnc() constructs + // every in-core store with `static_cast(0)` as its initValue, and DataStore's constructor + // unconditionally `std::fill_n`s the buffer with it (DataStore.hpp:66-69). The fillValue + // string on CreateArrayAction only adds a second, redundant fill. + // + // The explicit fill is therefore DEFENSIVE, not a repair of a live in-core defect: it + // restores what legacy SIMPL FindSchmids stated explicitly, and it is the only guarantee for + // any store implementation whose factory does not hard-code a zero initValue. This section + // is the regression lock on the guarantee, and the large dirty-heap fixture is what makes it + // a non-vacuous one -- at this size the assertion would catch a store that stopped filling. + constexpr usize k_FeatureCount = 20000; + SchmidFixture td = MakeFixture(k_FeatureCount, 3); + for(usize i = 2; i < k_FeatureCount; ++i) + { + (*td.featurePhases)[i] = 2; + } + + ComputeSchmidsFilter filter; + Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, true, false); + auto preflightResult = filter.preflight(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + + // Dirty the heap with blocks the same size as the five output arrays and free them + // immediately before the run, so that a default-initialized `new value_type[n]` of that size + // is served from a recycled block that still holds the poison. + { + std::vector dirtyBlocks; + for(int32 repetition = 0; repetition < 8; ++repetition) + { + for(usize byteCount : {k_FeatureCount * sizeof(float32), k_FeatureCount * 3 * sizeof(int32)}) + { + char* block = new char[byteCount]; + std::memset(block, 0xAB, byteCount); + dirtyBlocks.push_back(block); + } + } + for(char* block : dirtyBlocks) + { + delete[] block; + } + } + + auto executeResult = filter.execute(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + // Feature 1 still gets the full oracle answer. + CheckFeature(td.ds, 1, 0.4665694748158435, 10, {26, 53, 80}, 0.6172133998483701, 0.7559289460184544); + // Feature 2 was skipped: every output must be a defined zero. + CheckFeature(td.ds, 2, 0.0, 0, {0, 0, 0}, 0.0, 0.0); + + // ...and so must every other row the algorithm never writes: feature 0 (the sentinel) and + // features 2 through 19999. Aggregated into one CHECK per data type so the section stays a + // handful of assertions instead of a hundred thousand. + usize nonZeroFloatValues = 0; + for(const auto& arrayName : {k_SchmidsArrayName, k_PhisArrayName, k_LambdasArrayName}) + { + REQUIRE_NOTHROW(td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(arrayName))); + const auto& arrayRef = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(arrayName)); + REQUIRE(arrayRef.getSize() == k_FeatureCount); + for(usize i = 0; i < k_FeatureCount; ++i) + { + if(i != 1 && arrayRef[i] != 0.0F) + { + ++nonZeroFloatValues; + } + } + } + CHECK(nonZeroFloatValues == 0); + + usize nonZeroSlipSystemValues = 0; + { + REQUIRE_NOTHROW(td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SlipSystemsArrayName))); + const auto& slipSystemsRef = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SlipSystemsArrayName)); + REQUIRE(slipSystemsRef.getSize() == k_FeatureCount); + for(usize i = 0; i < k_FeatureCount; ++i) + { + if(i != 1 && slipSystemsRef[i] != 0) + { + ++nonZeroSlipSystemValues; + } + } + } + CHECK(nonZeroSlipSystemValues == 0); + + usize nonZeroPoleValues = 0; + { + REQUIRE_NOTHROW(td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PolesArrayName))); + const auto& polesRef = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PolesArrayName)); + REQUIRE(polesRef.getSize() == k_FeatureCount * 3); + for(usize i = 0; i < k_FeatureCount * 3; ++i) + { + if((i / 3) != 1 && polesRef[i] != 0) + { + ++nonZeroPoleValues; + } + } + } + CHECK(nonZeroPoleValues == 0); + + UnitTest::CheckArraysInheritTupleDims(td.ds); + } + + SECTION("a phase id beyond the ensemble count is an error, not an out-of-bounds read") + { + SchmidFixture td = MakeFixture(2); // 2 ensemble slots -> valid phase ids are 0 and 1 + (*td.featurePhases)[1] = 7; + + ComputeSchmidsFilter filter; + Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, true, false); + auto preflightResult = filter.preflight(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(td.ds, args); + SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); + REQUIRE(executeResult.result.errors().size() == 1); + CHECK(executeResult.result.errors()[0].code == -13501); + // The message must name the offending value and the ensemble count. + CHECK(executeResult.result.errors()[0].message.find('7') != std::string::npos); + CHECK(executeResult.result.errors()[0].message.find('2') != std::string::npos); + } + + SECTION("a negative phase id is an error, not an out-of-bounds read") + { + SchmidFixture td = MakeFixture(2); + (*td.featurePhases)[1] = -3; + + ComputeSchmidsFilter filter; + Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, true, false); + auto preflightResult = filter.preflight(td.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(td.ds, args); + SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); + REQUIRE(executeResult.result.errors().size() == 1); + CHECK(executeResult.result.errors()[0].code == -13502); + CHECK(executeResult.result.errors()[0].message.find("-3") != std::string::npos); + } +} + TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: SIMPL Backwards Compatibility", "[OrientationAnalysis][ComputeSchmidsFilter][BackwardsCompatibility]") { auto app = Application::GetOrCreateInstance(); diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeSchmidsFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeSchmidsFilter.md new file mode 100644 index 0000000000..c158cc4173 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/ComputeSchmidsFilter.md @@ -0,0 +1,211 @@ +# V&V Report: ComputeSchmidsFilter + +| | | +|--------|--------------| +| Plugin | OrientationAnalysis | +| SIMPLNX UUID | `b4681855-0a3d-4237-97f2-5aec509115c4` | +| DREAM3D 6.5.171 equivalent | `FindSchmids` (SIMPL UUID `e67ca06a-176f-58fc-a676-d6ee5553511a`) - `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindSchmids.{h,cpp}` | +| Verified commit | ** | +| Status | READY FOR REVIEW | +| Sign-off | Delegated to the PR reviewer (requester decision, 2026-08-18). Second-engineer oracle review is the PR review. | + +## At a glance + +| Aspect | Current state | +|---|---| +| Algorithm Relationship | Port with one port-time regression, and that one latent. The SIMPL/EbsdLib API changed and the arithmetic moved from `float` to `double`, but the control flow is line-for-line the legacy filter. What was lost in the port is the legacy `initValue 0` on the created arrays (SC-1) - real as a source-level regression, but never observable in-core because the store factory hard-codes a zero `initValue` of its own; see *Mutation verification*. A second defect, the unbounded phase-id index, is shared with legacy (SC-4). | +| Oracle (confirmed) | Class 1 (Analytical) - **12 value-asserting fixtures**: 5 auto-path loading directions, 4 exactly-representable quaternions, 3 override-path cases. Plus Class 4 (Invariant) checks for the physical bound `0 <= m <= 0.5`, `m == Phis * Lambdas`, `Poles == trunc(100 * crystalLoading)`, scale invariance and cubic-symmetry invariance, and 5 further non-value fixtures (the `StoreAngleComponents` toggle, the `LaueGroupEnd` skip row, the two phase-id guards and the non-normal slip-direction rejection). Cross-checked at 80 significant digits by `ww_work/ComputeSchmids/oracle.py`, except the two override-path `acos` values, which are transcendental and were instead recomputed to 60 significant digits and rounded to the nearest `double`. | +| Code paths enumerated | 11 of 12 paths exercised - only the cancel path is untested. | +| Tests today | 6 test cases (**601 assertions**, measured): 1 archive-consuming exemplar test, 3 new Class 1 / Class 4 test cases, 1 new options-and-guards test case, 1 SIMPL backwards-compatibility test. | +| Exemplar archive | `6_6_stats_test_v2.tar.gz` (unchanged, not regenerated). Its three float Schmid arrays were generated with EbsdLib <= 3.1.0 and are now stale by a known factor; the test asserts that exact relationship instead of equality. See *Exemplar archive*. | +| Legacy comparison | Run 2026-08-20 against DREAM3D 6.5.171 `PipelineRunner` on 10 hand-built fixtures (9 executing + 1 negative). 93 predicted relationships confirmed, 0 unexplained differences, 1 prediction amended after adjudication (D5). | +| Bug flags | SC-1 (`ComputeSchmidsFilter-D2`, fixed defensively - latent in-core, see *Mutation verification*), SC-2 (`-D1`, fixed in EbsdLib), SC-4 (fixed, no deviation - shared gap now guarded), SC-5 (`-D3`, fixed in EbsdLib + mitigated in NX; twelve further SC-2-class hexagonal divisors recorded there as **known-open**). | +| V&V phase | Phases 1-13 complete. Outstanding: OOC build run (waived, requester decision 2026-08-18); second-engineer sign-off (delegated to the PR reviewer, requester decision 2026-08-18). **This PR is merge-blocked until EbsdLib 3.1.1 is released** - see *Merge dependency*. | + +## Summary + +`ComputeSchmidsFilter` computes, for each Feature, the largest Schmid factor over the slip systems of that Feature's crystal structure, given a sample-frame loading direction and the Feature's average orientation. Verification used a Class 1 analytical oracle: the Schmid factor, the two direction cosines, the winning slip-system index and the fixed-point `Poles` encoding were all derived by hand from the algorithm source for identity and three exactly-representable rotated quaternions, then cross-checked in exact arithmetic. The cycle found and fixed four defects — two of them inside EbsdLib — and documented five deviations from DREAM3D 6.5.171, of which four were predicted from source before the comparison was run. + +## Merge dependency + +This branch bumps `vcpkg.json`'s `ebsdlib` constraint from `"version>=": "3.1.0"` to `"3.1.1"` because two of the four fixes live in EbsdLib (`topic/3_1_1_staging`, commits `4a56725` and `2c84f2a`) and the Class 1 oracle asserts the post-fix values to `1e-6`. Against EbsdLib 3.1.0 the new fixtures fail by roughly `7.4e-5`, which is the whole point of the fix. + +**Consequence, accepted by the requester on 2026-08-20:** the PR cannot pass stock CI until EbsdLib 3.1.1 is published to the vcpkg registry. Until then `vcpkg` rejects the manifest with `no version database entry for ebsdlib at 3.1.1` at *configure* time, in every preset — including the local `SIMPLNX_USE_LOCAL_EBSD_LIB=ON` preset, because the manifest's version constraints are resolved regardless of which features are enabled. All of the verification in this report was therefore performed with the bump **not** applied, against the local EbsdLib source through `NX-Com-Qt69-Vtk96-Rel-EbsdLib`, and the bump was applied last as a manifest-only change. Do not reconfigure any build directory on this branch until 3.1.1 exists. + +## Algorithm Relationship + +**Port**, with one port-time regression, and that one latent. + +`ComputeSchmids::operator()` is a statement-for-statement transcription of `FindSchmids::execute()`. Port-time deltas: + +1. **`float` to `double`.** Legacy holds the orientation matrix, the crystal-frame loading direction, the Schmid factor and the angle components in `float`; NX computes all of them in `double` and narrows only on store. Output arrays are `float32` in both. This tightens accuracy and does not change any decision — confirmed empirically, see D1. +2. **`qu2om` and the quaternion layout are unchanged.** EbsdLib's `Quaternion::toOrientationMatrix()` is line-identical to legacy's `qu2om`, the component order is `(x, y, z, w)` in both, and `epsijk == +1` (`OrientationFwd.hpp` defines `DREAM3D_PASSIVE_ROTATION`), so neither applies a transpose. The A/B run confirms this: `Poles`, which is a direct function of the orientation matrix, is bit-identical on every fixture. +3. **The `initValue` was dropped.** Legacy creates `Schmids`, `SlipSystems` and `Poles` with `createNonPrereqArrayFromPath(..., 0, ...)`. NX's `CreateArrayAction` calls passed no fill value. **This is SC-1, a port regression**, fixed here — but the exposure it carried turned out to be nil in-core: the store factory supplies its own zero `initValue` and `DataStore`'s constructor always applies it, so the rows read zero either way. Established by experiment, not assumed; see *Mutation verification*. +4. **The skip condition was inverted in form but not in meaning.** Legacy wraps the body in `if(xtal < LaueGroupEnd)`; NX uses `if(laueClass >= LaueGroupEnd) continue;`. Equivalent. +5. **Loop-scoped versus hoisted locals.** Both hoist `schmid`, `angleComps` and `ss` outside the Feature loop. Combined with EbsdLib Laue ops that do not write every output (SC-5), this leaks the previous Feature's angle components into a skipped Feature's row. NX now declares them inside the loop. +6. **Error code renumbered.** The non-normal slip plane/direction rejection is `-1001` in legacy and `-13500` in NX. Same condition, same message text, both at preflight. See D4. + +**Material PRs since baseline:** none identified for this filter. + +## Oracle + +**Class 1 (Analytical)** with **Class 4 (Invariant)** support, and Class 3 as corroboration only. + +The oracle was derived by transcribing three sources by hand — `ComputeSchmids.cpp`, EbsdLib's `Quaternion::toOrientationMatrix()`, and both `CubicOps::getSchmidFactorAndSS` overloads — into closed-form expressions, evaluating them exactly, and writing the results into the test as inline expected values with the derivation in a comment above each. The derivation is reproduced independently at 80 significant digits by `ww_work/ComputeSchmids/oracle.py`, which parses EbsdLib's own 24-element symmetry table out of the source rather than re-typing it. + +Identity quaternion, `Cubic_High`, auto slip-system path: + +| Loading | m | cos phi (Phis) | cos lambda (Lambdas) | SlipSystems | Poles | +|---|---|---|---|---|---| +| `[0,0,1]` | `1/sqrt(6)` = 0.4082482904638630 | `1/sqrt(3)` = 0.5773502691896258 | `1/sqrt(2)` = 0.7071067811865476 | 0 | (0, 0, 100) | +| `[1,1,1]` | `2/(3 sqrt(6))` = 0.2721655269759087 | `1/3` = 0.3333333333333333 | `2/sqrt(6)` = 0.8164965809277260 | 4 | (57, 57, 57) | +| `[0,1,1]` | `1/sqrt(6)` = 0.4082482904638630 | `2/sqrt(6)` = 0.8164965809277260 | `1/2` = 0.5 | 1 | (0, 70, 70) | +| `[1,2,3]` | `8/sqrt(294)` = 0.4665694748158435 | `4/sqrt(42)` = 0.6172133998483701 | `4/sqrt(28)` = 0.7559289460184544 | 10 | (26, 53, 80) | + +`[0,0,1]` and `[1,1,1]` and `[0,1,1]` all carry ties at the maximum and pin the strict-`>` tie-break to the lowest index. `[1,2,3]` has a unique maximum and pins the enumeration order. `[0,1,1]` is the row that separates `Phis` from `Lambdas` (0.8165 versus 0.5) — on the other three rows a swapped pair would go unnoticed. `[3,6,9]` repeats `[1,2,3]` to check that the loading direction is normalized. + +Rotated quaternions, loading `[1,2,3]`. Each is exactly representable in `float32` and produces an orientation matrix whose entries are all `0` or `+/-1`, so there is no floating-point slack in the expected `Poles`. All four are cubic symmetry operations, so `m` and both angle components must be identical across them while `SlipSystems` and `Poles` move — a Class 4 symmetry invariance riding on the Class 1 values: + +| Quaternion (x,y,z,w) | Orientation matrix | crystalLoading | SlipSystems | Poles | +|---|---|---|---|---| +| `(0,0,0,1)` identity | `I` | `(1,2,3)/sqrt(14)` | 10 | (26, 53, 80) | +| `(0,0,1,0)` 180 deg about Z | `diag(-1,-1,1)` | `(-1,-2,3)/sqrt(14)` | 8 | (-26, -53, 80) | +| `(0.5,0.5,0.5,0.5)` 120 deg about [111] | `[[0,0,1],[1,0,0],[0,1,0]]` | `(3,1,2)/sqrt(14)` | 6 | (80, 26, 53) | +| `(1,0,0,0)` 180 deg about X | `diag(1,-1,-1)` | `(1,-2,-3)/sqrt(14)` | 1 | (26, -53, -80) | + +Override slip-system path (`(001)[100]`, identity quaternion) — a different EbsdLib overload with different semantics for both the index and the angle components: + +| Loading | m | symmetry-operator index | Phis (radians) | Lambdas (radians) | +|---|---|---|---|---| +| `[1,2,3]` | `3/7` = 0.4285714285714286 | 3 | 0.6405223126794245 | 1.0068536854342678 | +| `[1,1,1]` | `1/3` = 0.3333333333333333 | 0 | 0.9553166181245093 | 0.9553166181245093 | +| `[0,0,1]` | 0 | 0 | 0 | 0 | + +The three angle columns on this path are `acos` values, i.e. transcendental rather than surds, so they are not covered by `oracle.py`'s exact-arithmetic table. They were recomputed independently to 60 significant digits — `acos(3/sqrt(14)) = atan(sqrt(5)/3) = 0.640522312679424574143…`, `acos(2/sqrt(14)) = atan(sqrt(10)/2) = 1.006853685434267776537…`, `acos(1/sqrt(3)) = atan(sqrt(2)) = 0.955316618124509278163…` — and rounded to the nearest `double`. The asserted tolerance is `1e-6`, far looser than that rounding. + +Class 4 invariants asserted alongside the Class 1 values: + +- `0 <= m <= 0.5` — the physical bound for a cubic crystal. **This became assertable only after the EbsdLib fix**: EbsdLib <= 3.1.0 reached 0.500090176 at the maximizing direction. Verified over a 400 x 400 direction sweep in `oracle.py`. +- `m == Phis * Lambdas` on the auto path (the stored values are cosines) and `m == cos(Phis) * cos(Lambdas)` on the override path (the stored values are angles). The pair of assertions is what proves the SC-3 unit flip is real behaviour rather than a documentation misreading. +- `Poles[k] == trunc(100 * crystalLoading[k])`, including negative components, which is what distinguishes truncation from rounding. +- Scale invariance: `[1,2,3]` and `[3,6,9]` agree on all five outputs. +- Cubic-symmetry invariance: all four quaternions give the same `m`, `Phis` and `Lambdas`. + +**Class 3 corroboration (not the oracle):** the three canonical FCC anchors fall out of the table — `m = 0.408` for `<001>` and `<011>` loading and `m = 0.272` for `<111>` loading, with `phi = 54.74 deg` / `lambda = 45 deg` and `phi = 70.53 deg` / `lambda = 35.26 deg` respectively (Dieter, *Mechanical Metallurgy* 3rd ed. section 4-3; Schmid & Boas, *Kristallplastizität*). These are quoted as a sanity check on the Class 1 derivation, not as its source. + +**Encoded test references** (all greppable in `src/Plugins/OrientationAnalysis/test/ComputeSchmidsTest.cpp`): + +- `"OrientationAnalysis::ComputeSchmidsFilter: Class 1 analytical oracle, auto slip system"` +- `"OrientationAnalysis::ComputeSchmidsFilter: orientation-matrix convention and equivariance"` +- `"OrientationAnalysis::ComputeSchmidsFilter: override slip system path"` +- `"OrientationAnalysis::ComputeSchmidsFilter: options, skip path and phase guards"` + +## Bugs found and fixed + +| ID | Where | Defect | Fix | +|---|---|---|---| +| SC-1 | simplnx, latent port regression | All five output arrays created with `CreateArrayAction`'s default empty fill value where legacy allocated with `initValue 0`. Feature 0 and any Feature whose Laue class has no slip systems are never written by the algorithm, so those rows depend entirely on how the store was initialized — and the filter was stating no requirement about that. Measured exposure in-core: **none**, because `CoreDataIOManager` hands every in-core `DataStore` a zero `initValue` of its own. | Explicit `"0"` fill on all five `CreateArrayAction` calls, plus an explicit Feature-0 row write. Defensive: it makes the filter's requirement explicit instead of inherited. | +| SC-2 | EbsdLib (Verdict C) | `CubicOps::getSchmidFactorAndSS` normalized with the `float` literals `1.732f` and `1.414f` inside an otherwise all-`double` computation. Both are smaller than the constants they approximate, inflating every Schmid factor by `sqrt(6)/(1.732f*1.414f) = 1.00018035284` and letting `m` exceed the physical maximum of 0.5. | `ebsdlib::constants::k_Sqrt3D` / `k_Sqrt2D`. EbsdLib `4a56725`. | +| SC-4 | shared with legacy | `crystalStructures[featurePhases[i]]` was unbounded — a phase id at or beyond the ensemble count, or a negative one, read outside the array and dispatched on whatever that read produced. | Bounds guard in the algorithm: `-13501` for too-large, `-13502` for negative, both naming the offending value and the ensemble count. | +| SC-5 | EbsdLib (Verdict C) + simplnx mitigation | `HexagonalLowOps::getSchmidFactorAndSS` **read** `schmidfactor` uninitialized and never initialized `slipsys`; `HexagonalOps` left `slipsys` and `angleComps` undefined; the seven Laue classes with no enumerated slip systems set `schmidfactor` and `slipsys` but left `angleComps` untouched, so a caller with a hoisted buffer got the previous Feature's angles. | All four outputs seeded in every affected overload (EbsdLib `2c84f2a`), plus NX-side per-iteration reinitialization of the three locals so the filter is correct against any EbsdLib. | + +## Code path coverage + +`Source: src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.cpp (137 lines).` + +The algorithm is flat: a parameter setup block, a sentinel-row write, then one pass over Features. + +11 of 12 paths exercised. + +| # | Phase | Path | Test case | +|---|---|---|---| +| 1 | *Preflight* | Create the three unconditional output arrays with a zero fill | all four V&V test cases | +| 2 | *Preflight* | Create `Phis`/`Lambdas` when `StoreAngleComponents` is true | "Class 1 analytical oracle, auto slip system" | +| 3 | *Preflight* | Skip `Phis`/`Lambdas` when `StoreAngleComponents` is false | "options, skip path and phase guards" / "StoreAngleComponents == false does not create Phis/Lambdas" | +| 4 | *Preflight* | Reject a slip direction not lying in the slip plane (`-13500`) | "override slip system path" / "preflight rejects a slip direction that is not in the slip plane" | +| 5 | *Execute* | Zero the Feature-0 sentinel row | "Class 1 analytical oracle, auto slip system" | +| 6 | *Execute* | Normalize the loading direction | "Class 1 analytical oracle, auto slip system" / loading `[3,6,9]` | +| 7 | *Execute* | Normalize the user slip plane and direction (`OverrideSystem` true) | "override slip system path" | +| 8 | *Execute - per-Feature* | Negative phase id guard (`-13502`) | "options, skip path and phase guards" / "a negative phase id is an error" | +| 9 | *Execute - per-Feature* | Out-of-range phase id guard (`-13501`) | "options, skip path and phase guards" / "a phase id beyond the ensemble count is an error" | +| 10 | *Execute - per-Feature* | Skip a Feature whose Laue class is at or beyond `LaueGroupEnd` | "options, skip path and phase guards" / "features whose Laue class is beyond LaueGroupEnd get defined zeros" | +| 11 | *Execute - per-Feature* | Auto slip-system path (`getSchmidFactorAndSS(load, ...)`) | "Class 1 analytical oracle, auto slip system"; "orientation-matrix convention and equivariance" | +| 12 | *Execute - per-Feature* | `m_ShouldCancel` early return | *Not directly tested. No cancel-signal injection infrastructure exists for algorithm classes; same gap as the rest of this plugin.* | + +## Test inventory + +| Test case | Status | Notes | +|---|---|---| +| `"OrientationAnalysis::ComputeSchmidsFilter"` | kept, modified | Consumes `6_6_stats_test_v2.tar.gz`. The two integer arrays (`SlipSystems`, `Poles`) still assert exact equality with the archive — both are provably invariant under the SC-2 fix. The three float arrays now assert the archive equals the freshly computed value times its predicted per-component bias factor, because the archive predates EbsdLib 3.1.1. 75 assertions. See *Exemplar archive*. | +| `"OrientationAnalysis::ComputeSchmidsFilter: Class 1 analytical oracle, auto slip system"` | new-for-V&V | 5 `DYNAMIC_SECTION` loadings x (5 outputs + 6 Class 4 invariants + the Feature-0 sentinel row). 221 assertions. | +| `"OrientationAnalysis::ComputeSchmidsFilter: orientation-matrix convention and equivariance"` | new-for-V&V | 4 `DYNAMIC_SECTION` quaternions pinning the `qu2om` convention, the no-transpose property, negative `Poles` truncation and cubic-symmetry invariance. 105 assertions. | +| `"OrientationAnalysis::ComputeSchmidsFilter: override slip system path"` | new-for-V&V | 3 `DYNAMIC_SECTION` loadings on the second EbsdLib overload, including the degenerate zero-Schmid case, plus the radians-versus-cosines assertion (SC-3) and the `-13500` preflight rejection. 84 assertions. | +| `"OrientationAnalysis::ComputeSchmidsFilter: options, skip path and phase guards"` | new-for-V&V | `StoreAngleComponents == false`; the `LaueGroupEnd` skip row asserting zeros over a 20 000-Feature dirty-heap fixture (SC-1); the `-13501` and `-13502` guards (SC-4). 83 assertions. | +| `"OrientationAnalysis::ComputeSchmidsFilter: SIMPL Backwards Compatibility"` | kept, untouched | 2 `DYNAMIC_SECTION` conversion fixtures (SIMPL 6.5 UUID, SIMPL 6.4 Filter_Name). 33 assertions. | + +**601 assertions total** (75 + 221 + 105 + 84 + 83 + 33), taken from a `ctest --verbose` run rather than tallied by hand. All 6 pass at the verified commit in the in-core build. The out-of-core build run is **waived** (requester decision, 2026-08-18). + +Full `OrientationAnalysis::` regression: 269 of 269 unit tests pass. 28 `PIPELINE::` / `PY::` tests fail in this build directory for an environment reason, not a code reason — the `NX-Com-Qt69-Vtk96-Rel-EbsdLib` directory was built with only the `SimplnxCore` and `OrientationAnalysis` plugin targets and no Python bindings (2 of 6 plugins, no `simplnx.cpython-312-darwin.so`), so the first pipeline in each chain fails with `ModuleNotFoundError: No module named 'simplnx'` and every downstream pipeline then fails with `Input file does not exist`. + +## Mutation verification + +Six mutations, each applied to the fixed tree, rebuilt, run, and reverted; the transcript is `ww_work/ComputeSchmids/mutation_transcript.txt` and the driver is `mutate.sh`. Both repositories were proven byte-identical to the pre-mutation snapshot afterwards, and the baseline re-ran green. The tree the matrix ran against is byte-identical to the committed tree except for the single `vcpkg.json` manifest line bumping `ebsdlib` from `3.1.0` to `3.1.1`, which was applied last (see *Merge dependency*) and which affects only dependency resolution, no compiled code. + +The `Killed by` column gives `ctest` ordinals as they stood in `NX-Com-Qt69-Vtk96-Rel-EbsdLib` at the time of the run: 163 = the archive-consuming test, 164 = Class 1 auto, 165 = orientation-matrix convention, 166 = override path, 167 = options/guards, 168 = SIMPL BC. Ordinals are not stable — later building `EbsdLibUnitTest` into the same directory shifted them to 570-575. The test **names** are the stable identifiers. + +| # | Mutation | Killed by | Verdict | +|---|---|---|---| +| M1 | Transpose the orientation matrix before rotating the loading direction | tests 163, 165 | killed | +| M2 | Swap quaternion components x and y on read | tests 163, 165 | killed | +| M3 | Round the `Poles` scaling instead of truncating | tests 163, 164, 165, 166, 167 | killed | +| M4 | Slip-system tie-break `>` becomes `>=` (EbsdLib `CubicOps`, all 11 comparisons) | test 164 | killed | +| M5 | Revert the SC-1 explicit zero fill | *nothing* | **SURVIVOR** — behaviour-preserving in-core; see below | +| M6 | Revert both SC-4 phase-id guards | test 167 | killed | + +M2 initially survived: the first three fixture quaternions all have `x == y`, so swapping them is a no-op on every one of them, and only the archive-consuming test caught it. A fourth quaternion, `(1,0,0,0)`, was added specifically to close that gap — swapping its x and y turns a 180 deg rotation about X into one about Y, moving `SlipSystems` from 1 to 4 and `Poles` from `(26,-53,-80)` to `(-26,53,-80)`. M2 is killed by the hand fixtures after that addition. + +**M5 survives, and the reason is now established rather than presumed.** The original write-up said only that no deterministic RED was obtainable on this platform, having tried two things that both produced nothing: poisoning the heap across six malloc size classes, and re-running under `MallocNanoZone=0 MallocPreScribble=1 MallocScribble=1`. Two null results with no mechanism is not a finding, so the decisive variant was run. + +**Experiment.** The `LaueGroupEnd` skip fixture was enlarged from 3 Features to **20 000**, so every output array is 78 KB or more rather than 8-36 bytes, with same-size blocks poisoned to `0xAB` and freed immediately before the run; the explicit `"0"` fill was then removed from all five `CreateArrayAction` calls and the case re-run. **Result: still all zeros — 0 non-zero values across all five arrays and all 19 999 uncomputed rows.** A standalone probe over the same size ladder confirms the allocator is not the reason: `new float[n]` after a same-size dirty free returns `0xABABABAB` for every `n` at or above 4096 elements (16 KB) and zeros below it, which both explains why the earlier 8-36-byte attempts saw nothing and rules out "libmalloc was kind" as the explanation at 20 000. + +**Mechanism.** The zeros are written by SIMPLNX. `CoreDataIOManager::addDataStoreFnc()` constructs every in-core store as `std::make_unique(tupleShape, componentShape, 0.0f)` — a hard-coded zero `initValue`, independent of the action's `fillValue` string — and `DataStore`'s constructor unconditionally `std::fill_n`s the buffer with it (`DataStore.hpp:66-69`). `CreateArrayAction`'s `fillValue` only adds a second, redundant `store->fill()`. So SC-1's premise, that an empty `fillValue` leaves the buffer default-initialized, is **false for the in-core store**, and M5 cannot be killed because there is nothing to kill: the mutation is behaviour-preserving in-core. + +**What that changes.** SC-1 is reclassified from an active defect to a **latent** one: the dropped `initValue` is a genuine source-level port regression, but no in-core release produced indeterminate output because of it. The explicit fill is kept as a defensive, self-documenting statement of the filter's requirement and as the only guarantee for a store implementation whose factory does not hard-code a zero. The enlarged fixture is kept because it makes the assertion non-vacuous: at 20 000 Features it would catch a store that stopped filling, which the 3-Feature version provably could not. *Evidence class: executed (the negative result and the probe), plus source for the mechanism.* + +The SC-2 mutation needs no separate entry — the entire pre-fix baseline run is it. Against EbsdLib 3.1.0 the new fixtures failed with exactly the predicted pre-fix values (`m = 0.4083219171` against an expected `0.4082482905`, `Phis = 0.5773671865` against `0.5773502692`, `Lambdas = 0.7072135806` against `0.7071067812`), recorded in `ww_work/ComputeSchmids/red_transcript.txt`. + +## Exemplar archive + +`6_6_stats_test_v2.tar.gz`, SHA512 `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723`, matching `src/Plugins/OrientationAnalysis/test/CMakeLists.txt:130`. **Neither the archive nor its `download_test_data()` line was changed.** Provenance sidecar: `vv/provenance/6_6_stats_test_v2.md`. + +The archive's `Schmids`, `Schmid_Phis` and `Schmid_Lambdas` arrays were generated with EbsdLib <= 3.1.0 and are therefore stale by a known amount. Rather than retire the comparison — which would discard 214 real regression checks — or loosen it into meaninglessness, the test now asserts the **relationship** between archive and fresh output: + +| Array | archived / freshly computed | Source of the factor | +|---|---|---| +| `Schmid_Phis` | 1.0000293384785181 | `sqrt(3) / 1.732f` | +| `Schmid_Lambdas` | 1.0001510099261918 | `sqrt(2) / 1.414f` | +| `Schmids` | 1.0001803528351113 | `sqrt(6) / (1.732f * 1.414f)` | + +That is strictly stronger than the equality it replaces: it pins both the new values and the exact size and shape of the change, and it fails if the fix perturbed anything the bias analysis did not account for. The predicted `Schmid_Lambdas` factor `1.00015100993` was met by the observed `1.00015102083` — agreement to `1.1e-8` relative, across the whole Small IN100 Feature set. Regenerating the archive against EbsdLib >= 3.1.1 would set all three factors to 1.0; that is deliberately left for a future cycle so this PR does not also churn shared test data. + +## Deviations from DREAM3D 6.5.171 + +Run 2026-08-20. Legacy binary `~/Applications/DREAM3D.app/Contents/Bin/PipelineRunner` (6.5.171); NX binary `DREAM3D-Build/NX-Com-Qt69-Vtk96-Rel-EbsdLib/Bin/nxrunner`, built against the fixed local EbsdLib. One shared hand-built legacy-format input carrying six Features — a sentinel, the four oracle quaternions, and one Feature whose phase has no slip systems — driven through 10 pipeline pairs (5 auto-path loadings, one `StoreAngleComponents = false`, 3 override-path loadings, 1 negative case). Predictions were written to `ww_work/ComputeSchmids/predictions.txt` **before** `compare.py` was run. + +Result: **93 predicted relationships confirmed, 0 unexplained differences, 1 prediction amended.** + +`SlipSystems` and `Poles` were bit-identical on all five auto-path fixtures. Every auto-path float matched its predicted bias factor to better than `1e-7` relative. On the override path, where neither implementation uses the truncated literals, the floats agreed to `1.2e-7` relative with no systematic bias — the control that proves the D1 divergence is the normalizer and nothing else. + +**No 6.5.172 surgical patch was produced for D1, deliberately.** Legacy's Schmid arithmetic lives in `OrientationLib/LaueOps/CubicOps.cpp`, not in the filter, so patching the filter could not bring the two into alignment and patching `OrientationLib` would be a library change rather than the "smallest possible diff, one filter per patch" the protocol asks for. The alignment proof here is instead that **fixed-EbsdLib NX reproduces the exact-arithmetic oracle** to `1e-6` on all 12 value-asserting Class 1 fixtures, which the unit tests establish directly. + +- `ComputeSchmidsFilter-D1` — Schmid factor and angle components inflated by a uniform +0.018% in 6.5.171 (and in NX before EbsdLib 3.1.1). Trust SIMPLNX. +- `ComputeSchmidsFilter-D2` — skipped Features: `Phis`/`Lambdas` read `-301` in 6.5.171 and `0` in NX; pre-fix NX read `0` by way of the in-core store factory's hard-coded zero rather than the filter's own intent (latent port regression — see the D2 entry). Trust SIMPLNX. +- `ComputeSchmidsFilter-D3` — EbsdLib Laue ops left `angleComps` and, for `HexagonalLowOps`, `schmidfactor`/`slipsys` undefined. Fixed in EbsdLib 3.1.1-staging plus an NX-side mitigation. D3 also records **twelve known-open SC-2-class divisors** in the hexagonal plane-normal geometry (`0.8164` for `2/sqrt(6)`, `1.154` for `2/sqrt(3)`), deferred because they can move the argmax and so need a hexagonal oracle of their own, and the new hexagonal `slipsys = 0` sentinel. +- `ComputeSchmidsFilter-D4` — `Phis`/`Lambdas` change units with `OverrideSystem` (cosines versus radians), and the default array names differ between versions. Documented, not changed. +- `ComputeSchmidsFilter-D5` — on the override path `SlipSystems` is a table-relative symmetry-operator index; the two libraries order the same 24-element group differently and six operators tie at the maximum, so 6.5.171 reports 8 where NX reports 3 for an identical physical answer. Either acceptable; documented. + +## Documentation changes + +`docs/ComputeSchmidsFilter.md` gained: the loading-direction normalization and sample-frame semantics; the Feature-0 and skipped-Feature zero behaviour; the tie-break rule and that it is enumeration-order rather than physics; the two incompatible `SlipSystems` numbering schemes and the table-relative, tie-arbitrary nature of the override index (D5); the `Phis`/`Lambdas` unit flip (D4); and a correction of the claim implicit in the name `Poles` — it is a fixed-point encoding of the crystal-frame unit loading direction, truncated toward zero, not a Miller index. The *Example Pipelines* citation was corrected from `(05) SmallIN100 Crystallographic Statistics` to the actual file name, `(04) Small IN100 Crystallographic Statistics`. The `SlipSystems` section additionally now states the cubic-0 versus hexagonal-1 numbering base and the two situations in which a reported `0` means "no slip system found" rather than slip system number 0, including the new hexagonal case introduced by the D3 fix. + +**Title convention.** The filter's `humanName()` is **"Compute Schmid Factors"**, and the user-facing doc is titled that way. This report, the deviations file and the commit title use the short form **"Compute Schmids"**, matching the class name `ComputeSchmidsFilter` and the short-form convention used across this V&V batch. Both names refer to the same filter, UUID `b4681855-0a3d-4237-97f2-5aec509115c4`. diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeSchmidsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeSchmidsFilter.md new file mode 100644 index 0000000000..c4de2dd212 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeSchmidsFilter.md @@ -0,0 +1,159 @@ +# Deviations from DREAM3D 6.5.171: ComputeSchmidsFilter + +This file lists every documented behavioral difference between this SIMPLNX filter and its DREAM3D 6.5.171 equivalent. + +Entries are referenced by stable ID (`ComputeSchmidsFilter-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. + +--- + +## ComputeSchmidsFilter-D1 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeSchmidsFilter-D1` | +| **Filter UUID** | `b4681855-0a3d-4237-97f2-5aec509115c4` | +| **Status** | active | + +**Symptom:** Every Schmid factor and both angle components produced by DREAM3D 6.5.171 on the default (auto) slip-system path are systematically too large by a fixed factor. The inflation is uniform, so it is invisible in any relative comparison and only shows up against an absolute reference — or against the physical bound. Concretely, for the maximizing loading direction of a cubic crystal 6.5.171 reports a Schmid factor of **0.500090176**, which is impossible: the Schmid factor cannot exceed 0.5. The reported `cos phi` and `cos lambda` can likewise exceed 1.0. + +The three inflation factors, per output array: + +| Array | 6.5.171 value / correct value | Exact expression | +|---|---|---| +| `SchmidPhis` (`Schmid_Phis`) | 1.0000293384785181 (+0.0029338 %) | `sqrt(3) / 1.732f` | +| `SchmidLambdas` (`Schmid_Lambdas`) | 1.0001510099261918 (+0.0151010 %) | `sqrt(2) / 1.414f` | +| `Schmids` | 1.0001803528351113 (+0.0180353 %) | `sqrt(6) / (1.732f * 1.414f)` | + +Measured against 6.5.171 on hand-built fixtures, the observed ratios matched these predictions to better than `1.1e-8` relative. + +`SlipSystems` and `Poles` are **not** affected. The inflation is a single positive scale factor applied to all twelve candidate slip systems, so the argmax cannot move; and `Poles` is a function of the crystal-frame loading direction, which the normalizers never touch. Both were bit-identical between 6.5.171 and SIMPLNX on every auto-path fixture. + +**Root cause:** Library. `OrientationLib/LaueOps/CubicOps.cpp:850-868` in 6.5.171 normalizes the `{111}` plane-normal dot products by the literal `1.732f` and the `<110>` slip-direction dot products by `1.414f`, instead of by `sqrt(3)` and `sqrt(2)`. Both literals are four-significant-digit truncations and both are smaller than the constants they stand in for, so every direction cosine comes out too large. EbsdLib carried the identical defect through version 3.1.0, which means **SIMPLNX built against EbsdLib <= 3.1.0 reproduces this deviation exactly** — this is a shared library defect, not a 6.5.171-only one. + +Fixed in EbsdLib on `topic/3_1_1_staging` (commit `4a56725`, `BUG: Use exact sqrt(3)/sqrt(2) normalizers in CubicOps::getSchmidFactorAndSS`) by replacing both literals with the full-precision `ebsdlib::constants::k_Sqrt3D` and `k_Sqrt2D`. The `double` type matches the surrounding arithmetic, which is entirely `double` — those two `float` literals were the only precision loss in the function. + +Note that the *override* slip-system path (`Override Default Slip System` = on) was never affected in either implementation: it normalizes the user-supplied plane and direction with `std::sqrt` and never used the truncated literals. The A/B run confirms this — override-path floats agree between the two builds to `1.2e-7` relative with no systematic bias, which is the control that isolates the normalizer as the sole cause of the auto-path divergence. + +**Affected users:** Everyone who has run Compute Schmid Factors, or legacy Find Schmid Factors, with `Override Default Slip System` off — that is, essentially every user of this filter. Absolute Schmid-factor values are wrong in the fourth decimal place. Relative comparisons *between* Features, rankings, histograms, and the reported slip system are all unaffected, so most downstream analyses reach the same conclusion. + +**Recommendation:** **Trust SIMPLNX**, built against EbsdLib >= 3.1.1. The corrected values match an exact-arithmetic oracle to `1e-6` on all twelve value-asserting Class 1 fixtures and satisfy the physical bound `0 <= m <= 0.5`, which the pre-fix values provably violate. + +No surgical patch to the legacy source was produced for this deviation, deliberately. The defective arithmetic lives in `OrientationLib`, not in `FindSchmids`, so a filter-level patch could not bring the two into alignment and a library-level patch would exceed the "smallest possible diff, one filter per patch" scope of the alignment-validation protocol. The alignment proof used instead is direct: fixed-EbsdLib SIMPLNX reproduces the exact-arithmetic oracle. + +--- + +## ComputeSchmidsFilter-D2 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeSchmidsFilter-D2` | +| **Filter UUID** | `b4681855-0a3d-4237-97f2-5aec509115c4` | +| **Status** | active | + +**Symptom:** For Features the filter does not compute — Feature 0, which is the conventional unassigned-Feature sentinel, and any Feature whose phase maps to a crystal structure with no enumerated slip systems — the two angle-component arrays read **`-301.0`** in 6.5.171 and **`0.0`** in SIMPLNX. `Schmids`, `SlipSystems` and `Poles` read `0` in both. + +**Root cause:** Bug in 6.5.171. `FindSchmids.cpp:234` and `:242` create the `SchmidPhis` and `SchmidLambdas` arrays with `createNonPrereqArrayFromPath, AbstractFilter, float>(this, tempPath, -301, cDims)`. The third argument of that template is the array's `initValue`, not an error code; `-301` is an error code that was pasted into the wrong parameter slot. The same call for `Schmids`, `SlipSystems` and `Poles` correctly passes `0`. Because the execute loop writes the angle components only inside `if(xtal < LaueGroupEnd)`, uncomputed rows keep the `-301` sentinel. + +SIMPLNX created all five arrays with `CreateArrayAction`'s default empty fill value, where 6.5.171 passed `initValue 0` for three of them. Preflight now passes an explicit `"0"` fill for all five arrays, and the algorithm additionally writes the Feature-0 row explicitly. + +**How much exposure that actually carried, measured rather than assumed.** The obvious reading — that without a fill the uncomputed rows hold indeterminate memory — does **not** hold for the in-core store. `CoreDataIOManager::addDataStoreFnc()` constructs every in-core `DataStore` with `static_cast(0)` as its `initValue` irrespective of the action's `fillValue` string, and `DataStore`'s constructor unconditionally `std::fill_n`s the buffer with it (`DataStore.hpp:66-69`). Removing the explicit fill and re-running a 20 000-Feature fixture with same-size heap blocks poisoned to `0xAB` and freed immediately before the run still yields all zeros; a standalone probe confirms that on the same platform a 20 000-element `new float[]` after a same-size dirty free *does* return the poison, so the zeros are written by SIMPLNX and not inherited from the allocator. The explicit fill is therefore **defensive on a reachable path, not merely hypothetical**: `DataIOCollection::checkStoreDataFormat()` silently rewrites an empty `dataFormat` to the preferred large-data format whenever the array exceeds the large-data threshold and such a store factory is registered (e.g. the external `SimplnxOoc` plugin), and nothing guarantees that factory hard-codes a zero `initValue`. The fill restores what legacy stated explicitly and is the only in-repo guarantee on that path, which this cycle did not verify (logged open). The SIMPLNX side of this is a **latent port regression** — the `initValue` was genuinely dropped, but no in-core release ever produced indeterminate output because of it. + +**Affected users:** Anyone whose data contains a phase whose crystal structure has no slip systems enumerated (anything other than Cubic-High, Cubic-Low, Hexagonal-High or Hexagonal-Low, or an unindexed/unknown phase), and anyone reading Feature 0. Under 6.5.171 those rows are a recognisable but undocumented `-301` sentinel. Under SIMPLNX they read `0` — before this cycle by way of the store factory's hard-coded zero rather than by the filter's own intent, and now by both. + +**Recommendation:** **Trust SIMPLNX.** Zero is the meaningful "not computed" value here and is consistent with what the other three output arrays already do for the same rows. Filter uncomputed Features by phase or by `Schmids == 0`, not by looking for `-301`. + +--- + +## ComputeSchmidsFilter-D3 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeSchmidsFilter-D3` | +| **Filter UUID** | `b4681855-0a3d-4237-97f2-5aec509115c4` | +| **Status** | active | + +**Symptom:** Two classes of undefined output from the orientation library's Schmid-factor routines, present in both 6.5.171's `OrientationLib` and EbsdLib through 3.1.0. + +1. `HexagonalLowOps::getSchmidFactorAndSS(load, schmidfactor, angleComps, slipsys)` **read** `schmidfactor` before writing it. The function declares its locals and then makes `if(schmid1 > schmidfactor)` the first statement that touches `schmidfactor`, so the entire comparison chain was driven by an indeterminate value. `slipsys` was likewise never initialized. Depending on the garbage, a Hexagonal-Low Feature could get a Schmid factor of zero with an arbitrary slip-system index, or could accept a candidate against a garbage incumbent. + +2. `HexagonalOps` seeded `schmidfactor` but not `slipsys` or `angleComps`; and the seven Laue classes that enumerate no slip systems (`TrigonalOps`, `TrigonalLowOps`, `TetragonalOps`, `TetragonalLowOps`, `OrthoRhombicOps`, `MonoclinicOps`, `TriclinicOps`) set `schmidfactor` and `slipsys` but left `angleComps` **untouched**. + +Case 2 is the more damaging in practice because it is silent and produces plausible-looking output. A caller that hoists one `angleComps[2]` buffer outside its per-Feature loop — the natural way to write the loop, and what both `FindSchmids` and `ComputeSchmids` did — gets the **previous Feature's** angle components attributed to the current Feature whenever the current Feature's Laue class is one of the seven stubs. The Schmid factor correctly reads 0 while the two angle columns read as real measurements carried over from an unrelated Feature. + +**Root cause:** Bug, in the library. Fixed in EbsdLib on `topic/3_1_1_staging` (commit `2c84f2a`, `BUG: Define every getSchmidFactorAndSS output on all paths`) by seeding all four outputs at the top of every affected overload. That changes no value on any path that already computed a result; it only replaces indeterminate or stale output with a defined zero. `CubicOps` and `CubicLowOps` already wrote all four unconditionally and were left alone, as were all eleven plane/direction overloads, which already zeroed their outputs on entry. + +Independently of the library fix, SIMPLNX now declares `schmid`, `angleComps` and `slipSystem` **inside** the per-Feature loop, so the stale-carryover half of this defect cannot occur regardless of which EbsdLib the filter is linked against. + +One consequence of the fix, and three observations deliberately **not** changed, are recorded here so the decisions are on the record. + +**A new `slipsys = 0` "no candidate" sentinel for the hexagonal classes.** `HexagonalOps` and `HexagonalLowOps` assign `slipsys` values **1 through 6** when a candidate wins the strict `>`. Seeding `slipsys = 0` therefore introduces a value *outside* that documented range, and it is now the reportable signal that no candidate ever beat the seeded `schmidfactor = 0.0` — the hexagonal analogue of the override path's `slipsys = 0`. Callers reading a hexagonal `SlipSystems` value must treat `0` as "no slip system found", not as a slip-system number. Before the fix the same situation produced whatever the caller had passed in. Recorded in `docs/ComputeSchmidsFilter.md`. + +**KNOWN-OPEN: twelve further SC-2 instances in the hexagonal plane-normal geometry.** Six of the hexagonal slip-plane normals compute their `z` component by dividing by a truncated **sqrt-derived normalizer held as a bare decimal literal**. Structurally these are the *same* defect as D1 — a four-significant-digit stand-in for an irrational constant inside otherwise all-`double` arithmetic — not merely an approximate direction cosine: + +| Literal | Constant it stands for | Relative error | Sites (EbsdLib `topic/3_1_1_staging`) | +|---|---|---|---| +| `0.8164` | `2/sqrt(6)` = 0.81649658 | −1.18e-4 | `HexagonalOps.cpp:758, 765, 772, 779`; `HexagonalLowOps.cpp:671, 678, 685, 692` | +| `1.154` | `2/sqrt(3)` = 1.15470054 | −6.07e-4 | `HexagonalOps.cpp:786, 793`; `HexagonalLowOps.cpp:699, 706` | + +The `1.154` bias is **3.4x** the `sqrt(6)/(1.732f * 1.414f)` cubic bias that D1 was raised for. Status is **known-open, deferred**, not benign. The reason for deferring is specific: unlike D1's uniform positive scale factor, these literals perturb the *direction* of a plane normal — `lnz` is one component of a vector that is renormalized by its own computed norm a few lines later, so the error tilts the normal instead of cancelling, and fixing it can move the argmax. A fix therefore changes both the reported hexagonal Schmid factor **and** the reported slip system, and needs its own hexagonal Class 1 oracle first — exactly the work that preceded the cubic fix. Follow-up work, with the twelve line numbers above. + +**Genuinely benign truncated literals, kept — and why they are different.** The remaining hexagonal literals (`0.707`, `0.57735`, the `0.4472`/`0.8944` pair, and the `0.4082`/`0.8164` pair in the direction-cosine triples at `HexagonalOps.cpp:664-673` / `HexagonalLowOps.cpp:577-586` — distinct occurrences from the twelve divisor sites above, where `0.8164` also appears) appear only as *proportional triples* that are divided by their own computed norm a few lines later, and the truncations preserve the component **ratios** exactly: the `0.707` and `0.57735` triples are equal-magnitude, `0.4472 / 0.8944 = 0.5` exactly, which is the exact `(1/sqrt(5)) / (2/sqrt(5))`, and `0.4082 / 0.8164 = 0.5` exactly, matching `(1/sqrt(6)) / (2/sqrt(6))`. A common relative error cancels identically in the renormalization, so these change no output bit. That is precisely the distinction from the twelve above, where the literal is a **divisor applied to one component only** and so survives renormalization. Two further items are noted but out of scope: `0.866025` for `sqrt(3)/2` in the hexagonal-to-Cartesian basis transform (relative error −4.6e-7, three orders of magnitude below the twelve), and `caratio` hard-coded to `1.633` — a modelling limitation rather than a precision one, since it ignores the actual `c/a` of the phase. + +**Slip-system numbering base.** `HexagonalOps` and `HexagonalLowOps` number their reported slip systems from **1**, while `CubicOps` numbers from **0**. Changing either would break existing user data. Documented, unfixed. + +**Affected users:** Anyone whose data contains a Hexagonal-Low phase (defect 1), and anyone whose data mixes a Cubic or Hexagonal phase with any of Triclinic, Monoclinic, Orthorhombic, Tetragonal-Low/High or Trigonal-Low/High while `Store Angle Components of Schmid Factor` is on (defect 2). + +**Recommendation:** **Trust SIMPLNX** built against EbsdLib >= 3.1.1. For SIMPLNX built against EbsdLib <= 3.1.0, the per-iteration reinitialization added in this cycle already removes the stale-carryover exposure; the `HexagonalLowOps` uninitialized read requires the library fix. + +--- + +## ComputeSchmidsFilter-D4 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeSchmidsFilter-D4` | +| **Filter UUID** | `b4681855-0a3d-4237-97f2-5aec509115c4` | +| **Status** | active | + +**Symptom:** Three interface-level differences that are stable across both implementations but easy to trip over. + +1. **The angle-component arrays change units with a boolean toggle.** With `Override Default Slip System` **off**, `Phis` and `Lambdas` hold the direction **cosines** `cos phi` and `cos lambda` — dimensionless, in `[0, 1]`. With it **on**, the same two arrays hold the **angles** `phi` and `lambda` themselves, in **radians**. The two code paths call different overloads: the auto path stores the raw dot products, the override path stores `std::acos` of them. Both parameter descriptions call these "the angle between ...", which is only true in the second case. Present identically in 6.5.171 and SIMPLNX. Consequence: with the toggle off the Schmid factor equals the product of the two stored values; with it on it equals the product of their cosines. + +2. **The default angle-array names differ between versions.** 6.5.171 defaults to `SchmidPhis` and `SchmidLambdas`; SIMPLNX defaults to `Schmid_Phis` and `Schmid_Lambdas`. Pipelines converted from SIMPL carry their explicit names across correctly, so this bites only when comparing outputs or hand-writing a downstream path. + +3. **The preflight rejection code differs.** A slip direction that does not lie in the slip plane is rejected at preflight by both, with the same message text (`"Slip Plane and Slip Direction must be normal"`) but different codes: `-1001` in 6.5.171, `-13500` in SIMPLNX. Verified on a dedicated negative fixture. + +**Root cause:** Algorithmic choice / order of operations, for (1); naming and error-code renumbering during the port for (2) and (3). + +**Affected users:** For (1), anyone who toggles `Override Default Slip System` and consumes `Phis`/`Lambdas` — the values silently change units and any downstream arithmetic that assumed cosines is off by a `cos`. For (2) and (3), anyone diffing outputs across versions or matching on error codes. + +**Recommendation:** **Either acceptable within tolerance** — the behaviour is the same in both versions and nothing is numerically wrong. The unit flip is documented in `docs/ComputeSchmidsFilter.md` with an explicit table, and unit tests assert both relationships (`m == Phis * Lambdas` on the auto path, `m == cos(Phis) * cos(Lambdas)` on the override path) so the distinction cannot silently regress. Convert explicitly rather than assuming a unit. + +--- + +## ComputeSchmidsFilter-D5 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeSchmidsFilter-D5` | +| **Filter UUID** | `b4681855-0a3d-4237-97f2-5aec509115c4` | +| **Status** | active | + +**Symptom:** With `Override Default Slip System` **on**, the `SlipSystems` output disagrees between the two versions for the same input and the same physical answer. On the `(001)[100]` system under `[1,2,3]` loading, 6.5.171 reports **8** and SIMPLNX reports **3**, while the Schmid factor (`3/7` exactly), `Phis`, `Lambdas` and `Poles` all agree. + +This deviation was **not predicted** before the comparison ran. The pre-run prediction asserted `SlipSystems` would be identical on every fixture, reasoning that a uniform positive scale factor cannot move an argmax. That reasoning is correct and does hold on all five auto-path fixtures — the divergence has a different cause entirely, found by adjudicating the unexpected difference rather than by explaining it away. + +**Root cause:** Algorithmic choice, specifically an indexing convention. On the override path the reported value is not a slip-system number at all: it is the index of the **symmetry operator**, within that library's 24-element cubic symmetry table, that maps the user-supplied plane and direction onto the maximizing variant. Two things follow. + +- The two tables hold the same group in a **different order**. `OrientationLib`'s `CubicMatSym` and EbsdLib's `CubicHigh::k_MatSym` differ at **18 of their 24 positions**, but as *sets* they are identical: every one of EbsdLib's 24 matrices occurs exactly once in `OrientationLib`'s table, so there is a well-defined reordering permutation between them. +- **Six operators tie at the maximum, and they are the same six group elements in both libraries.** EbsdLib's tie set `{3, 6, 8, 9, 11, 14}` maps element-by-element through that permutation exactly onto `OrientationLib`'s `{8, 9, 10, 12, 14, 16}` — the pairing is `3->9`, `6->8`, `8->16`, `9->12`, `11->10`, `14->14`. Both implementations are therefore choosing among the *same* six maximizing operators, and each reports whichever member its own enumeration order reaches first under the strict `>`: EbsdLib reaches its index 3 first, `OrientationLib` reaches its index 8 first, and `OrientationLib`'s 8 *is* EbsdLib's 6. That is the whole mechanism — one tie set, two enumeration orders. Verified directly against both source tables in exact rational arithmetic. + +The two *reported* matrices happen also to be transposes of one another, but that is a coincidence of which member each order reaches first and carries no weight here: transposition does not preserve the Schmid factor when the slip plane and the slip direction differ, so it could not by itself explain the equal maximum. + +Neither answer is wrong. The value is a handle into a specific library's table, not a portable physical label, and it is arbitrary among equivalent maximizing operators. + +This is distinct from the auto path, where `SlipSystems` **is** a genuine slip-system index into the crystal structure's built-in list (0-11 for Cubic-High) and **was** bit-identical between the two versions on every fixture. + +**Affected users:** Anyone who uses `Override Default Slip System` and records, filters on, or compares the `SlipSystems` value — particularly anyone migrating stored results or scripts from DREAM3D 6.5.x. + +**Recommendation:** **Either acceptable** — both values identify a correct maximizing symmetry operator. Where a choice is needed, **trust SIMPLNX**, because the SIMPLNX index is the one addressable through EbsdLib's own `getMatSymOp()` accessor, so a user can retrieve the actual matrix the index refers to. Do not treat this value as portable across versions, and do not treat it as uniquely determined even within one version. `docs/ComputeSchmidsFilter.md` now states both caveats, and the unit test that asserts index 3 carries a comment recording that it pins EbsdLib's table order rather than a physical label. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/6_6_stats_test_v2.md b/src/Plugins/OrientationAnalysis/vv/provenance/6_6_stats_test_v2.md new file mode 100644 index 0000000000..ce481cbe45 --- /dev/null +++ b/src/Plugins/OrientationAnalysis/vv/provenance/6_6_stats_test_v2.md @@ -0,0 +1,47 @@ +# Provenance: `6_6_stats_test_v2.tar.gz` + +| Field | Value | +|---|---| +| Archive | `6_6_stats_test_v2.tar.gz` | +| SHA512 | `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723` | +| Registered at | `src/Plugins/OrientationAnalysis/test/CMakeLists.txt:130` | +| Contents | `6_6_stats_test_v2.dream3d` (58.7 MB) + `6_6_stats_test_v2.xdmf` | +| Regenerated during this V&V cycle | **No** | + +## What it is + +A single `.dream3d` file holding the Small IN100 dataset carried through the reconstruction and statistics pipelines, with the Feature-level statistics arrays of many filters stored alongside their inputs. Consumers use it in the "self-exemplar" pattern: the test reads the file, runs one filter into `Calculated_` arrays inside the same `Cell Feature Data` attribute matrix, and compares against the pre-stored `` arrays. + +For `ComputeSchmidsFilter` the relevant pre-stored arrays are `Schmids`, `SlipSystems`, `Poles`, `Schmid_Phis` and `Schmid_Lambdas` under `DataContainer/Cell Feature Data`. + +## Who generated it, when, how + +Generated by the BlueQuartz team before this V&V programme began, by running the `Small_IN100_Processing` prebuilt pipeline series in DREAM3D-NX and writing the accumulated `DataStructure` out with `WriteDREAM3DFilter`. Uploaded to the [simplnx Data_Archive release](https://github.com/BlueQuartzSoftware/simplnx/releases/tag/Data_Archive). No per-array generation log survives. + +## What oracle was canonical when it was made + +**None.** This archive is a **circular oracle** with respect to every filter that consumes it: the stored arrays are that filter's own output from an earlier build of the same code, not an independently established expected value. It was never validated against an analytical, reference-implementation or paper-based oracle. + +That is the reason the V&V cycles that touch this archive do not treat it as the oracle. It is retained as a **regression** instrument — it answers "did this change alter the output on production-scale real data?", which is a genuinely useful question, and it exercises array shapes and Feature counts no hand-built fixture reaches. + +## Known staleness + +**The three float Schmid arrays in this archive are stale as of EbsdLib 3.1.1.** + +`Schmids`, `Schmid_Phis` and `Schmid_Lambdas` were produced by a build linked against EbsdLib <= 3.1.0, whose `CubicOps::getSchmidFactorAndSS` normalized with the `float` literals `1.732f` and `1.414f` instead of `sqrt(3)` and `sqrt(2)` (see `vv/deviations/ComputeSchmidsFilter.md`, `ComputeSchmidsFilter-D1`). Every archived value in those three arrays is therefore inflated by a known, uniform, per-array factor: + +| Array | archived / correct | Exact expression | +|---|---|---| +| `Schmid_Phis` | 1.0000293384785181 | `sqrt(3) / 1.732f` | +| `Schmid_Lambdas` | 1.0001510099261918 | `sqrt(2) / 1.414f` | +| `Schmids` | 1.0001803528351113 | `sqrt(6) / (1.732f * 1.414f)` | + +`SlipSystems` and `Poles` are **not** stale. The inflation is a single positive scale factor over all twelve candidate slip systems, so the argmax cannot move; and `Poles` is a function of the crystal-frame loading direction, which the normalizers never touch. + +`ComputeSchmidsTest.cpp` handles this by asserting the *relationship* rather than equality: the two integer arrays must still match the archive exactly, and each float array must equal the freshly computed value times the factor above, to `1e-6` relative. The predicted `Schmid_Lambdas` factor was met by the observed ratio to `1.1e-8` relative across the whole Feature set, which is a bit-level confirmation of the D1 analysis on production-scale data. + +**The archive was deliberately not regenerated in this cycle.** It is shared by several filters' tests, and reproducing it requires re-running the full `Small_IN100_Processing` series; churning it inside a Schmid-factor PR would put unrelated filters' regression baselines at risk in a change nobody reviewing that PR is equipped to check. When it is next regenerated against EbsdLib >= 3.1.1, all three factors become `1.0` and the `ComputeSchmidsTest.cpp` block that carries them should collapse back to a plain `CompareDataArrays`; a comment in the test says so. + +## Consumers + +`ComputeSchmidsTest.cpp` is documented here because this V&V cycle covered it. Other tests in this plugin and in `SimplnxCore` also consume this archive; their `download_test_data()` line and SHA512 are the same one recorded above, and this cycle changed neither. diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index bcb0fc0334..ccee7ad6d0 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -8,7 +8,7 @@ { "kind": "git", "repository": "https://github.com/bluequartzsoftware/simplnx-registry", - "baseline": "8e5010ce1bac7cb2a2a8722944913e4a56884439", + "baseline": "223c58872a56dd25313b6461ddef410267f61873", "packages": [ "benchmark", "blosc", diff --git a/vcpkg.json b/vcpkg.json index 9477e819d7..d0348c335a 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -83,7 +83,7 @@ "dependencies": [ { "name": "ebsdlib", - "version>=": "3.1.0" + "version>=": "3.1.1" } ] }, From 0b8a24b0c955f2e0a7031f4f97902dfc863bb5b5 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 26 Aug 2026 12:19:40 -0400 Subject: [PATCH 2/3] Remove unnecessary V&V files --- .../pipelines/legacy_6_5_171.json | 233 ------------------ .../pipelines/make_input.py | 84 ------- .../pipelines/nx.d3dpipeline | 137 ---------- .../results/.gitignore | 3 - .../results/comparison.md | 65 ----- .../vv/provenance/6_6_stats_test_v2.md | 47 ---- 6 files changed, 569 deletions(-) delete mode 100644 src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/pipelines/legacy_6_5_171.json delete mode 100644 src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/pipelines/make_input.py delete mode 100644 src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/pipelines/nx.d3dpipeline delete mode 100644 src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/results/.gitignore delete mode 100644 src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/results/comparison.md delete mode 100644 src/Plugins/OrientationAnalysis/vv/provenance/6_6_stats_test_v2.md diff --git a/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/pipelines/legacy_6_5_171.json b/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/pipelines/legacy_6_5_171.json deleted file mode 100644 index b47c1dab5d..0000000000 --- a/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/pipelines/legacy_6_5_171.json +++ /dev/null @@ -1,233 +0,0 @@ -{ - "0": { - "FilterVersion": "6.5.171", - "Filter_Enabled": true, - "Filter_Human_Label": "Read DREAM.3D Data File", - "Filter_Name": "DataContainerReader", - "Filter_Uuid": "{043cbde5-3878-5718-958f-ae75714df0df}", - "InputFile": "/Users/mjackson/Workspace5/simplnx/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/results/caxis_ab_input.dream3d", - "OverwriteExistingDataContainers": false, - "InputFileDataContainerArrayProxy": { - "Data Containers": [ - { - "Attribute Matricies": [ - { - "Data Arrays": [ - {"Component Dimensions": [4], "Flag": 2, "Name": "Quats", "Object Type": "DataArray", "Path": "/DataContainers/TC1_Chain/CellData", "Tuple Dimensions": [1], "Version": 2}, - {"Component Dimensions": [1], "Flag": 2, "Name": "Phases", "Object Type": "DataArray", "Path": "/DataContainers/TC1_Chain/CellData", "Tuple Dimensions": [1], "Version": 2} - ], - "Flag": 2, - "Name": "CellData", - "Type": 3 - }, - { - "Data Arrays": [ - {"Component Dimensions": [1], "Flag": 2, "Name": "CrystalStructures", "Object Type": "DataArray", "Path": "/DataContainers/TC1_Chain/CellEnsembleData", "Tuple Dimensions": [1], "Version": 2} - ], - "Flag": 2, - "Name": "CellEnsembleData", - "Type": 11 - } - ], - "Flag": 2, - "Geometry": {"Geometry_Type": 0, "Geometry_Type_Name": "ImageGeometry"}, - "Name": "TC1_Chain" - }, - { - "Attribute Matricies": [ - { - "Data Arrays": [ - {"Component Dimensions": [4], "Flag": 2, "Name": "Quats", "Object Type": "DataArray", "Path": "/DataContainers/TC2_PiFold/CellData", "Tuple Dimensions": [1], "Version": 2}, - {"Component Dimensions": [1], "Flag": 2, "Name": "Phases", "Object Type": "DataArray", "Path": "/DataContainers/TC2_PiFold/CellData", "Tuple Dimensions": [1], "Version": 2} - ], - "Flag": 2, - "Name": "CellData", - "Type": 3 - }, - { - "Data Arrays": [ - {"Component Dimensions": [1], "Flag": 2, "Name": "CrystalStructures", "Object Type": "DataArray", "Path": "/DataContainers/TC2_PiFold/CellEnsembleData", "Tuple Dimensions": [1], "Version": 2} - ], - "Flag": 2, - "Name": "CellEnsembleData", - "Type": 11 - } - ], - "Flag": 2, - "Geometry": {"Geometry_Type": 0, "Geometry_Type_Name": "ImageGeometry"}, - "Name": "TC2_PiFold" - }, - { - "Attribute Matricies": [ - { - "Data Arrays": [ - {"Component Dimensions": [4], "Flag": 2, "Name": "Quats", "Object Type": "DataArray", "Path": "/DataContainers/TC3_Mask/CellData", "Tuple Dimensions": [1], "Version": 2}, - {"Component Dimensions": [1], "Flag": 2, "Name": "Phases", "Object Type": "DataArray", "Path": "/DataContainers/TC3_Mask/CellData", "Tuple Dimensions": [1], "Version": 2}, - {"Component Dimensions": [1], "Flag": 2, "Name": "Mask", "Object Type": "DataArray", "Path": "/DataContainers/TC3_Mask/CellData", "Tuple Dimensions": [1], "Version": 2} - ], - "Flag": 2, - "Name": "CellData", - "Type": 3 - }, - { - "Data Arrays": [ - {"Component Dimensions": [1], "Flag": 2, "Name": "CrystalStructures", "Object Type": "DataArray", "Path": "/DataContainers/TC3_Mask/CellEnsembleData", "Tuple Dimensions": [1], "Version": 2} - ], - "Flag": 2, - "Name": "CellEnsembleData", - "Type": 11 - } - ], - "Flag": 2, - "Geometry": {"Geometry_Type": 0, "Geometry_Type_Name": "ImageGeometry"}, - "Name": "TC3_Mask" - }, - { - "Attribute Matricies": [ - { - "Data Arrays": [ - {"Component Dimensions": [4], "Flag": 2, "Name": "Quats", "Object Type": "DataArray", "Path": "/DataContainers/TC4_Phase0/CellData", "Tuple Dimensions": [1], "Version": 2}, - {"Component Dimensions": [1], "Flag": 2, "Name": "Phases", "Object Type": "DataArray", "Path": "/DataContainers/TC4_Phase0/CellData", "Tuple Dimensions": [1], "Version": 2} - ], - "Flag": 2, - "Name": "CellData", - "Type": 3 - }, - { - "Data Arrays": [ - {"Component Dimensions": [1], "Flag": 2, "Name": "CrystalStructures", "Object Type": "DataArray", "Path": "/DataContainers/TC4_Phase0/CellEnsembleData", "Tuple Dimensions": [1], "Version": 2} - ], - "Flag": 2, - "Name": "CellEnsembleData", - "Type": 11 - } - ], - "Flag": 2, - "Geometry": {"Geometry_Type": 0, "Geometry_Type_Name": "ImageGeometry"}, - "Name": "TC4_Phase0" - }, - { - "Attribute Matricies": [ - { - "Data Arrays": [ - {"Component Dimensions": [4], "Flag": 2, "Name": "Quats", "Object Type": "DataArray", "Path": "/DataContainers/TC5_3D/CellData", "Tuple Dimensions": [1], "Version": 2}, - {"Component Dimensions": [1], "Flag": 2, "Name": "Phases", "Object Type": "DataArray", "Path": "/DataContainers/TC5_3D/CellData", "Tuple Dimensions": [1], "Version": 2}, - {"Component Dimensions": [1], "Flag": 2, "Name": "Mask", "Object Type": "DataArray", "Path": "/DataContainers/TC5_3D/CellData", "Tuple Dimensions": [1], "Version": 2} - ], - "Flag": 2, - "Name": "CellData", - "Type": 3 - }, - { - "Data Arrays": [ - {"Component Dimensions": [1], "Flag": 2, "Name": "CrystalStructures", "Object Type": "DataArray", "Path": "/DataContainers/TC5_3D/CellEnsembleData", "Tuple Dimensions": [1], "Version": 2} - ], - "Flag": 2, - "Name": "CellEnsembleData", - "Type": 11 - } - ], - "Flag": 2, - "Geometry": {"Geometry_Type": 0, "Geometry_Type_Name": "ImageGeometry"}, - "Name": "TC5_3D" - } - ], - "Version": 6 - } - }, - "1": { - "ActiveArrayName": "Active", - "CellFeatureAttributeMatrixName": "CellFeatureData", - "CellPhasesArrayPath": {"Data Container Name": "TC1_Chain", "Attribute Matrix Name": "CellData", "Data Array Name": "Phases"}, - "CrystalStructuresArrayPath": {"Data Container Name": "TC1_Chain", "Attribute Matrix Name": "CellEnsembleData", "Data Array Name": "CrystalStructures"}, - "FeatureIdsArrayName": "FeatureIds", - "FilterVersion": "6.5.171", - "Filter_Enabled": true, - "Filter_Human_Label": "Segment Features (C-Axis Misalignment)", - "Filter_Name": "CAxisSegmentFeatures", - "Filter_Uuid": "{bff6be19-1219-5876-8838-1574ad29d965}", - "GoodVoxelsArrayPath": {"Data Container Name": "TC1_Chain", "Attribute Matrix Name": "CellData", "Data Array Name": "Mask"}, - "MisorientationTolerance": 10.0, - "QuatsArrayPath": {"Data Container Name": "TC1_Chain", "Attribute Matrix Name": "CellData", "Data Array Name": "Quats"}, - "UseGoodVoxels": 0 - }, - "2": { - "ActiveArrayName": "Active", - "CellFeatureAttributeMatrixName": "CellFeatureData", - "CellPhasesArrayPath": {"Data Container Name": "TC2_PiFold", "Attribute Matrix Name": "CellData", "Data Array Name": "Phases"}, - "CrystalStructuresArrayPath": {"Data Container Name": "TC2_PiFold", "Attribute Matrix Name": "CellEnsembleData", "Data Array Name": "CrystalStructures"}, - "FeatureIdsArrayName": "FeatureIds", - "FilterVersion": "6.5.171", - "Filter_Enabled": true, - "Filter_Human_Label": "Segment Features (C-Axis Misalignment)", - "Filter_Name": "CAxisSegmentFeatures", - "Filter_Uuid": "{bff6be19-1219-5876-8838-1574ad29d965}", - "GoodVoxelsArrayPath": {"Data Container Name": "TC2_PiFold", "Attribute Matrix Name": "CellData", "Data Array Name": "Mask"}, - "MisorientationTolerance": 10.0, - "QuatsArrayPath": {"Data Container Name": "TC2_PiFold", "Attribute Matrix Name": "CellData", "Data Array Name": "Quats"}, - "UseGoodVoxels": 0 - }, - "3": { - "ActiveArrayName": "Active", - "CellFeatureAttributeMatrixName": "CellFeatureData", - "CellPhasesArrayPath": {"Data Container Name": "TC3_Mask", "Attribute Matrix Name": "CellData", "Data Array Name": "Phases"}, - "CrystalStructuresArrayPath": {"Data Container Name": "TC3_Mask", "Attribute Matrix Name": "CellEnsembleData", "Data Array Name": "CrystalStructures"}, - "FeatureIdsArrayName": "FeatureIds", - "FilterVersion": "6.5.171", - "Filter_Enabled": true, - "Filter_Human_Label": "Segment Features (C-Axis Misalignment)", - "Filter_Name": "CAxisSegmentFeatures", - "Filter_Uuid": "{bff6be19-1219-5876-8838-1574ad29d965}", - "GoodVoxelsArrayPath": {"Data Container Name": "TC3_Mask", "Attribute Matrix Name": "CellData", "Data Array Name": "Mask"}, - "MisorientationTolerance": 10.0, - "QuatsArrayPath": {"Data Container Name": "TC3_Mask", "Attribute Matrix Name": "CellData", "Data Array Name": "Quats"}, - "UseGoodVoxels": 1 - }, - "4": { - "ActiveArrayName": "Active", - "CellFeatureAttributeMatrixName": "CellFeatureData", - "CellPhasesArrayPath": {"Data Container Name": "TC4_Phase0", "Attribute Matrix Name": "CellData", "Data Array Name": "Phases"}, - "CrystalStructuresArrayPath": {"Data Container Name": "TC4_Phase0", "Attribute Matrix Name": "CellEnsembleData", "Data Array Name": "CrystalStructures"}, - "FeatureIdsArrayName": "FeatureIds", - "FilterVersion": "6.5.171", - "Filter_Enabled": true, - "Filter_Human_Label": "Segment Features (C-Axis Misalignment)", - "Filter_Name": "CAxisSegmentFeatures", - "Filter_Uuid": "{bff6be19-1219-5876-8838-1574ad29d965}", - "GoodVoxelsArrayPath": {"Data Container Name": "TC4_Phase0", "Attribute Matrix Name": "CellData", "Data Array Name": "Mask"}, - "MisorientationTolerance": 10.0, - "QuatsArrayPath": {"Data Container Name": "TC4_Phase0", "Attribute Matrix Name": "CellData", "Data Array Name": "Quats"}, - "UseGoodVoxels": 0 - }, - "5": { - "ActiveArrayName": "Active", - "CellFeatureAttributeMatrixName": "CellFeatureData", - "CellPhasesArrayPath": {"Data Container Name": "TC5_3D", "Attribute Matrix Name": "CellData", "Data Array Name": "Phases"}, - "CrystalStructuresArrayPath": {"Data Container Name": "TC5_3D", "Attribute Matrix Name": "CellEnsembleData", "Data Array Name": "CrystalStructures"}, - "FeatureIdsArrayName": "FeatureIds", - "FilterVersion": "6.5.171", - "Filter_Enabled": true, - "Filter_Human_Label": "Segment Features (C-Axis Misalignment)", - "Filter_Name": "CAxisSegmentFeatures", - "Filter_Uuid": "{bff6be19-1219-5876-8838-1574ad29d965}", - "GoodVoxelsArrayPath": {"Data Container Name": "TC5_3D", "Attribute Matrix Name": "CellData", "Data Array Name": "Mask"}, - "MisorientationTolerance": 10.0, - "QuatsArrayPath": {"Data Container Name": "TC5_3D", "Attribute Matrix Name": "CellData", "Data Array Name": "Quats"}, - "UseGoodVoxels": 1 - }, - "6": { - "FilterVersion": "6.5.171", - "Filter_Enabled": true, - "Filter_Human_Label": "Write DREAM.3D Data File", - "Filter_Name": "DataContainerWriter", - "Filter_Uuid": "{3fcd4c43-9d75-5b86-aad4-4441bc914f37}", - "OutputFile": "/Users/mjackson/Workspace5/simplnx/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/results/legacy_out.dream3d", - "WriteTimeSeries": false, - "WriteXdmfFile": false - }, - "PipelineBuilder": { - "Name": "CAxisSegmentFeatures Legacy 6.5.171 A/B", - "Number_Filters": 7, - "Version": 6 - } -} diff --git a/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/pipelines/make_input.py b/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/pipelines/make_input.py deleted file mode 100644 index 8da2d22baa..0000000000 --- a/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/pipelines/make_input.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python3 -"""Author the shared legacy-format input file for the CAxisSegmentFeatures A/B comparison. - -Builds four pure-Phi Bunge fixtures (the same Class 1 analytical fixtures encoded in -CAxisSegmentFeaturesTest.cpp) as separate DataContainers in one legacy v7 .dream3d file that -BOTH the DREAM3D 6.5.171 PipelineRunner and nxrunner read: - - TC1_Chain 8x1x1 Phi = [0, 5, 8, 45, 50, 120, 124, 90], tol 10 -> partition {0,1,2}{3,4}{5,6}{7} - TC2_PiFold 3x1x1 Phi = [2, 176, 88], tol 10 -> partition {0,1}{2} - TC3_Mask 5x1x1 Phi = [0, 20, 22, 0, 90], mask [0,1,1,0,1] -> {1,2}{4}, cells 0,3 -> id 0 - TC4_Phase0 4x1x1 Phi = [0,0,0,0], phases [0,1,1,1] -> {1,2,3}, cell 0 -> id 0 - TC5_3D 3x2x2 Phi = [0,50,55, 4,90,53, 8,95,130, 6,176,120] -> see below - mask [0,1,1,1,1,0, 1,1,1,1,1,1] - -TC5_3D exercises the y- and z-stride branches of the 3-D flood fill against legacy (TC1-TC4 are -all 1-D lines). With x-fastest linearization (idx = x + 3y + 6z) and tol 10 the expected partition -is: {1,2} via x; {3,6,9,10} spanning a z hop (3->9: |4-6|=2), a y hop (9->6: |6-8|=2), and a -pi-fold x hop (9->10: fold(6,176)=10); singletons {4} and {7}; {8,11} via a y hop at z=1 -(|130-120|=10). Masked cells 0 and 5 keep id 0 (cell 0 also pins the validated-first-seed path). -Expected FeatureIds (canonical NX labeling): [0,1,1,2,3,0,2,4,5,2,2,5] -> 5 features. - -A pure rotation about x by Phi tilts the c-axis by Phi in the sample y-z plane, so the c-axis -angular distance between two cells is exactly min(|dPhi|, 180 - |dPhi|). Quaternion storage is -{x, y, z, w} = {sin(Phi/2), 0, 0, cos(Phi/2)} for both SIMPL and NX. - -Usage: python3 make_input.py (writes ../results/caxis_ab_input.dream3d) -""" -import math -import os -import sys - -import numpy as np - -# The legacy_dream3d writer module ships with the compare-legacy-dream3d skill; point -# LEGACY_DREAM3D_SKILL_DIR at a checkout of it if yours lives elsewhere. -sys.path.insert(0, os.environ.get("LEGACY_DREAM3D_SKILL_DIR", "/Users/mjackson/Workspace1/Claude_Support/skills/compare-legacy-dream3d")) -from legacy_dream3d import D3DLegacyWriter # noqa: E402 - -HEX_HIGH = 0 # EbsdLib CrystalStructure enum value for Hexagonal_High -UNKNOWN = 999 # EbsdLib CrystalStructure "unknown" sentinel (ensemble 0) - - -def quat_from_phi_deg(phi_deg): - half = math.radians(phi_deg) * 0.5 - return [math.sin(half), 0.0, 0.0, math.cos(half)] - - -def add_case(w, dc_name, phis, phases=None, mask=None, num_ensembles=2, dims=None): - n = len(phis) - if dims is None: - dims = (n, 1, 1) # (x, y, z) - assert dims[0] * dims[1] * dims[2] == n - w.add_image_geom(dc_name, dims) - w.add_attribute_matrix(dc_name, "CellData", (dims[2], dims[1], dims[0]), "Cell") - quats = np.array([quat_from_phi_deg(p) for p in phis], dtype=np.float32) - w.add_data_array(dc_name, "CellData", "Quats", quats, comp_dims=(4,)) - if phases is None: - phases = [1] * n - w.add_data_array(dc_name, "CellData", "Phases", np.array(phases, dtype=np.int32)) - if mask is not None: - w.add_data_array(dc_name, "CellData", "Mask", np.array(mask, dtype=bool), object_type="DataArray") - w.add_attribute_matrix(dc_name, "CellEnsembleData", (num_ensembles,), "CellEnsemble") - structures = np.full(num_ensembles, HEX_HIGH, dtype=np.uint32) - structures[0] = UNKNOWN - w.add_data_array(dc_name, "CellEnsembleData", "CrystalStructures", structures) - - -def main(): - out_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "results") - os.makedirs(out_dir, exist_ok=True) - out_path = os.path.abspath(os.path.join(out_dir, "caxis_ab_input.dream3d")) - - with D3DLegacyWriter(out_path) as w: - add_case(w, "TC1_Chain", [0.0, 5.0, 8.0, 45.0, 50.0, 120.0, 124.0, 90.0]) - add_case(w, "TC2_PiFold", [2.0, 176.0, 88.0]) - add_case(w, "TC3_Mask", [0.0, 20.0, 22.0, 0.0, 90.0], mask=[0, 1, 1, 0, 1]) - add_case(w, "TC4_Phase0", [0.0, 0.0, 0.0, 0.0], phases=[0, 1, 1, 1]) - add_case(w, "TC5_3D", [0.0, 50.0, 55.0, 4.0, 90.0, 53.0, 8.0, 95.0, 130.0, 6.0, 176.0, 120.0], mask=[0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1], dims=(3, 2, 2)) - - print(f"wrote {out_path}") - - -if __name__ == "__main__": - main() diff --git a/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/pipelines/nx.d3dpipeline b/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/pipelines/nx.d3dpipeline deleted file mode 100644 index db8279ab4c..0000000000 --- a/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/pipelines/nx.d3dpipeline +++ /dev/null @@ -1,137 +0,0 @@ -{ - "isDisabled": false, - "name": "nx.d3dpipeline", - "pinnedParams": [], - "pipeline": [ - { - "args": { - "import_data_object": { - "value": { - "data_paths": [], - "file_path": "/Users/mjackson/Workspace5/simplnx/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/results/caxis_ab_input.dream3d", - "path_import_policy": 0 - }, - "version": 2 - }, - "parameters_version": 1 - }, - "comments": "Read the SAME legacy-format input file the 6.5.171 pipeline reads.", - "filter": {"name": "nx::core::ReadDREAM3DFilter", "uuid": "0dbd31c7-19e0-4077-83ef-f4a6459a0e2d"}, - "isDisabled": false - }, - { - "args": { - "input_image_geometry_path": {"value": "TC1_Chain", "version": 1}, - "misorientation_tolerance": {"value": 10.0, "version": 1}, - "use_mask": {"value": false, "version": 1}, - "quats_array_path": {"value": "TC1_Chain/CellData/Quats", "version": 1}, - "cell_phases_array_path": {"value": "TC1_Chain/CellData/Phases", "version": 1}, - "mask_array_path": {"value": "", "version": 1}, - "crystal_structures_array_path": {"value": "TC1_Chain/CellEnsembleData/CrystalStructures", "version": 1}, - "feature_ids_array_name": {"value": "FeatureIds", "version": 1}, - "cell_feature_attribute_matrix_name": {"value": "CellFeatureData", "version": 1}, - "active_array_name": {"value": "Active", "version": 1}, - "randomize_feature_ids": {"value": false, "version": 1}, - "neighbor_scheme_index": {"value": 0, "version": 1}, - "parameters_version": 2 - }, - "comments": "TC1 chain, face, no mask. Expected partition {0,1,2}{3,4}{5,6}{7}.", - "filter": {"name": "nx::core::CAxisSegmentFeaturesFilter", "uuid": "9fe07e17-aef1-4bf1-834c-d3a73dafc27d"}, - "isDisabled": false - }, - { - "args": { - "input_image_geometry_path": {"value": "TC2_PiFold", "version": 1}, - "misorientation_tolerance": {"value": 10.0, "version": 1}, - "use_mask": {"value": false, "version": 1}, - "quats_array_path": {"value": "TC2_PiFold/CellData/Quats", "version": 1}, - "cell_phases_array_path": {"value": "TC2_PiFold/CellData/Phases", "version": 1}, - "mask_array_path": {"value": "", "version": 1}, - "crystal_structures_array_path": {"value": "TC2_PiFold/CellEnsembleData/CrystalStructures", "version": 1}, - "feature_ids_array_name": {"value": "FeatureIds", "version": 1}, - "cell_feature_attribute_matrix_name": {"value": "CellFeatureData", "version": 1}, - "active_array_name": {"value": "Active", "version": 1}, - "randomize_feature_ids": {"value": false, "version": 1}, - "neighbor_scheme_index": {"value": 0, "version": 1}, - "parameters_version": 2 - }, - "comments": "TC2 pi-fold antiparallel c-axes. Expected partition {0,1}{2}.", - "filter": {"name": "nx::core::CAxisSegmentFeaturesFilter", "uuid": "9fe07e17-aef1-4bf1-834c-d3a73dafc27d"}, - "isDisabled": false - }, - { - "args": { - "input_image_geometry_path": {"value": "TC3_Mask", "version": 1}, - "misorientation_tolerance": {"value": 10.0, "version": 1}, - "use_mask": {"value": true, "version": 1}, - "quats_array_path": {"value": "TC3_Mask/CellData/Quats", "version": 1}, - "cell_phases_array_path": {"value": "TC3_Mask/CellData/Phases", "version": 1}, - "mask_array_path": {"value": "TC3_Mask/CellData/Mask", "version": 1}, - "crystal_structures_array_path": {"value": "TC3_Mask/CellEnsembleData/CrystalStructures", "version": 1}, - "feature_ids_array_name": {"value": "FeatureIds", "version": 1}, - "cell_feature_attribute_matrix_name": {"value": "CellFeatureData", "version": 1}, - "active_array_name": {"value": "Active", "version": 1}, - "randomize_feature_ids": {"value": false, "version": 1}, - "neighbor_scheme_index": {"value": 0, "version": 1}, - "parameters_version": 2 - }, - "comments": "TC3 bool mask excludes voxels 0 and 3. Expected {1,2}{4}, masked cells id 0.", - "filter": {"name": "nx::core::CAxisSegmentFeaturesFilter", "uuid": "9fe07e17-aef1-4bf1-834c-d3a73dafc27d"}, - "isDisabled": false - }, - { - "args": { - "input_image_geometry_path": {"value": "TC4_Phase0", "version": 1}, - "misorientation_tolerance": {"value": 10.0, "version": 1}, - "use_mask": {"value": false, "version": 1}, - "quats_array_path": {"value": "TC4_Phase0/CellData/Quats", "version": 1}, - "cell_phases_array_path": {"value": "TC4_Phase0/CellData/Phases", "version": 1}, - "mask_array_path": {"value": "", "version": 1}, - "crystal_structures_array_path": {"value": "TC4_Phase0/CellEnsembleData/CrystalStructures", "version": 1}, - "feature_ids_array_name": {"value": "FeatureIds", "version": 1}, - "cell_feature_attribute_matrix_name": {"value": "CellFeatureData", "version": 1}, - "active_array_name": {"value": "Active", "version": 1}, - "randomize_feature_ids": {"value": false, "version": 1}, - "neighbor_scheme_index": {"value": 0, "version": 1}, - "parameters_version": 2 - }, - "comments": "TC4 unindexed (phase 0) cell 0. Expected {1,2,3}, cell 0 id 0.", - "filter": {"name": "nx::core::CAxisSegmentFeaturesFilter", "uuid": "9fe07e17-aef1-4bf1-834c-d3a73dafc27d"}, - "isDisabled": false - }, - { - "args": { - "input_image_geometry_path": {"value": "TC5_3D", "version": 1}, - "misorientation_tolerance": {"value": 10.0, "version": 1}, - "use_mask": {"value": true, "version": 1}, - "quats_array_path": {"value": "TC5_3D/CellData/Quats", "version": 1}, - "cell_phases_array_path": {"value": "TC5_3D/CellData/Phases", "version": 1}, - "mask_array_path": {"value": "TC5_3D/CellData/Mask", "version": 1}, - "crystal_structures_array_path": {"value": "TC5_3D/CellEnsembleData/CrystalStructures", "version": 1}, - "feature_ids_array_name": {"value": "FeatureIds", "version": 1}, - "cell_feature_attribute_matrix_name": {"value": "CellFeatureData", "version": 1}, - "active_array_name": {"value": "Active", "version": 1}, - "randomize_feature_ids": {"value": false, "version": 1}, - "neighbor_scheme_index": {"value": 0, "version": 1}, - "parameters_version": 2 - }, - "comments": "TC5 3x2x2 (x-fastest), bool mask excludes voxels 0 and 5; features span y/z strides and a pi-fold x hop. Expected {1,2}{3,6,9,10}{4}{7}{8,11}, masked cells id 0.", - "filter": {"name": "nx::core::CAxisSegmentFeaturesFilter", "uuid": "9fe07e17-aef1-4bf1-834c-d3a73dafc27d"}, - "isDisabled": false - }, - { - "args": { - "export_file_path": {"value": "/Users/mjackson/Workspace5/simplnx/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/results/nx_out.dream3d", "version": 1}, - "write_xdmf_file": {"value": false, "version": 1}, - "use_compression": {"value": true, "version": 1}, - "compression_level": {"value": 5, "version": 1}, - "parameters_version": 2 - }, - "comments": "", - "filter": {"name": "nx::core::WriteDREAM3DFilter", "uuid": "b3a95784-2ced-41ec-8d3d-0242ac130003"}, - "isDisabled": false - } - ], - "version": 1, - "workflowParams": [] -} diff --git a/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/results/.gitignore b/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/results/.gitignore deleted file mode 100644 index 90f47baddc..0000000000 --- a/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/results/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Regenerable A/B comparison outputs — reproduce via ../pipelines (see comparison.md) -*.dream3d -*.xdmf diff --git a/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/results/comparison.md b/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/results/comparison.md deleted file mode 100644 index 8b4893f1e9..0000000000 --- a/src/Plugins/OrientationAnalysis/vv/comparisons/CAxisSegmentFeaturesFilter/results/comparison.md +++ /dev/null @@ -1,65 +0,0 @@ -# Legacy Comparison: CAxisSegmentFeaturesFilter - -Date: 2026-07-22 (TC1–TC4); TC5_3D added and full suite rerun 2026-07-24 - -## Test Cases - -Five pure-Phi Bunge fixtures (TC1–TC4 identical to the Class 1 analytical fixtures in -`test/CAxisSegmentFeaturesTest.cpp`; TC5_3D exercises the y/z stride branches of the 3-D flood -fill, which the 1-D fixtures cannot), tolerance 10°, face neighbors: - -| Case | Grid | Phi per cell (deg) | Extras | Expected partition | -|---|---|---|---|---| -| TC1_Chain | 8×1×1 | 0, 5, 8, 45, 50, 120, 124, 90 | — | {0,1,2} {3,4} {5,6} {7} | -| TC2_PiFold | 3×1×1 | 2, 176, 88 | π-fold branch | {0,1} {2} | -| TC3_Mask | 5×1×1 | 0, 20, 22, 0, 90 | bool mask [0,1,1,0,1] | {1,2} {4}; cells 0,3 → id 0 | -| TC4_Phase0 | 4×1×1 | 0, 0, 0, 0 | phases [0,1,1,1] | {1,2,3}; cell 0 → id 0 | -| TC5_3D | 3×2×2 | 0, 50, 55, 4, 90, 53, 8, 95, 130, 6, 176, 120 | bool mask [0,1,1,1,1,0,1,1,1,1,1,1] | {1,2} {3,6,9,10} {4} {7} {8,11}; cells 0,5 → id 0 | - -TC5_3D uses x-fastest linearization (idx = x + 3y + 6z). Feature {3,6,9,10} spans a z-stride hop -(3→9: |4−6| = 2°), a y-stride hop (9→6: |6−8| = 2°), and a π-fold x hop (9→10: fold(6°,176°) = 10°); -{8,11} spans a y-stride hop at z = 1 (|130−120| = 10°). Masked voxel 0 additionally pins the -validated-first-seed (D1) path in 3-D against legacy. - -## Input Data - -One legacy v7 `.dream3d` file authored by `../pipelines/make_input.py` (legacy_dream3d writer), -read by BOTH runners, so the Quats/Phases/Mask/CrystalStructures inputs are byte-identical. - -## Runners - -- Legacy: `/Users/mjackson/Applications/DREAM3D.app/Contents/Bin/PipelineRunner` (official 6.5.171) -- NX: `/Users/mjackson/Workspace5/DREAM3D-Build/NX-Com-Qt69-Vtk95-Rel/Bin/nxrunner`, simplnx at the V&V commit (post D1/D4/D5 fixes) - -## Results - -Full-suite rerun 2026-07-24 (legacy FeatureIds differ run-to-run by construction, D2): - -| Case | Legacy FeatureIds | NX FeatureIds | Partition match | Feature count match | -|---|---|---|---|---| -| TC1_Chain | [1,1,1,4,4,2,2,3] | [1,1,1,2,2,3,3,4] | YES | YES (4+1 Active tuples) | -| TC2_PiFold | [1,1,2] | [1,1,2] | YES | YES (2+1) | -| TC3_Mask | [0,2,2,0,1] | [0,1,1,0,2] | YES | YES (2+1) | -| TC4_Phase0 | [0,1,1,1] | [0,1,1,1] | YES | YES (1+1) | -| TC5_3D | [0,4,4,2,1,0,2,5,3,2,2,3] | [0,1,1,2,3,0,2,4,5,2,2,5] | YES | YES (5+1) | - -**All five cases match at the segmentation-partition level, and feature counts are identical.** -Raw legacy ids are a random permutation of NX ids because 6.5.171 unconditionally randomizes -FeatureIds with a clock-derived seed (`CAxisSegmentFeaturesFilter-D2`); bit-identical FeatureIds are -not attainable against 6.5.171 by construction. NX ids additionally match the Class 1 oracle -expectation exactly (TC5_3D against an independently computed reference flood fill). - -## Fixes Applied - -None in this comparison round — SIMPLNX had already been reconciled against the Class 1/4 -oracle earlier in the V&V cycle, which fixed three SIMPLNX defects (D1 unvalidated first seed, -D4 spurious rejection of unindexed/masked cells, D5 RectGrid crash). No legacy patch: legacy -output is not wrong on any shared code path. - -## Notes - -- Legacy 6.5.171 cannot disable FeatureIds randomization (not exposed as a parameter), so the - comparison is partition-level by necessity. Partition equality is the meaningful invariant. -- NX-only behavior not comparable against 6.5.171: 26-neighbor "All Connected" scheme, RectGrid - geometry input, uint8 masks, crystal-structure validation (D3). -- Reproduce: `python3 ../pipelines/make_input.py`, then run both pipelines in `../pipelines/`. diff --git a/src/Plugins/OrientationAnalysis/vv/provenance/6_6_stats_test_v2.md b/src/Plugins/OrientationAnalysis/vv/provenance/6_6_stats_test_v2.md deleted file mode 100644 index ce481cbe45..0000000000 --- a/src/Plugins/OrientationAnalysis/vv/provenance/6_6_stats_test_v2.md +++ /dev/null @@ -1,47 +0,0 @@ -# Provenance: `6_6_stats_test_v2.tar.gz` - -| Field | Value | -|---|---| -| Archive | `6_6_stats_test_v2.tar.gz` | -| SHA512 | `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723` | -| Registered at | `src/Plugins/OrientationAnalysis/test/CMakeLists.txt:130` | -| Contents | `6_6_stats_test_v2.dream3d` (58.7 MB) + `6_6_stats_test_v2.xdmf` | -| Regenerated during this V&V cycle | **No** | - -## What it is - -A single `.dream3d` file holding the Small IN100 dataset carried through the reconstruction and statistics pipelines, with the Feature-level statistics arrays of many filters stored alongside their inputs. Consumers use it in the "self-exemplar" pattern: the test reads the file, runs one filter into `Calculated_` arrays inside the same `Cell Feature Data` attribute matrix, and compares against the pre-stored `` arrays. - -For `ComputeSchmidsFilter` the relevant pre-stored arrays are `Schmids`, `SlipSystems`, `Poles`, `Schmid_Phis` and `Schmid_Lambdas` under `DataContainer/Cell Feature Data`. - -## Who generated it, when, how - -Generated by the BlueQuartz team before this V&V programme began, by running the `Small_IN100_Processing` prebuilt pipeline series in DREAM3D-NX and writing the accumulated `DataStructure` out with `WriteDREAM3DFilter`. Uploaded to the [simplnx Data_Archive release](https://github.com/BlueQuartzSoftware/simplnx/releases/tag/Data_Archive). No per-array generation log survives. - -## What oracle was canonical when it was made - -**None.** This archive is a **circular oracle** with respect to every filter that consumes it: the stored arrays are that filter's own output from an earlier build of the same code, not an independently established expected value. It was never validated against an analytical, reference-implementation or paper-based oracle. - -That is the reason the V&V cycles that touch this archive do not treat it as the oracle. It is retained as a **regression** instrument — it answers "did this change alter the output on production-scale real data?", which is a genuinely useful question, and it exercises array shapes and Feature counts no hand-built fixture reaches. - -## Known staleness - -**The three float Schmid arrays in this archive are stale as of EbsdLib 3.1.1.** - -`Schmids`, `Schmid_Phis` and `Schmid_Lambdas` were produced by a build linked against EbsdLib <= 3.1.0, whose `CubicOps::getSchmidFactorAndSS` normalized with the `float` literals `1.732f` and `1.414f` instead of `sqrt(3)` and `sqrt(2)` (see `vv/deviations/ComputeSchmidsFilter.md`, `ComputeSchmidsFilter-D1`). Every archived value in those three arrays is therefore inflated by a known, uniform, per-array factor: - -| Array | archived / correct | Exact expression | -|---|---|---| -| `Schmid_Phis` | 1.0000293384785181 | `sqrt(3) / 1.732f` | -| `Schmid_Lambdas` | 1.0001510099261918 | `sqrt(2) / 1.414f` | -| `Schmids` | 1.0001803528351113 | `sqrt(6) / (1.732f * 1.414f)` | - -`SlipSystems` and `Poles` are **not** stale. The inflation is a single positive scale factor over all twelve candidate slip systems, so the argmax cannot move; and `Poles` is a function of the crystal-frame loading direction, which the normalizers never touch. - -`ComputeSchmidsTest.cpp` handles this by asserting the *relationship* rather than equality: the two integer arrays must still match the archive exactly, and each float array must equal the freshly computed value times the factor above, to `1e-6` relative. The predicted `Schmid_Lambdas` factor was met by the observed ratio to `1.1e-8` relative across the whole Feature set, which is a bit-level confirmation of the D1 analysis on production-scale data. - -**The archive was deliberately not regenerated in this cycle.** It is shared by several filters' tests, and reproducing it requires re-running the full `Small_IN100_Processing` series; churning it inside a Schmid-factor PR would put unrelated filters' regression baselines at risk in a change nobody reviewing that PR is equipped to check. When it is next regenerated against EbsdLib >= 3.1.1, all three factors become `1.0` and the `ComputeSchmidsTest.cpp` block that carries them should collapse back to a plain `CompareDataArrays`; a comment in the test says so. - -## Consumers - -`ComputeSchmidsTest.cpp` is documented here because this V&V cycle covered it. Other tests in this plugin and in `SimplnxCore` also consume this archive; their `download_test_data()` line and SHA512 are the same one recorded above, and this cycle changed neither. From 0ac30cbb416c0d6aa85d7f1a04716635bb207f86 Mon Sep 17 00:00:00 2001 From: Michael Jackson Date: Wed, 26 Aug 2026 12:20:45 -0400 Subject: [PATCH 3/3] VERS: Update EbsdLib to 3.1.2 --- docs/vv_templates/report_template.md | 23 +- .../docs/ComputeSchmidsFilter.md | 18 +- .../Filters/Algorithms/ComputeSchmids.cpp | 81 +-- .../Filters/Algorithms/ComputeSchmids.hpp | 4 +- .../Filters/ComputeSchmidsFilter.cpp | 114 ++-- .../test/ComputeSchmidsTest.cpp | 535 ++++++++---------- .../vv/ComputeSchmidsFilter.md | 181 +++--- .../vv/deviations/ComputeSchmidsFilter.md | 45 +- vcpkg-configuration.json | 2 +- vcpkg.json | 2 +- 10 files changed, 508 insertions(+), 497 deletions(-) diff --git a/docs/vv_templates/report_template.md b/docs/vv_templates/report_template.md index b6bcc360e0..3b3123334a 100644 --- a/docs/vv_templates/report_template.md +++ b/docs/vv_templates/report_template.md @@ -1,5 +1,7 @@ # V&V Report: + + | | | |--------|--------------| | Plugin | | @@ -46,6 +48,17 @@ For worked instances see `src/Plugins/OrientationAnalysis/vv/BadDataNeighborOrie *Second-engineer review:* ** OR *skipped — reason.* +## Bugs found and fixed + + + +*This branch fixes all defects in this table. The fixes will be in the DREAM3D-NX release after v.* + +| Deviation | Defect | Affected released versions | Resolution in this branch | +|---|---|---|---| +| `-D1` | *State the defect and its effect.* | *DREAM.3D 6.5.171; DREAM3D-NX v through v.* | *State how the verified branch fixes the defect.* | +| `-D2` | *State the defect and its effect.* | *DREAM.3D 6.5.171 only. DREAM3D-NX was not affected.* | *State how the verified branch fixes or prevents the defect.* | + ## Code path coverage *N of M paths exercised. If N < M: which paths are NOT covered, and why each gap is acceptable (or what would close it).* @@ -78,7 +91,7 @@ When a path is intentionally not covered, write `*Not directly tested. `* - **Provenance:** *`src/Plugins/

/vv/provenance/.md`* -## Deviations from DREAM3D 6.5.171 +## Deviations from DREAM.3D 6.5.171 *Either:* @@ -86,5 +99,9 @@ When a path is intentionally not covered, write `*Not directly tested. -D1` — *one-line symptom* — see `vv/deviations/.md` -- `-D2` — *one-line symptom* — see `vv/deviations/.md` +See `vv/deviations/.md` for the root cause, affected users, and recommendation for each deviation. + +| Deviation | Observed difference | +|---|---| +| `-D1` | *State the user-visible difference.* | +| `-D2` | *State the user-visible difference.* | diff --git a/src/Plugins/OrientationAnalysis/docs/ComputeSchmidsFilter.md b/src/Plugins/OrientationAnalysis/docs/ComputeSchmidsFilter.md index 0e109e9861..068883fe01 100644 --- a/src/Plugins/OrientationAnalysis/docs/ComputeSchmidsFilter.md +++ b/src/Plugins/OrientationAnalysis/docs/ComputeSchmidsFilter.md @@ -6,17 +6,17 @@ Statistics (Crystallography) ## Description -This **Filter** calculates the Schmid factor of each **Feature** given its average orientation and a user defined loading axis. The Schmid Factor is the combination of the component of the axial force *F* that lies parallel to the slip direction and the component that lies perpendicular to the slip plane. The equation for the Schmid Factor is given as: +This **Filter** calculates the Schmid factor of each **Feature** given its average orientation and a user-defined loading axis. The Schmid factor combines the component of the axial force *F* that lies parallel to the slip direction with the component perpendicular to the slip plane. The equation is: Schmid Factor = (cos φ cos λ) *The angle φ is the angle between the tensile axis and the slip plane normal, and λ is the angle between the tensile axis and the slip direction in the slip plane.* -The **Filter** determines the Schmid factor for each **Feature** by using the above equation for all possible slip systems (given the **Feature's** crystal structure). The largest Schmid factor from all of the slip systems is stored for the **Feature**. Only the Schmid factor is used in determining which slip system's Schmid factor to report. The critical resolved shear stress for the different slip systems is not considered. +The **Filter** evaluates this equation for every enumerated slip system in each **Feature's** crystal structure. It stores the largest Schmid factor and the corresponding slip-system index. Critical resolved shear stress is not considered. -The user-supplied *Loading Direction* is normalized before use, so only its direction matters — `[1, 2, 3]` and `[3, 6, 9]` give identical results. It is a **sample-frame** direction; the **Filter** rotates it into each **Feature's** crystal frame using that **Feature's** average orientation before evaluating the slip systems. +The user-supplied *Loading Direction* is normalized before use, so only its direction matters — `[1, 2, 3]` and `[3, 6, 9]` give identical results. It is a **sample-frame** direction; the **Filter** rotates it into each **Feature's** crystal frame using that **Feature's** average orientation before evaluating the slip systems. The loading direction must be non-zero. When *Override Default Slip System* is enabled, the supplied slip-plane normal and slip direction must also be non-zero and perpendicular. -**Feature 0** is the conventional "unassigned" **Feature** and is never computed; all of its output values are zero. A **Feature** whose phase maps to a crystal structure for which no slip systems are enumerated is skipped, and all of its output values are likewise zero. +**Feature 0** is the conventional "unassigned" **Feature** and is never computed; all of its output values are zero. A **Feature** whose crystal-structure value is outside EbsdLib's supported Laue-group range is skipped, and all of its output values are also zero. For a valid Laue class that has no enumerated slip systems, *Schmids*, *Slip Systems*, *Phis*, and *Lambdas* are zero, while *Poles* still records the transformed loading direction. ### Ties Between Slip Systems @@ -33,7 +33,7 @@ The meaning of the *Slip Systems* output changes with the *Override Default Slip The two numbering schemes are not comparable, and neither is the base of the *off* numbering comparable between the cubic and hexagonal classes. -A *Slip Systems* value of `0` means **"no slip system found"** in two situations, and in neither of them is it a slip-system number: +A *Slip Systems* value of `0` can mean **"no slip system found"** in two situations: - With *Override Default Slip System* **on** and no symmetry-operator variant producing a non-zero Schmid factor — for example a loading direction parallel to the slip plane normal, which puts the loading axis perpendicular to every slip direction. - With the toggle **off** on a **Hexagonal-High or Hexagonal-Low** phase, where the built-in systems are numbered 1–6 and no candidate exceeds the initial Schmid factor of `0`. Because that numbering starts at 1, `0` falls outside the valid range and is unambiguous. (For the cubic classes, whose numbering starts at 0, the reported `0` in this situation is indistinguishable from a genuine win on system 0; check the reported Schmid factor, which is `0` in the degenerate case.) @@ -44,7 +44,7 @@ With *Override Default Slip System* on, the reported index is **relative to the ### Phis and Lambdas Units Depend on Override Default Slip System -The *Phis* and *Lambdas* arrays change **units** with the same toggle, which is easy to miss because the parameter descriptions and array names do not: +The *Phis* and *Lambdas* arrays change **units** with the same toggle, which is easy to miss because the array names do not change: | Override Default Slip System | *Phis* / *Lambdas* contents | |---|---| @@ -57,6 +57,12 @@ With the toggle off, the reported Schmid factor is the product of the two stored Despite its name, the *Poles* array does **not** contain a crystallographic index. It is the unit loading direction expressed in the **Feature's** crystal frame, multiplied by 100 and **truncated toward zero** to an integer — a compact fixed-point encoding of that unit vector, retaining two decimal places. Components can be negative, and the sum of squares is approximately 10 000 rather than 1. Because the conversion truncates rather than rounds, a component whose scaled value falls near an integer can differ by one from the value you would get by rounding. Reducing the triplet to a Miller index requires dividing by the greatest common divisor yourself, and the truncation means the result is only approximate. +### Required Input Sources + +- **Average Quaternions** -- produced by [Compute Average Orientations](ComputeAvgOrientationsFilter.md). +- **Feature Phases** -- produced by [Compute Feature Phases](../SimplnxCore/ComputeFeaturePhasesFilter.md). +- **Crystal Structures** -- read from EBSD data or produced by [Create Ensemble Info](CreateEnsembleInfoFilter.md). + % Auto generated parameter table will be inserted here ## Example Pipelines diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.cpp index ec72abbf01..111e30120e 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.cpp @@ -9,11 +9,11 @@ using namespace nx::core; // ----------------------------------------------------------------------------- -ComputeSchmids::ComputeSchmids(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, ComputeSchmidsInputValues* inputValues) +ComputeSchmids::ComputeSchmids(DataStructure& dataStructure, const IFilter::MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ComputeSchmidsInputValues* inputValues) : m_DataStructure(dataStructure) , m_InputValues(inputValues) , m_ShouldCancel(shouldCancel) -, m_MessageHandler(mesgHandler) +, m_MessageHandler(messageHandler) { } @@ -21,7 +21,7 @@ ComputeSchmids::ComputeSchmids(DataStructure& dataStructure, const IFilter::Mess ComputeSchmids::~ComputeSchmids() noexcept = default; // ----------------------------------------------------------------------------- -const std::atomic_bool& ComputeSchmids::getCancel() +const std::atomic_bool& ComputeSchmids::getCancel() const { return m_ShouldCancel; } @@ -31,32 +31,32 @@ Result<> ComputeSchmids::operator()() { std::vector orientationOps = ebsdlib::LaueOps::GetAllOrientationOps(); - const auto& avgQuatPtr = m_DataStructure.getDataRefAs(m_InputValues->AvgQuatsArrayPath); - const auto& featurePhases = m_DataStructure.getDataRefAs(m_InputValues->FeaturePhasesArrayPath); - const auto& crystalStructures = m_DataStructure.getDataRefAs(m_InputValues->CrystalStructuresArrayPath); - auto& schmidArray = m_DataStructure.getDataRefAs(m_InputValues->SchmidsArrayName); - auto& slipSystems = m_DataStructure.getDataRefAs(m_InputValues->SlipSystemsArrayName); - auto& poleArrays = m_DataStructure.getDataRefAs(m_InputValues->PolesArrayName); + const auto& avgQuatsRef = m_DataStructure.getDataRefAs(m_InputValues->AvgQuatsArrayPath); + const auto& featurePhasesRef = m_DataStructure.getDataRefAs(m_InputValues->FeaturePhasesArrayPath); + const auto& crystalStructuresRef = m_DataStructure.getDataRefAs(m_InputValues->CrystalStructuresArrayPath); + auto& schmidsRef = m_DataStructure.getDataRefAs(m_InputValues->SchmidsArrayName); + auto& slipSystemsRef = m_DataStructure.getDataRefAs(m_InputValues->SlipSystemsArrayName); + auto& polesRef = m_DataStructure.getDataRefAs(m_InputValues->PolesArrayName); - auto* phiArray = m_DataStructure.getDataAs(m_InputValues->PhisArrayName); - auto* lambdaArray = m_DataStructure.getDataAs(m_InputValues->LambdasArrayName); + auto* phisPtr = m_DataStructure.getDataAs(m_InputValues->PhisArrayName); + auto* lambdasPtr = m_DataStructure.getDataAs(m_InputValues->LambdasArrayName); // Feature 0 is the conventional unassigned-Feature sentinel and is never computed; the loop below // starts at 1. Preflight now creates all five arrays with a "0" fill value so every tuple is // defined regardless, but these explicit writes are kept because they are what documents the // sentinel row's meaning at the point of use. if(m_InputValues->StoreAngleComponents) { - (*phiArray)[0] = 0.0F; - (*lambdaArray)[0] = 0.0F; + (*phisPtr)[0] = 0.0F; + (*lambdasPtr)[0] = 0.0F; } - schmidArray[0] = 0.0F; - poleArrays[0] = 0; - poleArrays[1] = 0; - poleArrays[2] = 0; - slipSystems[0] = 0; + schmidsRef[0] = 0.0F; + polesRef[0] = 0; + polesRef[1] = 0; + polesRef[2] = 0; + slipSystemsRef[0] = 0; - size_t totalFeatures = avgQuatPtr.getNumberOfTuples(); - const usize numEnsembles = crystalStructures.getNumberOfTuples(); + const usize featureCount = avgQuatsRef.getNumberOfTuples(); + const usize ensembleCount = crystalStructuresRef.getNumberOfTuples(); Eigen::Vector3d sampleLoading = {m_InputValues->LoadingDirection[0], m_InputValues->LoadingDirection[1], m_InputValues->LoadingDirection[2]}; sampleLoading.normalize(); @@ -72,7 +72,7 @@ Result<> ComputeSchmids::operator()() direction.normalize(); } - for(size_t i = 1; i < totalFeatures; i++) + for(usize featureIdx = 1; featureIdx < featureCount; featureIdx++) { if(m_ShouldCancel) { @@ -82,19 +82,19 @@ Result<> ComputeSchmids::operator()() // The phase id indexes the Crystal Structures array directly, so it has to be validated before // it is used. An id at or beyond the ensemble count, or a negative one, would read outside the // array and dispatch on whatever Laue value that read produced. - const int32 phaseId = featurePhases[i]; + const int32 phaseId = featurePhasesRef[featureIdx]; if(phaseId < 0) { - return MakeErrorResult(-13502, fmt::format("Feature {} has a negative phase value of {}. Phase values index the Crystal Structures array '{}' and must be zero or greater.", i, phaseId, + return MakeErrorResult(-13502, fmt::format("Feature {} has a negative phase value of {}. Phase values index the Crystal Structures array '{}' and must be zero or greater.", featureIdx, phaseId, m_InputValues->CrystalStructuresArrayPath.toString())); } - if(static_cast(phaseId) >= numEnsembles) + if(static_cast(phaseId) >= ensembleCount) { - return MakeErrorResult(-13501, fmt::format("Feature {} has a phase value of {} but the Crystal Structures array '{}' only has {} entries (valid phase values are 0 through {}).", i, phaseId, - m_InputValues->CrystalStructuresArrayPath.toString(), numEnsembles, numEnsembles - 1)); + return MakeErrorResult(-13501, fmt::format("Feature {} has a phase value of {} but the Crystal Structures array '{}' only has {} entries (valid phase values are 0 through {}).", featureIdx, + phaseId, m_InputValues->CrystalStructuresArrayPath.toString(), ensembleCount, ensembleCount - 1)); } - uint32_t laueClass = crystalStructures[phaseId]; + const uint32_t laueClass = crystalStructuresRef[phaseId]; if(laueClass >= ebsdlib::CrystalStructure::LaueGroupEnd) { continue; @@ -103,35 +103,36 @@ Result<> ComputeSchmids::operator()() // Re-initialized every iteration on purpose. EbsdLib Laue ops that enumerate no slip systems // for their class return without writing every output, so a buffer hoisted out of the loop // would carry the previous Feature's values into the current Feature's row. Fixed in EbsdLib - // 3.1.1-staging as well; keeping the locals loop-scoped means this filter is correct against + // 3.1.1 as well; keeping the locals loop-scoped means this filter is correct against // any EbsdLib. double schmid = 0.0; - double angleComps[2] = {0.0, 0.0}; + double angleComponents[2] = {0.0, 0.0}; int32_t slipSystem = 0; - auto om = ebsdlib::QuaternionDType(avgQuatPtr[i * 4 + 0], avgQuatPtr[i * 4 + 1], avgQuatPtr[i * 4 + 2], avgQuatPtr[i * 4 + 3]).toOrientationMatrix(); - Eigen::Vector3d crystalLoading = om * sampleLoading; + const auto orientationMatrix = + ebsdlib::QuaternionDType(avgQuatsRef[featureIdx * 4 + 0], avgQuatsRef[featureIdx * 4 + 1], avgQuatsRef[featureIdx * 4 + 2], avgQuatsRef[featureIdx * 4 + 3]).toOrientationMatrix(); + Eigen::Vector3d crystalLoading = orientationMatrix * sampleLoading; if(!m_InputValues->OverrideSystem) { - orientationOps[laueClass]->getSchmidFactorAndSS(crystalLoading.data(), schmid, angleComps, slipSystem); + orientationOps[laueClass]->getSchmidFactorAndSS(crystalLoading.data(), schmid, angleComponents, slipSystem); } else { - orientationOps[laueClass]->getSchmidFactorAndSS(crystalLoading.data(), plane.data(), direction.data(), schmid, angleComps, slipSystem); + orientationOps[laueClass]->getSchmidFactorAndSS(crystalLoading.data(), plane.data(), direction.data(), schmid, angleComponents, slipSystem); } - schmidArray[i] = static_cast(schmid); + schmidsRef[featureIdx] = static_cast(schmid); if(m_InputValues->StoreAngleComponents) { - (*phiArray)[i] = angleComps[0]; - (*lambdaArray)[i] = angleComps[1]; + (*phisPtr)[featureIdx] = angleComponents[0]; + (*lambdasPtr)[featureIdx] = angleComponents[1]; } - poleArrays[3 * i] = static_cast(crystalLoading[0] * 100.0); - poleArrays[3 * i + 1] = static_cast(crystalLoading[1] * 100.0); - poleArrays[3 * i + 2] = static_cast(crystalLoading[2] * 100.0); - slipSystems[i] = slipSystem; + polesRef[3 * featureIdx] = static_cast(crystalLoading[0] * 100.0); + polesRef[3 * featureIdx + 1] = static_cast(crystalLoading[1] * 100.0); + polesRef[3 * featureIdx + 2] = static_cast(crystalLoading[2] * 100.0); + slipSystemsRef[featureIdx] = slipSystem; } return {}; diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.hpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.hpp index 5974e034ec..612e5a317b 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.hpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.hpp @@ -54,7 +54,7 @@ struct ORIENTATIONANALYSIS_EXPORT ComputeSchmidsInputValues class ORIENTATIONANALYSIS_EXPORT ComputeSchmids { public: - ComputeSchmids(DataStructure& dataStructure, const IFilter::MessageHandler& mesgHandler, const std::atomic_bool& shouldCancel, ComputeSchmidsInputValues* inputValues); + ComputeSchmids(DataStructure& dataStructure, const IFilter::MessageHandler& messageHandler, const std::atomic_bool& shouldCancel, const ComputeSchmidsInputValues* inputValues); ~ComputeSchmids() noexcept; ComputeSchmids(const ComputeSchmids&) = delete; @@ -64,7 +64,7 @@ class ORIENTATIONANALYSIS_EXPORT ComputeSchmids Result<> operator()(); - const std::atomic_bool& getCancel(); + const std::atomic_bool& getCancel() const; private: DataStructure& m_DataStructure; diff --git a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeSchmidsFilter.cpp b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeSchmidsFilter.cpp index f50e296ae8..a4808311b3 100644 --- a/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeSchmidsFilter.cpp +++ b/src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/ComputeSchmidsFilter.cpp @@ -12,6 +12,9 @@ #include "simplnx/Parameters/VectorParameter.hpp" +#include +#include + using namespace nx::core; namespace nx::core @@ -54,21 +57,21 @@ Parameters ComputeSchmidsFilter::parameters() const // Create the parameter descriptors that are needed for this filter params.insertSeparator(Parameters::Separator{"Input Parameter(s)"}); - params.insert(std::make_unique(k_LoadingDirection_Key, "Loading Direction", "The loading axis for the sample", std::vector({1.0F, 1.0F, 1.0F}), - std::vector({"X", "Y", "Z"}))); + params.insert(std::make_unique(k_LoadingDirection_Key, "Loading Direction", "A non-zero loading direction in the sample reference frame", + std::vector({1.0F, 1.0F, 1.0F}), std::vector({"X", "Y", "Z"}))); params.insertLinkableParameter( std::make_unique(k_StoreAngleComponents_Key, "Store Angle Components of Schmid Factor", "Whether to store the angle components for each Feature", false)); params.insertLinkableParameter(std::make_unique(k_OverrideSystem_Key, "Override Default Slip System", "Allows the user to manually input the slip plane and slip direction", false)); - params.insert(std::make_unique(k_SlipPlane_Key, "Slip Plane", "Vector defining the slip plane normal.", std::vector({0.0F, 0.0F, 1.0F}), + params.insert(std::make_unique(k_SlipPlane_Key, "Slip Plane", "A non-zero vector defining the slip plane normal", std::vector({0.0F, 0.0F, 1.0F}), std::vector({"X", "Y", "Z"}))); - params.insert(std::make_unique(k_SlipDirection_Key, "Slip Direction", "Vector defining the slip direction.", std::vector({1.0F, 0.0F, 0.0F}), + params.insert(std::make_unique(k_SlipDirection_Key, "Slip Direction", "A non-zero vector defining the slip direction", std::vector({1.0F, 0.0F, 0.0F}), std::vector({"X", "Y", "Z"}))); params.insertSeparator(Parameters::Separator{"Input Feature Data"}); - params.insert(std::make_unique(k_FeaturePhasesArrayPath_Key, "Phases", "Specifies to which Ensemble each cell belongs", DataPath({"Cell Feature Data", "Phases"}), + params.insert(std::make_unique(k_FeaturePhasesArrayPath_Key, "Phases", "Specifies the Ensemble to which each Feature belongs", DataPath({"Cell Feature Data", "Phases"}), ArraySelectionParameter::AllowedTypes{nx::core::DataType::int32}, ArraySelectionParameter::AllowedComponentShapes{{1}})); - params.insert(std::make_unique(k_AvgQuatsArrayPath_Key, "Average Quaternions", "Specifies the average orienation of each Feature in quaternion representation", + params.insert(std::make_unique(k_AvgQuatsArrayPath_Key, "Average Quaternions", "Specifies the average orientation of each Feature in quaternion representation", DataPath({"Cell Feature Data", "AvgQuats"}), ArraySelectionParameter::AllowedTypes{nx::core::DataType::float32}, ArraySelectionParameter::AllowedComponentShapes{{4}})); params.insertSeparator(Parameters::Separator{"Input Ensemble Data"}); @@ -82,10 +85,12 @@ Parameters ComputeSchmidsFilter::parameters() const "Schmids")); params.insert(std::make_unique(k_SlipSystemsArrayName_Key, "Slip Systems", "The name of the array containing the enumeration of the slip system that has the highest Schmid factor", "SlipSystems")); - params.insert(std::make_unique(k_PolesArrayName_Key, "Poles", - "The name of the array specifying the crystallographic pole that points along the user defined loading direction", "Poles")); - params.insert(std::make_unique(k_PhisArrayName_Key, "Phis", "The name of the array containing the angle between tensile axis and slip plane normal. ", "Schmid_Phis")); - params.insert(std::make_unique(k_LambdasArrayName_Key, "Lambdas", "The name of the array containing the angle between tensile axis and slip direction.", "Schmid_Lambdas")); + params.insert(std::make_unique( + k_PolesArrayName_Key, "Poles", "The name of the array containing the normalized loading direction in the crystal frame, multiplied by 100 and truncated toward zero", "Poles")); + params.insert(std::make_unique( + k_PhisArrayName_Key, "Phis", "The name of the array containing cos(phi) when the default slip systems are used or phi in radians when they are overridden", "Schmid_Phis")); + params.insert(std::make_unique( + k_LambdasArrayName_Key, "Lambdas", "The name of the array containing cos(lambda) when the default slip systems are used or lambda in radians when they are overridden", "Schmid_Lambdas")); // Associate the Linkable Parameter(s) to the children parameters that they control params.linkParameters(k_StoreAngleComponents_Key, k_PhisArrayName_Key, true); params.linkParameters(k_StoreAngleComponents_Key, k_LambdasArrayName_Key, true); @@ -98,7 +103,8 @@ Parameters ComputeSchmidsFilter::parameters() const //------------------------------------------------------------------------------ IFilter::VersionType ComputeSchmidsFilter::parametersVersion() const { - return 1; + // Version 2 clarifies the vector constraints and the mode-dependent output-array semantics. + return 2; } //------------------------------------------------------------------------------ @@ -119,26 +125,53 @@ IFilter::PreflightResult ComputeSchmidsFilter::preflightImpl(const DataStructure auto pFeaturePhasesArrayPathValue = filterArgs.value(k_FeaturePhasesArrayPath_Key); auto pAvgQuatsArrayPathValue = filterArgs.value(k_AvgQuatsArrayPath_Key); auto pCrystalStructuresArrayPathValue = filterArgs.value(k_CrystalStructuresArrayPath_Key); - DataPath cellFeatDataPath = pFeaturePhasesArrayPathValue.getParent(); - auto pSchmidsArrayNameValue = cellFeatDataPath.createChildPath(filterArgs.value(k_SchmidsArrayName_Key)); - auto pSlipSystemsArrayNameValue = cellFeatDataPath.createChildPath(filterArgs.value(k_SlipSystemsArrayName_Key)); - auto pPolesArrayNameValue = cellFeatDataPath.createChildPath(filterArgs.value(k_PolesArrayName_Key)); - auto pPhisArrayNameValue = cellFeatDataPath.createChildPath(filterArgs.value(k_PhisArrayName_Key)); - auto pLambdasArrayNameValue = cellFeatDataPath.createChildPath(filterArgs.value(k_LambdasArrayName_Key)); - - PreflightResult preflightResult; + DataPath cellFeatureDataPath = pFeaturePhasesArrayPathValue.getParent(); + auto pSchmidsArrayNameValue = cellFeatureDataPath.createChildPath(filterArgs.value(k_SchmidsArrayName_Key)); + auto pSlipSystemsArrayNameValue = cellFeatureDataPath.createChildPath(filterArgs.value(k_SlipSystemsArrayName_Key)); + auto pPolesArrayNameValue = cellFeatureDataPath.createChildPath(filterArgs.value(k_PolesArrayName_Key)); + auto pPhisArrayNameValue = cellFeatureDataPath.createChildPath(filterArgs.value(k_PhisArrayName_Key)); + auto pLambdasArrayNameValue = cellFeatureDataPath.createChildPath(filterArgs.value(k_LambdasArrayName_Key)); nx::core::Result resultOutputActions; - DataPath featureDataGroup = pFeaturePhasesArrayPathValue.getParent(); + const Int32Array& phasesRef = dataStructure.getDataRefAs(pFeaturePhasesArrayPathValue); + const UInt32Array& crystalStructuresRef = dataStructure.getDataRefAs(pCrystalStructuresArrayPathValue); + + auto tupleValidityCheck = dataStructure.validateNumberOfTuples({pFeaturePhasesArrayPathValue, pAvgQuatsArrayPathValue}); + if(!tupleValidityCheck) + { + return {MakeErrorResult(-13503, fmt::format("The Feature Phases array '{}' and Average Quaternions array '{}' must have equal tuple counts. {}", + pFeaturePhasesArrayPathValue.toString(), pAvgQuatsArrayPathValue.toString(), tupleValidityCheck.error()))}; + } + + if(phasesRef.getNumberOfTuples() == 0) + { + return {MakeErrorResult( + -13504, fmt::format("The Feature Phases array '{}' and Average Quaternions array '{}' each contain 0 tuples. They must contain at least the Feature 0 sentinel tuple.", + pFeaturePhasesArrayPathValue.toString(), pAvgQuatsArrayPathValue.toString()))}; + } + + if(crystalStructuresRef.getNumberOfTuples() == 0) + { + return {MakeErrorResult( + -13505, fmt::format("The Crystal Structures array '{}' contains 0 tuples. It must contain at least one Ensemble tuple.", pCrystalStructuresArrayPathValue.toString()))}; + } + + const auto vectorMagnitude = [](const VectorFloat32Parameter::ValueType& vector) { + return std::sqrt(static_cast(vector[0]) * vector[0] + static_cast(vector[1]) * vector[1] + static_cast(vector[2]) * vector[2]); + }; + + if(vectorMagnitude(pLoadingDirectionValue) < std::numeric_limits::epsilon()) + { + return {MakeErrorResult(-13506, fmt::format("Loading Direction <{}, {}, {}> has zero length. Its components must define a non-zero direction.", pLoadingDirectionValue[0], + pLoadingDirectionValue[1], pLoadingDirectionValue[2]))}; + } - const Int32Array& phases = dataStructure.getDataRefAs(pFeaturePhasesArrayPathValue); - auto tupleShape = phases.getIDataStore()->getTupleShape(); + const auto tupleShape = phasesRef.getIDataStore()->getTupleShape(); // All five output arrays are created with an explicit "0" fill value. The algorithm skips any - // Feature whose Laue class has no slip systems enumerated for it, and Feature 0 is a sentinel - // slot that is never computed, so without the fill those tuples would be handed to the user as - // whatever the allocator happened to hold. Legacy SIMPL FindSchmids allocated with initValue 0. + // Feature whose crystal structure is outside the supported Laue-group range, and Feature 0 is a + // sentinel slot that is never computed. Legacy SIMPL FindSchmids allocated with initValue 0. const std::string k_ZeroFill = "0"; // Create output Schmids Array @@ -171,11 +204,24 @@ IFilter::PreflightResult ComputeSchmidsFilter::preflightImpl(const DataStructure if(pOverrideSystemValue) { + if(vectorMagnitude(pSlipPlaneValue) < std::numeric_limits::epsilon()) + { + return {MakeErrorResult( + -13507, fmt::format("Slip Plane <{}, {}, {}> has zero length. Its components must define a non-zero plane normal.", pSlipPlaneValue[0], pSlipPlaneValue[1], pSlipPlaneValue[2]))}; + } + if(vectorMagnitude(pSlipDirectionValue) < std::numeric_limits::epsilon()) + { + return {MakeErrorResult(-13508, fmt::format("Slip Direction <{}, {}, {}> has zero length. Its components must define a non-zero direction.", pSlipDirectionValue[0], + pSlipDirectionValue[1], pSlipDirectionValue[2]))}; + } + // make sure direction lies in plane - float cosVec = pSlipPlaneValue[0] * pSlipDirectionValue[0] + pSlipPlaneValue[1] * pSlipDirectionValue[1] + pSlipPlaneValue[2] * pSlipDirectionValue[2]; - if(0.0F != cosVec) + const float32 dotProduct = pSlipPlaneValue[0] * pSlipDirectionValue[0] + pSlipPlaneValue[1] * pSlipDirectionValue[1] + pSlipPlaneValue[2] * pSlipDirectionValue[2]; + if(0.0F != dotProduct) { - return {MakeErrorResult(-13500, "Slip Plane and Slip Direction must be normal")}; + return { + MakeErrorResult(-13500, fmt::format("Slip Plane <{}, {}, {}> and Slip Direction <{}, {}, {}> have a dot product of {}. The vectors must be perpendicular.", pSlipPlaneValue[0], + pSlipPlaneValue[1], pSlipPlaneValue[2], pSlipDirectionValue[0], pSlipDirectionValue[1], pSlipDirectionValue[2], dotProduct))}; } } @@ -203,12 +249,12 @@ Result<> ComputeSchmidsFilter::executeImpl(DataStructure& dataStructure, const A inputValues.FeaturePhasesArrayPath = filterArgs.value(k_FeaturePhasesArrayPath_Key); inputValues.AvgQuatsArrayPath = filterArgs.value(k_AvgQuatsArrayPath_Key); inputValues.CrystalStructuresArrayPath = filterArgs.value(k_CrystalStructuresArrayPath_Key); - DataPath cellFeatDataPath = inputValues.FeaturePhasesArrayPath.getParent(); - inputValues.SchmidsArrayName = cellFeatDataPath.createChildPath(filterArgs.value(k_SchmidsArrayName_Key)); - inputValues.SlipSystemsArrayName = cellFeatDataPath.createChildPath(filterArgs.value(k_SlipSystemsArrayName_Key)); - inputValues.PolesArrayName = cellFeatDataPath.createChildPath(filterArgs.value(k_PolesArrayName_Key)); - inputValues.PhisArrayName = cellFeatDataPath.createChildPath(filterArgs.value(k_PhisArrayName_Key)); - inputValues.LambdasArrayName = cellFeatDataPath.createChildPath(filterArgs.value(k_LambdasArrayName_Key)); + DataPath cellFeatureDataPath = inputValues.FeaturePhasesArrayPath.getParent(); + inputValues.SchmidsArrayName = cellFeatureDataPath.createChildPath(filterArgs.value(k_SchmidsArrayName_Key)); + inputValues.SlipSystemsArrayName = cellFeatureDataPath.createChildPath(filterArgs.value(k_SlipSystemsArrayName_Key)); + inputValues.PolesArrayName = cellFeatureDataPath.createChildPath(filterArgs.value(k_PolesArrayName_Key)); + inputValues.PhisArrayName = cellFeatureDataPath.createChildPath(filterArgs.value(k_PhisArrayName_Key)); + inputValues.LambdasArrayName = cellFeatureDataPath.createChildPath(filterArgs.value(k_LambdasArrayName_Key)); return ComputeSchmids(dataStructure, messageHandler, shouldCancel, &inputValues)(); } diff --git a/src/Plugins/OrientationAnalysis/test/ComputeSchmidsTest.cpp b/src/Plugins/OrientationAnalysis/test/ComputeSchmidsTest.cpp index 3ea56a7b38..ddcd6149b2 100644 --- a/src/Plugins/OrientationAnalysis/test/ComputeSchmidsTest.cpp +++ b/src/Plugins/OrientationAnalysis/test/ComputeSchmidsTest.cpp @@ -3,6 +3,7 @@ #include "simplnx/Core/Application.hpp" #include "simplnx/DataStructure/AttributeMatrix.hpp" +#include "simplnx/DataStructure/DataArray.hpp" #include "simplnx/DataStructure/Geometry/ImageGeom.hpp" #include "simplnx/Parameters/ArrayCreationParameter.hpp" #include "simplnx/Parameters/BoolParameter.hpp" @@ -10,15 +11,14 @@ #include "simplnx/Pipeline/Pipeline.hpp" #include "simplnx/Pipeline/PipelineFilter.hpp" #include "simplnx/UnitTest/UnitTestCommon.hpp" +#include "simplnx/Utilities/DataStoreUtilities.hpp" #include #include #include -#include #include -#include #include namespace fs = std::filesystem; @@ -33,7 +33,6 @@ const std::string k_SlipSystemsArrayName("SlipSystems"); const std::string k_PolesArrayName("Poles"); const std::string k_PhisArrayName("Schmid_Phis"); const std::string k_LambdasArrayName("Schmid_Lambdas"); -const std::string k_CalculatedArrayPrefix("Calculated_"); } // namespace // ===================================================================================== @@ -81,7 +80,7 @@ const std::string k_CalculatedArrayPrefix("Calculated_"); // 1.732f / 1.414f, which inflate every Schmid factor by the uniform factor // sqrt(6) / (1.732f * 1.414f) = 1.00018035284 (+0.0180353 %) // and let m exceed the physical cubic maximum of 0.5 (0.500090176 at the maximizing -// direction). Fixed on EbsdLib topic/3_1_1_staging; the values asserted below are the +// direction). Fixed in EbsdLib 3.1.1; the values asserted below are the // EXACT-arithmetic post-fix values and will fail by ~7.4e-5 against EbsdLib <= 3.1.0. namespace { @@ -105,52 +104,58 @@ const DataPath k_EnsembleAMPath({k_GeomName, k_EnsembleAMName}); struct SchmidFixture { DataStructure ds; - Int32Array* featurePhases = nullptr; - Float32Array* avgQuats = nullptr; - UInt32Array* crystalStructures = nullptr; + Int32Array* featurePhasesPtr = nullptr; + Float32Array* avgQuatsPtr = nullptr; + UInt32Array* crystalStructuresPtr = nullptr; }; SchmidFixture MakeFixture(usize numFeatures, usize numEnsembles = 2) { - SchmidFixture td; - auto* imageGeom = ImageGeom::Create(td.ds, k_GeomName); - imageGeom->setDimensions({1, 1, 1}); - imageGeom->setSpacing({1.0F, 1.0F, 1.0F}); - imageGeom->setOrigin({0.0F, 0.0F, 0.0F}); - - auto* featureAM = AttributeMatrix::Create(td.ds, k_FeatureAMName, ShapeType{numFeatures}, imageGeom->getId()); - auto* ensembleAM = AttributeMatrix::Create(td.ds, k_EnsembleAMName, ShapeType{numEnsembles}, imageGeom->getId()); - - td.featurePhases = UnitTest::CreateTestDataArray(td.ds, k_PhasesName, {numFeatures}, {1}, featureAM->getId()); - td.avgQuats = UnitTest::CreateTestDataArray(td.ds, k_AvgQuatsName, {numFeatures}, {4}, featureAM->getId()); - td.crystalStructures = UnitTest::CreateTestDataArray(td.ds, k_CrystalStructuresName, {numEnsembles}, {1}, ensembleAM->getId()); - - for(usize i = 0; i < numFeatures; ++i) + SchmidFixture fixture; + auto* imageGeomPtr = ImageGeom::Create(fixture.ds, k_GeomName); + imageGeomPtr->setDimensions({1, 1, 1}); + imageGeomPtr->setSpacing({1.0F, 1.0F, 1.0F}); + imageGeomPtr->setOrigin({0.0F, 0.0F, 0.0F}); + + auto* featureAMPtr = AttributeMatrix::Create(fixture.ds, k_FeatureAMName, ShapeType{numFeatures}, imageGeomPtr->getId()); + auto* ensembleAMPtr = AttributeMatrix::Create(fixture.ds, k_EnsembleAMName, ShapeType{numEnsembles}, imageGeomPtr->getId()); + + auto featurePhasesStore = DataStoreUtilities::CreateDataStore({numFeatures}, {1}, IDataAction::Mode::Execute); + fixture.featurePhasesPtr = Int32Array::Create(fixture.ds, k_PhasesName, featurePhasesStore, featureAMPtr->getId()); + auto avgQuatsStore = DataStoreUtilities::CreateDataStore({numFeatures}, {4}, IDataAction::Mode::Execute); + fixture.avgQuatsPtr = Float32Array::Create(fixture.ds, k_AvgQuatsName, avgQuatsStore, featureAMPtr->getId()); + auto crystalStructuresStore = DataStoreUtilities::CreateDataStore({numEnsembles}, {1}, IDataAction::Mode::Execute); + fixture.crystalStructuresPtr = UInt32Array::Create(fixture.ds, k_CrystalStructuresName, crystalStructuresStore, ensembleAMPtr->getId()); + + for(usize featureIdx = 0; featureIdx < numFeatures; ++featureIdx) { - (*td.featurePhases)[i] = 1; - (*td.avgQuats)[i * 4 + 0] = 0.0F; // x - (*td.avgQuats)[i * 4 + 1] = 0.0F; // y - (*td.avgQuats)[i * 4 + 2] = 0.0F; // z - (*td.avgQuats)[i * 4 + 3] = 1.0F; // w -> identity + (*fixture.featurePhasesPtr)[featureIdx] = 1; + (*fixture.avgQuatsPtr)[featureIdx * 4 + 0] = 0.0F; // x + (*fixture.avgQuatsPtr)[featureIdx * 4 + 1] = 0.0F; // y + (*fixture.avgQuatsPtr)[featureIdx * 4 + 2] = 0.0F; // z + (*fixture.avgQuatsPtr)[featureIdx * 4 + 3] = 1.0F; // w -> identity } - (*td.featurePhases)[0] = 0; // feature 0 is the sentinel; the algorithm starts its loop at 1 - for(usize e = 0; e < numEnsembles; ++e) + if(numFeatures > 0) { - (*td.crystalStructures)[e] = ebsdlib::CrystalStructure::UnknownCrystalStructure; + (*fixture.featurePhasesPtr)[0] = 0; // feature 0 is the sentinel; the algorithm starts its loop at 1 + } + for(usize ensembleIdx = 0; ensembleIdx < numEnsembles; ++ensembleIdx) + { + (*fixture.crystalStructuresPtr)[ensembleIdx] = ebsdlib::CrystalStructure::UnknownCrystalStructure; } if(numEnsembles > 1) { - (*td.crystalStructures)[1] = ebsdlib::CrystalStructure::Cubic_High; // EbsdLib LaueOps index 1 + (*fixture.crystalStructuresPtr)[1] = ebsdlib::CrystalStructure::Cubic_High; // EbsdLib LaueOps index 1 } - return td; + return fixture; } -void SetQuat(SchmidFixture& td, usize featureIdx, const std::array& q) +void SetQuat(SchmidFixture& fixture, usize featureIdx, const std::array& quat) { - (*td.avgQuats)[featureIdx * 4 + 0] = q[0]; - (*td.avgQuats)[featureIdx * 4 + 1] = q[1]; - (*td.avgQuats)[featureIdx * 4 + 2] = q[2]; - (*td.avgQuats)[featureIdx * 4 + 3] = q[3]; + (*fixture.avgQuatsPtr)[featureIdx * 4 + 0] = quat[0]; + (*fixture.avgQuatsPtr)[featureIdx * 4 + 1] = quat[1]; + (*fixture.avgQuatsPtr)[featureIdx * 4 + 2] = quat[2]; + (*fixture.avgQuatsPtr)[featureIdx * 4 + 3] = quat[3]; } Arguments MakeArgs(const std::vector& loading, bool storeAngleComps, bool overrideSystem, const std::vector& plane = {0.0F, 0.0F, 1.0F}, @@ -175,166 +180,38 @@ Arguments MakeArgs(const std::vector& loading, bool storeAngleComps, bo /** * @brief Asserts the five outputs of one feature against the oracle. Angle components are - * checked only when phis/lambdas are >= 0 (a negative sentinel means "do not check"). + * checked only when phisRef/lambdasRef are >= 0 (a negative sentinel means "do not check"). */ void CheckFeature(const DataStructure& ds, usize featureIdx, float64 expectedM, int32 expectedSlipSystem, const std::array& expectedPoles, float64 expectedPhi, float64 expectedLambda) { REQUIRE_NOTHROW(ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SchmidsArrayName))); REQUIRE_NOTHROW(ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SlipSystemsArrayName))); REQUIRE_NOTHROW(ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PolesArrayName))); - const auto& schmids = ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SchmidsArrayName)); - const auto& slipSystems = ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SlipSystemsArrayName)); - const auto& poles = ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PolesArrayName)); + const auto& schmidsRef = ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SchmidsArrayName)); + const auto& slipSystemsRef = ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SlipSystemsArrayName)); + const auto& polesRef = ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PolesArrayName)); CAPTURE(featureIdx); - CHECK(static_cast(schmids[featureIdx]) == Approx(expectedM).margin(k_Tol)); - CHECK(slipSystems[featureIdx] == expectedSlipSystem); - for(usize k = 0; k < 3; ++k) + CHECK(static_cast(schmidsRef[featureIdx]) == Approx(expectedM).margin(k_Tol)); + CHECK(slipSystemsRef[featureIdx] == expectedSlipSystem); + for(usize compIdx = 0; compIdx < 3; ++compIdx) { - CAPTURE(k); - CHECK(poles[featureIdx * 3 + k] == expectedPoles[k]); + CAPTURE(compIdx); + CHECK(polesRef[featureIdx * 3 + compIdx] == expectedPoles[compIdx]); } if(expectedPhi >= 0.0) { REQUIRE_NOTHROW(ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PhisArrayName))); REQUIRE_NOTHROW(ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_LambdasArrayName))); - const auto& phis = ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PhisArrayName)); - const auto& lambdas = ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_LambdasArrayName)); - CHECK(static_cast(phis[featureIdx]) == Approx(expectedPhi).margin(k_Tol)); - CHECK(static_cast(lambdas[featureIdx]) == Approx(expectedLambda).margin(k_Tol)); + const auto& phisRef = ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PhisArrayName)); + const auto& lambdasRef = ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_LambdasArrayName)); + CHECK(static_cast(phisRef[featureIdx]) == Approx(expectedPhi).margin(k_Tol)); + CHECK(static_cast(lambdasRef[featureIdx]) == Approx(expectedLambda).margin(k_Tol)); } } } // namespace -TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter", "[OrientationAnalysis][ComputeSchmidsFilter]") -{ - UnitTest::LoadPlugins(); - - const nx::core::UnitTest::TestFileSentinel testDataSentinel(nx::core::unit_test::k_TestFilesDir, "6_6_stats_test_v2.tar.gz", "6_6_stats_test_v2.dream3d"); - - // Read the Small IN100 Data set - auto baseDataFilePath = fs::path(fmt::format("{}/6_6_stats_test_v2.dream3d", unit_test::k_TestFilesDir)); - DataStructure dataStructure = UnitTest::LoadDataStructure(baseDataFilePath); - DataPath smallIn100Group({nx::core::Constants::k_DataContainer}); - DataPath cellDataPath = smallIn100Group.createChildPath(nx::core::Constants::k_CellData); - DataPath cellFeatureDataPath({k_DataContainer, k_CellFeatureData}); - DataPath avgQuatsDataPath = cellFeatureDataPath.createChildPath(k_AvgQuats); - DataPath featurePhasesDataPath = cellFeatureDataPath.createChildPath(k_Phases); - - { - // Instantiate the filter, a DataStructure object and an Arguments Object - ComputeSchmidsFilter filter; - Arguments args; - - // Create default Parameters for the filter. - args.insertOrAssign(ComputeSchmidsFilter::k_LoadingDirection_Key, std::make_any(std::vector{1.0F, 1.0F, 1.0F})); - args.insertOrAssign(ComputeSchmidsFilter::k_StoreAngleComponents_Key, std::make_any(true)); - args.insertOrAssign(ComputeSchmidsFilter::k_OverrideSystem_Key, std::make_any(false)); - args.insertOrAssign(ComputeSchmidsFilter::k_SlipPlane_Key, std::make_any(std::vector{0.0F, 0.0F, 1.0F})); - args.insertOrAssign(ComputeSchmidsFilter::k_SlipDirection_Key, std::make_any(std::vector{1.0F, 0.0F, 0.0F})); - - args.insertOrAssign(ComputeSchmidsFilter::k_FeaturePhasesArrayPath_Key, std::make_any(featurePhasesDataPath)); - args.insertOrAssign(ComputeSchmidsFilter::k_AvgQuatsArrayPath_Key, std::make_any(avgQuatsDataPath)); - args.insertOrAssign(ComputeSchmidsFilter::k_CrystalStructuresArrayPath_Key, std::make_any(k_CrystalStructuresArrayPath)); - - args.insertOrAssign(ComputeSchmidsFilter::k_SchmidsArrayName_Key, std::make_any(k_CalculatedArrayPrefix + k_SchmidsArrayName)); - args.insertOrAssign(ComputeSchmidsFilter::k_SlipSystemsArrayName_Key, std::make_any(k_CalculatedArrayPrefix + k_SlipSystemsArrayName)); - args.insertOrAssign(ComputeSchmidsFilter::k_PolesArrayName_Key, std::make_any(k_CalculatedArrayPrefix + k_PolesArrayName)); - args.insertOrAssign(ComputeSchmidsFilter::k_PhisArrayName_Key, std::make_any(k_CalculatedArrayPrefix + k_PhisArrayName)); - args.insertOrAssign(ComputeSchmidsFilter::k_LambdasArrayName_Key, std::make_any(k_CalculatedArrayPrefix + k_LambdasArrayName)); - - // 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); - } - - // The two INTEGER outputs must still match the archive EXACTLY. Both are provably invariant - // under the EbsdLib 3.1.1 normalizer fix: - // * SlipSystems is an argmax over twelve candidates that the fix rescales by a single positive - // factor, so the ranking -- and therefore the winner -- cannot move. - // * Poles is derived from crystalLoading, which the fix does not touch at all. - // Keeping these exact is the check that the fix changed only what it was supposed to. - { - const std::vector comparisonNames = {k_SlipSystemsArrayName, k_PolesArrayName}; - for(const auto& comparisonName : comparisonNames) - { - const DataPath exemplarPath({k_DataContainer, k_CellFeatureData, comparisonName}); - const DataPath calculatedPath({k_DataContainer, k_CellFeatureData, k_CalculatedArrayPrefix + comparisonName}); - const auto& exemplarData = dataStructure.getDataRefAs(exemplarPath); - const auto& calculatedData = dataStructure.getDataRefAs(calculatedPath); - UnitTest::CompareDataArrays(exemplarData, calculatedData, 1); - } - } - - // The three FLOAT outputs no longer match the archive, and they are not supposed to. The archive - // was generated with EbsdLib <= 3.1.0, whose CubicOps::getSchmidFactorAndSS normalized with the - // float literals 1.732f and 1.414f instead of sqrt(3) and sqrt(2). Both literals differ from the - // constants they stand in for, so every archived value carries a KNOWN, UNIFORM, per-component - // inflation factor: - // - // Phis (cos phi, normalized by sqrt(3)): sqrt(3) / 1.732f = 1.0000293385 - // Lambdas (cos lambda, normalized by sqrt(2)): sqrt(2) / 1.414f = 1.0001510099 - // Schmids (their product) : sqrt(6) / (1.732f*1.414f) = 1.0001803528 - // - // Rather than retire the comparison -- which would throw away 214 real regression checks -- or - // loosen it into meaninglessness, assert the RELATIONSHIP. Every archived value must equal the - // freshly computed value times its predicted factor. That is a strictly STRONGER statement than - // the equality it replaces: it pins both the new values and the exact size and shape of the - // change, and it fails if the fix perturbed anything the bias analysis did not account for. - // - // Recompute these expectations (and regenerate the archive) if the exemplar is ever rebuilt - // against EbsdLib >= 3.1.1, at which point the factors all become 1.0. - { - const std::vector> biasedComparisons = { - {k_SchmidsArrayName, 1.0001803528351113}, - {k_PhisArrayName, 1.0000293384785181}, - {k_LambdasArrayName, 1.0001510099261918}, - }; - for(const auto& [comparisonName, biasFactor] : biasedComparisons) - { - const DataPath exemplarPath({k_DataContainer, k_CellFeatureData, comparisonName}); - const DataPath calculatedPath({k_DataContainer, k_CellFeatureData, k_CalculatedArrayPrefix + comparisonName}); - REQUIRE_NOTHROW(dataStructure.getDataRefAs(exemplarPath)); - REQUIRE_NOTHROW(dataStructure.getDataRefAs(calculatedPath)); - const auto& exemplarData = dataStructure.getDataRefAs(exemplarPath); - const auto& calculatedData = dataStructure.getDataRefAs(calculatedPath); - REQUIRE(exemplarData.getSize() == calculatedData.getSize()); - - usize mismatches = 0; - for(usize i = 1; i < calculatedData.getSize(); i++) - { - const float64 expected = static_cast(calculatedData[i]) * biasFactor; - const float64 archived = static_cast(exemplarData[i]); - // Absolute margin covers the float32 quantization of both operands near zero; the relative - // epsilon covers it away from zero. - if(archived != Approx(expected).margin(2.0e-7).epsilon(1.0e-6)) - { - CAPTURE(comparisonName, i, archived, expected, biasFactor); - CHECK(archived == Approx(expected).margin(2.0e-7).epsilon(1.0e-6)); - if(++mismatches >= 5) - { - break; - } - } - } - CAPTURE(comparisonName); - CHECK(mismatches == 0); - } - } - -// Write the DataStructure out to the file system -#ifdef SIMPLNX_WRITE_TEST_OUTPUT - WriteTestDataStructure(dataStructure, fs::path(fmt::format("{}/find_schmids.dream3d", unit_test::k_BinaryTestOutputDir))); -#endif - - UnitTest::CheckArraysInheritTupleDims(dataStructure); -} - // ----------------------------------------------------------------------------- // Class 1 (Analytical): auto slip-system path, identity quaternion, Cubic_High. // @@ -406,39 +283,42 @@ TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: Class 1 analytical oracle, { DYNAMIC_SECTION("loading " << row.label) { - SchmidFixture td = MakeFixture(2); + SchmidFixture fixture = MakeFixture(2); ComputeSchmidsFilter filter; Arguments args = MakeArgs(row.loading, true, false); - auto preflightResult = filter.preflight(td.ds, args); + auto preflightResult = filter.preflight(fixture.ds, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); - auto executeResult = filter.execute(td.ds, args); + auto executeResult = filter.execute(fixture.ds, args); SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); - CheckFeature(td.ds, 1, row.m, row.slipSystem, row.poles, row.cosPhi, row.cosLambda); + CheckFeature(fixture.ds, 1, row.m, row.slipSystem, row.poles, row.cosPhi, row.cosLambda); // Class 4 invariants. - const auto& schmids = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SchmidsArrayName)); - const auto& phis = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PhisArrayName)); - const auto& lambdas = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_LambdasArrayName)); + REQUIRE_NOTHROW(fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SchmidsArrayName))); + REQUIRE_NOTHROW(fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PhisArrayName))); + REQUIRE_NOTHROW(fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_LambdasArrayName))); + const auto& schmidsRef = fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SchmidsArrayName)); + const auto& phisRef = fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PhisArrayName)); + const auto& lambdasRef = fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_LambdasArrayName)); // (a) The Schmid factor of a cubic crystal cannot exceed 0.5 and cannot be negative. // EbsdLib <= 3.1.0 violated the upper bound (max 0.500090176); this assertion is only // satisfiable against the fixed normalizers. - CHECK(schmids[1] >= 0.0F); - CHECK(schmids[1] <= 0.5F); + CHECK(schmidsRef[1] >= 0.0F); + CHECK(schmidsRef[1] <= 0.5F); // (b) On the auto path angleComps are COSINES, so m must be their product exactly. - CHECK(static_cast(schmids[1]) == Approx(static_cast(phis[1]) * static_cast(lambdas[1])).margin(k_Tol)); + CHECK(static_cast(schmidsRef[1]) == Approx(static_cast(phisRef[1]) * static_cast(lambdasRef[1])).margin(k_Tol)); // (c) Both cosines are direction cosines: in [0, 1]. - CHECK(phis[1] >= 0.0F); - CHECK(phis[1] <= 1.0F); - CHECK(lambdas[1] >= 0.0F); - CHECK(lambdas[1] <= 1.0F); + CHECK(phisRef[1] >= 0.0F); + CHECK(phisRef[1] <= 1.0F); + CHECK(lambdasRef[1] >= 0.0F); + CHECK(lambdasRef[1] <= 1.0F); // Feature 0 is the sentinel slot; the algorithm writes explicit zeros there. - CheckFeature(td.ds, 0, 0.0, 0, {0, 0, 0}, 0.0, 0.0); + CheckFeature(fixture.ds, 0, 0.0, 0, {0, 0, 0}, 0.0, 0.0); - UnitTest::CheckArraysInheritTupleDims(td.ds); + UnitTest::CheckArraysInheritTupleDims(fixture.ds); } } } @@ -510,23 +390,24 @@ TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: orientation-matrix convent { DYNAMIC_SECTION("quat " << row.label) { - SchmidFixture td = MakeFixture(2); - SetQuat(td, 1, row.quat); + SchmidFixture fixture = MakeFixture(2); + SetQuat(fixture, 1, row.quat); ComputeSchmidsFilter filter; Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, true, false); - auto preflightResult = filter.preflight(td.ds, args); + auto preflightResult = filter.preflight(fixture.ds, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); - auto executeResult = filter.execute(td.ds, args); + auto executeResult = filter.execute(fixture.ds, args); SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); - CheckFeature(td.ds, 1, k_ExpectedM, row.slipSystem, row.poles, k_ExpectedPhi, k_ExpectedLambda); + CheckFeature(fixture.ds, 1, k_ExpectedM, row.slipSystem, row.poles, k_ExpectedPhi, k_ExpectedLambda); - const auto& schmids = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SchmidsArrayName)); - CHECK(schmids[1] <= 0.5F); + REQUIRE_NOTHROW(fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SchmidsArrayName))); + const auto& schmidsRef = fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SchmidsArrayName)); + CHECK(schmidsRef[1] <= 0.5F); - UnitTest::CheckArraysInheritTupleDims(td.ds); + UnitTest::CheckArraysInheritTupleDims(fixture.ds); } } } @@ -600,41 +481,44 @@ TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: override slip system path" { DYNAMIC_SECTION("loading " << row.label) { - SchmidFixture td = MakeFixture(2); + SchmidFixture fixture = MakeFixture(2); ComputeSchmidsFilter filter; Arguments args = MakeArgs(row.loading, true, true, {0.0F, 0.0F, 1.0F}, {1.0F, 0.0F, 0.0F}); - auto preflightResult = filter.preflight(td.ds, args); + auto preflightResult = filter.preflight(fixture.ds, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); - auto executeResult = filter.execute(td.ds, args); + auto executeResult = filter.execute(fixture.ds, args); SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); - CheckFeature(td.ds, 1, row.m, row.symOpIndex, row.poles, row.phiRadians, row.lambdaRadians); + CheckFeature(fixture.ds, 1, row.m, row.symOpIndex, row.poles, row.phiRadians, row.lambdaRadians); // Class 4: on the override path m must equal cos(Phis) * cos(Lambdas) because the stored // components are the ANGLES, not their cosines. This is the assertion that proves the SC-3 // unit flip is real rather than a documentation misreading. - const auto& schmids = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SchmidsArrayName)); - const auto& phis = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PhisArrayName)); - const auto& lambdas = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_LambdasArrayName)); + REQUIRE_NOTHROW(fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SchmidsArrayName))); + REQUIRE_NOTHROW(fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PhisArrayName))); + REQUIRE_NOTHROW(fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_LambdasArrayName))); + const auto& schmidsRef = fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SchmidsArrayName)); + const auto& phisRef = fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PhisArrayName)); + const auto& lambdasRef = fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_LambdasArrayName)); if(row.m > 0.0) { - CHECK(static_cast(schmids[1]) == Approx(std::cos(static_cast(phis[1])) * std::cos(static_cast(lambdas[1]))).margin(k_Tol)); + CHECK(static_cast(schmidsRef[1]) == Approx(std::cos(static_cast(phisRef[1])) * std::cos(static_cast(lambdasRef[1]))).margin(k_Tol)); } - CHECK(schmids[1] <= 0.5F); + CHECK(schmidsRef[1] <= 0.5F); - UnitTest::CheckArraysInheritTupleDims(td.ds); + UnitTest::CheckArraysInheritTupleDims(fixture.ds); } } SECTION("preflight rejects a slip direction that is not in the slip plane") { - SchmidFixture td = MakeFixture(2); + SchmidFixture fixture = MakeFixture(2); ComputeSchmidsFilter filter; // (0,0,1) . (1,0,1) == 1 != 0 Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, true, true, {0.0F, 0.0F, 1.0F}, {1.0F, 0.0F, 1.0F}); - auto preflightResult = filter.preflight(td.ds, args); + auto preflightResult = filter.preflight(fixture.ds, args); SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); REQUIRE(preflightResult.outputActions.errors().size() == 1); CHECK(preflightResult.outputActions.errors()[0].code == -13500); @@ -650,7 +534,7 @@ TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: override slip system path" // entirely on how the store was initialized. The fixture below is large and dirties the heap // first, which is what settled how much of that exposure is real -- see the note inside it. // -// SC-4: `crystalStructures[featurePhases[i]]` (ComputeSchmids.cpp:82) is unbounded. A phase id +// SC-4: `crystalStructures[featurePhases[featureIdx]]` was unbounded. A phase id // beyond the ensemble count, or a negative one, reads out of bounds. // ----------------------------------------------------------------------------- TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: options, skip path and phase guards", "[OrientationAnalysis][ComputeSchmidsFilter][VV]") @@ -659,87 +543,47 @@ TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: options, skip path and pha SECTION("StoreAngleComponents == false does not create Phis/Lambdas") { - SchmidFixture td = MakeFixture(2); + SchmidFixture fixture = MakeFixture(2); ComputeSchmidsFilter filter; Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, false, false); - auto preflightResult = filter.preflight(td.ds, args); + auto preflightResult = filter.preflight(fixture.ds, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); - auto executeResult = filter.execute(td.ds, args); + auto executeResult = filter.execute(fixture.ds, args); SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); // The three unconditional outputs are unchanged by the toggle. - CheckFeature(td.ds, 1, 0.4665694748158435, 10, {26, 53, 80}, -1.0, -1.0); - CHECK(td.ds.getDataAs(k_FeatureAMPath.createChildPath(k_PhisArrayName)) == nullptr); - CHECK(td.ds.getDataAs(k_FeatureAMPath.createChildPath(k_LambdasArrayName)) == nullptr); + CheckFeature(fixture.ds, 1, 0.4665694748158435, 10, {26, 53, 80}, -1.0, -1.0); + CHECK(fixture.ds.getDataAs(k_FeatureAMPath.createChildPath(k_PhisArrayName)) == nullptr); + CHECK(fixture.ds.getDataAs(k_FeatureAMPath.createChildPath(k_LambdasArrayName)) == nullptr); - UnitTest::CheckArraysInheritTupleDims(td.ds); + UnitTest::CheckArraysInheritTupleDims(fixture.ds); } - SECTION("features whose Laue class is beyond LaueGroupEnd get defined zeros, not stale memory") + SECTION("Features whose Laue class is beyond LaueGroupEnd get defined zeros") { // Ensemble slots: 0 = 999 sentinel, 1 = Cubic_High, 2 = 999 sentinel (>= LaueGroupEnd == 11). // Feature 1 uses phase 1 (computed); every other feature uses phase 2 (skipped by the - // continue), so all of their output rows are ones the algorithm never writes. - // - // WHY THE FIXTURE IS LARGE, AND WHAT THAT SETTLED. The claim SC-1 originally rested on was - // that without an explicit fill the skipped rows hold indeterminate memory. Removing the - // explicit "0" fill from all five CreateArrayAction calls and running THIS fixture -- 20000 - // features, so every output array is 78 KB or more, with same-size blocks poisoned to 0xAB - // and freed immediately before the run -- still yields all zeros. That is not the allocator - // being kind: a standalone probe confirms that on this platform `new float[20000]` after a - // same-size dirty free does return 0xABABABAB, whereas the 8-to-36-byte allocations a - // three-feature fixture uses come from libmalloc's nano zone as freshly zeroed pages. So the - // zeros here are written by SIMPLNX, not inherited from the heap, and the mechanism is in the - // store factory rather than in the action: CoreDataIOManager::addDataStoreFnc() constructs - // every in-core store with `static_cast(0)` as its initValue, and DataStore's constructor - // unconditionally `std::fill_n`s the buffer with it (DataStore.hpp:66-69). The fillValue - // string on CreateArrayAction only adds a second, redundant fill. - // - // The explicit fill is therefore DEFENSIVE, not a repair of a live in-core defect: it - // restores what legacy SIMPL FindSchmids stated explicitly, and it is the only guarantee for - // any store implementation whose factory does not hard-code a zero initValue. This section - // is the regression lock on the guarantee, and the large dirty-heap fixture is what makes it - // a non-vacuous one -- at this size the assertion would catch a store that stopped filling. + // continue), so the explicit CreateArrayAction fill must define every skipped output row. constexpr usize k_FeatureCount = 20000; - SchmidFixture td = MakeFixture(k_FeatureCount, 3); - for(usize i = 2; i < k_FeatureCount; ++i) + SchmidFixture fixture = MakeFixture(k_FeatureCount, 3); + for(usize featureIdx = 2; featureIdx < k_FeatureCount; ++featureIdx) { - (*td.featurePhases)[i] = 2; + (*fixture.featurePhasesPtr)[featureIdx] = 2; } ComputeSchmidsFilter filter; Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, true, false); - auto preflightResult = filter.preflight(td.ds, args); + auto preflightResult = filter.preflight(fixture.ds, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); - // Dirty the heap with blocks the same size as the five output arrays and free them - // immediately before the run, so that a default-initialized `new value_type[n]` of that size - // is served from a recycled block that still holds the poison. - { - std::vector dirtyBlocks; - for(int32 repetition = 0; repetition < 8; ++repetition) - { - for(usize byteCount : {k_FeatureCount * sizeof(float32), k_FeatureCount * 3 * sizeof(int32)}) - { - char* block = new char[byteCount]; - std::memset(block, 0xAB, byteCount); - dirtyBlocks.push_back(block); - } - } - for(char* block : dirtyBlocks) - { - delete[] block; - } - } - - auto executeResult = filter.execute(td.ds, args); + auto executeResult = filter.execute(fixture.ds, args); SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); // Feature 1 still gets the full oracle answer. - CheckFeature(td.ds, 1, 0.4665694748158435, 10, {26, 53, 80}, 0.6172133998483701, 0.7559289460184544); + CheckFeature(fixture.ds, 1, 0.4665694748158435, 10, {26, 53, 80}, 0.6172133998483701, 0.7559289460184544); // Feature 2 was skipped: every output must be a defined zero. - CheckFeature(td.ds, 2, 0.0, 0, {0, 0, 0}, 0.0, 0.0); + CheckFeature(fixture.ds, 2, 0.0, 0, {0, 0, 0}, 0.0, 0.0); // ...and so must every other row the algorithm never writes: feature 0 (the sentinel) and // features 2 through 19999. Aggregated into one CHECK per data type so the section stays a @@ -747,12 +591,12 @@ TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: options, skip path and pha usize nonZeroFloatValues = 0; for(const auto& arrayName : {k_SchmidsArrayName, k_PhisArrayName, k_LambdasArrayName}) { - REQUIRE_NOTHROW(td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(arrayName))); - const auto& arrayRef = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(arrayName)); + REQUIRE_NOTHROW(fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(arrayName))); + const auto& arrayRef = fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(arrayName)); REQUIRE(arrayRef.getSize() == k_FeatureCount); - for(usize i = 0; i < k_FeatureCount; ++i) + for(usize featureIdx = 0; featureIdx < k_FeatureCount; ++featureIdx) { - if(i != 1 && arrayRef[i] != 0.0F) + if(featureIdx != 1 && arrayRef[featureIdx] != 0.0F) { ++nonZeroFloatValues; } @@ -762,12 +606,12 @@ TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: options, skip path and pha usize nonZeroSlipSystemValues = 0; { - REQUIRE_NOTHROW(td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SlipSystemsArrayName))); - const auto& slipSystemsRef = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SlipSystemsArrayName)); + REQUIRE_NOTHROW(fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SlipSystemsArrayName))); + const auto& slipSystemsRef = fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_SlipSystemsArrayName)); REQUIRE(slipSystemsRef.getSize() == k_FeatureCount); - for(usize i = 0; i < k_FeatureCount; ++i) + for(usize featureIdx = 0; featureIdx < k_FeatureCount; ++featureIdx) { - if(i != 1 && slipSystemsRef[i] != 0) + if(featureIdx != 1 && slipSystemsRef[featureIdx] != 0) { ++nonZeroSlipSystemValues; } @@ -777,12 +621,12 @@ TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: options, skip path and pha usize nonZeroPoleValues = 0; { - REQUIRE_NOTHROW(td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PolesArrayName))); - const auto& polesRef = td.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PolesArrayName)); + REQUIRE_NOTHROW(fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PolesArrayName))); + const auto& polesRef = fixture.ds.getDataRefAs(k_FeatureAMPath.createChildPath(k_PolesArrayName)); REQUIRE(polesRef.getSize() == k_FeatureCount * 3); - for(usize i = 0; i < k_FeatureCount * 3; ++i) + for(usize compIdx = 0; compIdx < k_FeatureCount * 3; ++compIdx) { - if((i / 3) != 1 && polesRef[i] != 0) + if((compIdx / 3) != 1 && polesRef[compIdx] != 0) { ++nonZeroPoleValues; } @@ -790,19 +634,36 @@ TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: options, skip path and pha } CHECK(nonZeroPoleValues == 0); - UnitTest::CheckArraysInheritTupleDims(td.ds); + UnitTest::CheckArraysInheritTupleDims(fixture.ds); + } + + SECTION("Valid Laue classes without slip systems keep zero Schmid outputs and computed Poles") + { + SchmidFixture fixture = MakeFixture(3, 3); + (*fixture.featurePhasesPtr)[2] = 2; + (*fixture.crystalStructuresPtr)[2] = ebsdlib::CrystalStructure::Triclinic; + + ComputeSchmidsFilter filter; + Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, true, false); + auto preflightResult = filter.preflight(fixture.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); + auto executeResult = filter.execute(fixture.ds, args); + SIMPLNX_RESULT_REQUIRE_VALID(executeResult.result); + + CheckFeature(fixture.ds, 2, 0.0, 0, {26, 53, 80}, 0.0, 0.0); + UnitTest::CheckArraysInheritTupleDims(fixture.ds); } SECTION("a phase id beyond the ensemble count is an error, not an out-of-bounds read") { - SchmidFixture td = MakeFixture(2); // 2 ensemble slots -> valid phase ids are 0 and 1 - (*td.featurePhases)[1] = 7; + SchmidFixture fixture = MakeFixture(2); // 2 ensemble slots -> valid phase ids are 0 and 1 + (*fixture.featurePhasesPtr)[1] = 7; ComputeSchmidsFilter filter; Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, true, false); - auto preflightResult = filter.preflight(td.ds, args); + auto preflightResult = filter.preflight(fixture.ds, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); - auto executeResult = filter.execute(td.ds, args); + auto executeResult = filter.execute(fixture.ds, args); SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); REQUIRE(executeResult.result.errors().size() == 1); CHECK(executeResult.result.errors()[0].code == -13501); @@ -813,14 +674,14 @@ TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: options, skip path and pha SECTION("a negative phase id is an error, not an out-of-bounds read") { - SchmidFixture td = MakeFixture(2); - (*td.featurePhases)[1] = -3; + SchmidFixture fixture = MakeFixture(2); + (*fixture.featurePhasesPtr)[1] = -3; ComputeSchmidsFilter filter; Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, true, false); - auto preflightResult = filter.preflight(td.ds, args); + auto preflightResult = filter.preflight(fixture.ds, args); SIMPLNX_RESULT_REQUIRE_VALID(preflightResult.outputActions); - auto executeResult = filter.execute(td.ds, args); + auto executeResult = filter.execute(fixture.ds, args); SIMPLNX_RESULT_REQUIRE_INVALID(executeResult.result); REQUIRE(executeResult.result.errors().size() == 1); CHECK(executeResult.result.errors()[0].code == -13502); @@ -828,6 +689,98 @@ TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: options, skip path and pha } } +TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: preflight input validation", "[OrientationAnalysis][ComputeSchmidsFilter][VV][preflight]") +{ + UnitTest::LoadPlugins(); + + SECTION("Feature Phases and Average Quaternions must have equal tuple counts") + { + DataStructure dataStructure; + auto* imageGeomPtr = ImageGeom::Create(dataStructure, k_GeomName); + imageGeomPtr->setDimensions({1, 1, 1}); + + auto* phasesAMPtr = AttributeMatrix::Create(dataStructure, "Phases Data", ShapeType{2}, imageGeomPtr->getId()); + auto featurePhasesStore = DataStoreUtilities::CreateDataStore({2}, {1}, IDataAction::Mode::Execute); + Int32Array::Create(dataStructure, k_PhasesName, featurePhasesStore, phasesAMPtr->getId()); + auto* quatsAMPtr = AttributeMatrix::Create(dataStructure, "Quaternions Data", ShapeType{6}, imageGeomPtr->getId()); + auto avgQuatsStore = DataStoreUtilities::CreateDataStore({6}, {4}, IDataAction::Mode::Execute); + Float32Array::Create(dataStructure, k_AvgQuatsName, avgQuatsStore, quatsAMPtr->getId()); + auto* ensembleAMPtr = AttributeMatrix::Create(dataStructure, k_EnsembleAMName, ShapeType{2}, imageGeomPtr->getId()); + auto crystalStructuresStore = DataStoreUtilities::CreateDataStore({2}, {1}, IDataAction::Mode::Execute); + UInt32Array::Create(dataStructure, k_CrystalStructuresName, crystalStructuresStore, ensembleAMPtr->getId()); + + ComputeSchmidsFilter filter; + Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, true, false); + args.insertOrAssign(ComputeSchmidsFilter::k_FeaturePhasesArrayPath_Key, std::make_any(DataPath({k_GeomName, "Phases Data", k_PhasesName}))); + args.insertOrAssign(ComputeSchmidsFilter::k_AvgQuatsArrayPath_Key, std::make_any(DataPath({k_GeomName, "Quaternions Data", k_AvgQuatsName}))); + + auto preflightResult = filter.preflight(dataStructure, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + CHECK(preflightResult.outputActions.errors()[0].code == -13503); + } + + SECTION("Feature arrays must contain the sentinel tuple") + { + SchmidFixture fixture = MakeFixture(0); + ComputeSchmidsFilter filter; + Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, true, false); + + auto preflightResult = filter.preflight(fixture.ds, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + CHECK(preflightResult.outputActions.errors()[0].code == -13504); + } + + SECTION("Crystal Structures must contain at least one ensemble") + { + SchmidFixture fixture = MakeFixture(2, 0); + ComputeSchmidsFilter filter; + Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, true, false); + + auto preflightResult = filter.preflight(fixture.ds, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + CHECK(preflightResult.outputActions.errors()[0].code == -13505); + } + + SECTION("Loading Direction must be non-zero") + { + SchmidFixture fixture = MakeFixture(2); + ComputeSchmidsFilter filter; + Arguments args = MakeArgs({0.0F, 0.0F, 0.0F}, true, false); + + auto preflightResult = filter.preflight(fixture.ds, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + CHECK(preflightResult.outputActions.errors()[0].code == -13506); + } + + SECTION("Override Slip Plane must be non-zero") + { + SchmidFixture fixture = MakeFixture(2); + ComputeSchmidsFilter filter; + Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, true, true, {0.0F, 0.0F, 0.0F}, {1.0F, 0.0F, 0.0F}); + + auto preflightResult = filter.preflight(fixture.ds, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + CHECK(preflightResult.outputActions.errors()[0].code == -13507); + } + + SECTION("Override Slip Direction must be non-zero") + { + SchmidFixture fixture = MakeFixture(2); + ComputeSchmidsFilter filter; + Arguments args = MakeArgs({1.0F, 2.0F, 3.0F}, true, true, {0.0F, 0.0F, 1.0F}, {0.0F, 0.0F, 0.0F}); + + auto preflightResult = filter.preflight(fixture.ds, args); + SIMPLNX_RESULT_REQUIRE_INVALID(preflightResult.outputActions); + REQUIRE(preflightResult.outputActions.errors().size() == 1); + CHECK(preflightResult.outputActions.errors()[0].code == -13508); + } +} + TEST_CASE("OrientationAnalysis::ComputeSchmidsFilter: SIMPL Backwards Compatibility", "[OrientationAnalysis][ComputeSchmidsFilter][BackwardsCompatibility]") { auto app = Application::GetOrCreateInstance(); diff --git a/src/Plugins/OrientationAnalysis/vv/ComputeSchmidsFilter.md b/src/Plugins/OrientationAnalysis/vv/ComputeSchmidsFilter.md index c158cc4173..a80e43f6c6 100644 --- a/src/Plugins/OrientationAnalysis/vv/ComputeSchmidsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/ComputeSchmidsFilter.md @@ -7,43 +7,40 @@ | DREAM3D 6.5.171 equivalent | `FindSchmids` (SIMPL UUID `e67ca06a-176f-58fc-a676-d6ee5553511a`) - `Source/Plugins/OrientationAnalysis/OrientationAnalysisFilters/FindSchmids.{h,cpp}` | | Verified commit | ** | | Status | READY FOR REVIEW | -| Sign-off | Delegated to the PR reviewer (requester decision, 2026-08-18). Second-engineer oracle review is the PR review. | +| Sign-off | Pending second-engineer PR review. | ## At a glance | Aspect | Current state | |---|---| -| Algorithm Relationship | Port with one port-time regression, and that one latent. The SIMPL/EbsdLib API changed and the arithmetic moved from `float` to `double`, but the control flow is line-for-line the legacy filter. What was lost in the port is the legacy `initValue 0` on the created arrays (SC-1) - real as a source-level regression, but never observable in-core because the store factory hard-codes a zero `initValue` of its own; see *Mutation verification*. A second defect, the unbounded phase-id index, is shared with legacy (SC-4). | -| Oracle (confirmed) | Class 1 (Analytical) - **12 value-asserting fixtures**: 5 auto-path loading directions, 4 exactly-representable quaternions, 3 override-path cases. Plus Class 4 (Invariant) checks for the physical bound `0 <= m <= 0.5`, `m == Phis * Lambdas`, `Poles == trunc(100 * crystalLoading)`, scale invariance and cubic-symmetry invariance, and 5 further non-value fixtures (the `StoreAngleComponents` toggle, the `LaueGroupEnd` skip row, the two phase-id guards and the non-normal slip-direction rejection). Cross-checked at 80 significant digits by `ww_work/ComputeSchmids/oracle.py`, except the two override-path `acos` values, which are transcendental and were instead recomputed to 60 significant digits and rounded to the nearest `double`. | -| Code paths enumerated | 11 of 12 paths exercised - only the cancel path is untested. | -| Tests today | 6 test cases (**601 assertions**, measured): 1 archive-consuming exemplar test, 3 new Class 1 / Class 4 test cases, 1 new options-and-guards test case, 1 SIMPL backwards-compatibility test. | -| Exemplar archive | `6_6_stats_test_v2.tar.gz` (unchanged, not regenerated). Its three float Schmid arrays were generated with EbsdLib <= 3.1.0 and are now stale by a known factor; the test asserts that exact relationship instead of equality. See *Exemplar archive*. | -| Legacy comparison | Run 2026-08-20 against DREAM3D 6.5.171 `PipelineRunner` on 10 hand-built fixtures (9 executing + 1 negative). 93 predicted relationships confirmed, 0 unexplained differences, 1 prediction amended after adjudication (D5). | -| Bug flags | SC-1 (`ComputeSchmidsFilter-D2`, fixed defensively - latent in-core, see *Mutation verification*), SC-2 (`-D1`, fixed in EbsdLib), SC-4 (fixed, no deviation - shared gap now guarded), SC-5 (`-D3`, fixed in EbsdLib + mitigated in NX; twelve further SC-2-class hexagonal divisors recorded there as **known-open**). | -| V&V phase | Phases 1-13 complete. Outstanding: OOC build run (waived, requester decision 2026-08-18); second-engineer sign-off (delegated to the PR reviewer, requester decision 2026-08-18). **This PR is merge-blocked until EbsdLib 3.1.1 is released** - see *Merge dependency*. | +| Algorithm Relationship | Port with with bug fixes. See 'Dependency' section | +| Oracle (confirmed) | Class 1 (Analytical) - **12 value-asserting fixtures**: 5 auto-path loading directions, 4 exactly-representable quaternions, and 3 override-path cases. Class 4 assertions cover the physical bound `0 <= m <= 0.5`, angle-component relationships, fixed-point `Poles`, scale invariance, and cubic-symmetry invariance. The fixtures are inline and independent of archived filter output. | +| Code paths enumerated | 20 of 21 paths exercised - only the cancel path is untested. | +| Tests today | 6 inline test cases (**598 assertions**, measured): 3 Class 1 / Class 4 oracle cases, 1 options-and-guards case, 1 preflight-validation case, and 1 SIMPL backwards-compatibility case. | +| Exemplar archive | **None for this filter.** The circular `6_6_stats_test_v2.tar.gz` comparison was retired. | +| Legacy comparison | **Run — SIMPLNX vs DREAM3D 6.5.171; independently repeated during review.** One shared six-Feature input was exercised through 10 original pipeline pairs plus 3 zero-vector validation pairs. The independent rerun confirmed **291/291 expected relationships**: auto-path floats matched the exact D1 bias factors, auto-path `SlipSystems` and `Poles` were bit-identical, override-path floats agreed within `1.2e-7` relative, and D5 was traced to equivalent symmetry tables with different ordering and tied maxima. 6 deviations: **D1** truncated cubic normalizers; **D2** legacy `-301` skipped-row sentinel; **D3** undefined library outputs; **D4** mode-dependent angle units, names, and error reporting; **D5** table-relative override index; **D6** malformed-input validation. See the deviations document for root-cause detail. | +| Bug flags | **Four bugs resolved in SIMPLNX or EbsdLib:** `ComputeSchmidsFilter-D1` (truncated cubic normalizers), `ComputeSchmidsFilter-D2` (legacy `-301` skipped-row initialization), `ComputeSchmidsFilter-D3` (undefined orientation-library outputs), and `ComputeSchmidsFilter-D6` (missing malformed-input validation). | +| V&V phase | **COMPLETE** | ## Summary -`ComputeSchmidsFilter` computes, for each Feature, the largest Schmid factor over the slip systems of that Feature's crystal structure, given a sample-frame loading direction and the Feature's average orientation. Verification used a Class 1 analytical oracle: the Schmid factor, the two direction cosines, the winning slip-system index and the fixed-point `Poles` encoding were all derived by hand from the algorithm source for identity and three exactly-representable rotated quaternions, then cross-checked in exact arithmetic. The cycle found and fixed four defects — two of them inside EbsdLib — and documented five deviations from DREAM3D 6.5.171, of which four were predicted from source before the comparison was run. +`ComputeSchmidsFilter` computes, for each Feature, the largest Schmid factor over the slip systems of that Feature's crystal structure, given a sample-frame loading direction and the Feature's average orientation. Verification uses inline Class 1 analytical fixtures plus Class 4 invariants; the circular production-scale exemplar was retired. The cycle found the original four defects plus two input-validation gaps during adversarial review and documents six deviations from DREAM3D 6.5.171. -## Merge dependency +## Dependency state -This branch bumps `vcpkg.json`'s `ebsdlib` constraint from `"version>=": "3.1.0"` to `"3.1.1"` because two of the four fixes live in EbsdLib (`topic/3_1_1_staging`, commits `4a56725` and `2c84f2a`) and the Class 1 oracle asserts the post-fix values to `1e-6`. Against EbsdLib 3.1.0 the new fixtures fail by roughly `7.4e-5`, which is the whole point of the fix. - -**Consequence, accepted by the requester on 2026-08-20:** the PR cannot pass stock CI until EbsdLib 3.1.1 is published to the vcpkg registry. Until then `vcpkg` rejects the manifest with `no version database entry for ebsdlib at 3.1.1` at *configure* time, in every preset — including the local `SIMPLNX_USE_LOCAL_EBSD_LIB=ON` preset, because the manifest's version constraints are resolved regardless of which features are enabled. All of the verification in this report was therefore performed with the bump **not** applied, against the local EbsdLib source through `NX-Com-Qt69-Vtk96-Rel-EbsdLib`, and the bump was applied last as a manifest-only change. Do not reconfigure any build directory on this branch until 3.1.1 exists. +- EbsdLib version 3.1.1 ## Algorithm Relationship -**Port**, with one port-time regression, and that one latent. +**Port with minor algorithm improvements and updates for the SIMPLNX API.** + +*Evidence:* `ComputeSchmids::operator()` preserves the legacy `FindSchmids::execute()` feature iteration, loading-direction transformation, crystal-structure dispatch, and automatic/override slip-system calculations. -`ComputeSchmids::operator()` is a statement-for-statement transcription of `FindSchmids::execute()`. Port-time deltas: +Port-time updates: -1. **`float` to `double`.** Legacy holds the orientation matrix, the crystal-frame loading direction, the Schmid factor and the angle components in `float`; NX computes all of them in `double` and narrows only on store. Output arrays are `float32` in both. This tightens accuracy and does not change any decision — confirmed empirically, see D1. -2. **`qu2om` and the quaternion layout are unchanged.** EbsdLib's `Quaternion::toOrientationMatrix()` is line-identical to legacy's `qu2om`, the component order is `(x, y, z, w)` in both, and `epsijk == +1` (`OrientationFwd.hpp` defines `DREAM3D_PASSIVE_ROTATION`), so neither applies a transpose. The A/B run confirms this: `Poles`, which is a direct function of the orientation matrix, is bit-identical on every fixture. -3. **The `initValue` was dropped.** Legacy creates `Schmids`, `SlipSystems` and `Poles` with `createNonPrereqArrayFromPath(..., 0, ...)`. NX's `CreateArrayAction` calls passed no fill value. **This is SC-1, a port regression**, fixed here — but the exposure it carried turned out to be nil in-core: the store factory supplies its own zero `initValue` and `DataStore`'s constructor always applies it, so the rows read zero either way. Established by experiment, not assumed; see *Mutation verification*. -4. **The skip condition was inverted in form but not in meaning.** Legacy wraps the body in `if(xtal < LaueGroupEnd)`; NX uses `if(laueClass >= LaueGroupEnd) continue;`. Equivalent. -5. **Loop-scoped versus hoisted locals.** Both hoist `schmid`, `angleComps` and `ss` outside the Feature loop. Combined with EbsdLib Laue ops that do not write every output (SC-5), this leaks the previous Feature's angle components into a skipped Feature's row. NX now declares them inside the loop. -6. **Error code renumbered.** The non-normal slip plane/direction rejection is `-1001` in legacy and `-13500` in NX. Same condition, same message text, both at preflight. See D4. +1. **Higher-precision calculations.** Legacy holds the orientation matrix, crystal-frame loading direction, Schmid factor, and angle components in `float`; SIMPLNX computes them in `double` and narrows only when storing the `float32` outputs. This improves numerical accuracy without changing the selected slip system. +2. **SIMPLNX API updates and input validation.** Selection parameters validate the required arrays, output actions declare every created array, and preflight/execute guards reject mismatched tuple counts, empty arrays, invalid phase indices, and zero-length input vectors before unsafe access or normalization. +3. **Improved error messages.** SIMPLNX identifies the offending array paths, tuple counts, vector components, phase values, and valid ranges so users can correct invalid inputs directly. **Material PRs since baseline:** none identified for this filter. @@ -102,110 +99,92 @@ Class 4 invariants asserted alongside the Class 1 values: ## Bugs found and fixed -| ID | Where | Defect | Fix | +This branch fixes all defects in this table. The fixes will be in the DREAM3D-NX release after v7.4.1. + +| Deviation | Defect | Affected released versions | Resolution in this branch | |---|---|---|---| -| SC-1 | simplnx, latent port regression | All five output arrays created with `CreateArrayAction`'s default empty fill value where legacy allocated with `initValue 0`. Feature 0 and any Feature whose Laue class has no slip systems are never written by the algorithm, so those rows depend entirely on how the store was initialized — and the filter was stating no requirement about that. Measured exposure in-core: **none**, because `CoreDataIOManager` hands every in-core `DataStore` a zero `initValue` of its own. | Explicit `"0"` fill on all five `CreateArrayAction` calls, plus an explicit Feature-0 row write. Defensive: it makes the filter's requirement explicit instead of inherited. | -| SC-2 | EbsdLib (Verdict C) | `CubicOps::getSchmidFactorAndSS` normalized with the `float` literals `1.732f` and `1.414f` inside an otherwise all-`double` computation. Both are smaller than the constants they approximate, inflating every Schmid factor by `sqrt(6)/(1.732f*1.414f) = 1.00018035284` and letting `m` exceed the physical maximum of 0.5. | `ebsdlib::constants::k_Sqrt3D` / `k_Sqrt2D`. EbsdLib `4a56725`. | -| SC-4 | shared with legacy | `crystalStructures[featurePhases[i]]` was unbounded — a phase id at or beyond the ensemble count, or a negative one, read outside the array and dispatched on whatever that read produced. | Bounds guard in the algorithm: `-13501` for too-large, `-13502` for negative, both naming the offending value and the ensemble count. | -| SC-5 | EbsdLib (Verdict C) + simplnx mitigation | `HexagonalLowOps::getSchmidFactorAndSS` **read** `schmidfactor` uninitialized and never initialized `slipsys`; `HexagonalOps` left `slipsys` and `angleComps` undefined; the seven Laue classes with no enumerated slip systems set `schmidfactor` and `slipsys` but left `angleComps` untouched, so a caller with a hoisted buffer got the previous Feature's angles. | All four outputs seeded in every affected overload (EbsdLib `2c84f2a`), plus NX-side per-iteration reinitialization of the three locals so the filter is correct against any EbsdLib. | +| `ComputeSchmidsFilter-D1` | The cubic calculation used truncated normalizers. The calculated Schmid factor could exceed the physical maximum of 0.5. | DREAM.3D 6.5.171; DREAM3D-NX v7.0.0 through v7.4.1. | EbsdLib 3.1.1 uses full-precision constants. | +| `ComputeSchmidsFilter-D2` | DREAM.3D initialized uncomputed angle values to `-301`. The value was an error code that was used as an initialization value. | DREAM.3D 6.5.171 only. DREAM3D-NX was not affected. | SIMPLNX initializes all output arrays to zero. The algorithm also initializes Feature 0. | +| `ComputeSchmidsFilter-D3` | The orientation library did not initialize all output values. The filter could reuse values from the previous Feature. | DREAM.3D 6.5.171; DREAM3D-NX v7.0.0 through v7.4.1. | EbsdLib 3.1.1 initializes all output values. The algorithm also initializes its output variables for each Feature. | +| `ComputeSchmidsFilter-D6` | The filter accepted invalid phase values, empty arrays, arrays with different tuple counts, and zero direction vectors. These inputs could cause invalid output or invalid memory access. | DREAM.3D 6.5.171; DREAM3D-NX v7.0.0 through v7.4.1. | The algorithm rejects invalid phase values. Preflight rejects empty arrays, arrays with different tuple counts, and zero direction vectors. | ## Code path coverage -`Source: src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.cpp (137 lines).` +`Source: src/Plugins/OrientationAnalysis/src/OrientationAnalysis/Filters/Algorithms/ComputeSchmids.cpp (139 lines).` The algorithm is flat: a parameter setup block, a sentinel-row write, then one pass over Features. -11 of 12 paths exercised. +20 of 21 paths exercised. | # | Phase | Path | Test case | |---|---|---|---| -| 1 | *Preflight* | Create the three unconditional output arrays with a zero fill | all four V&V test cases | -| 2 | *Preflight* | Create `Phis`/`Lambdas` when `StoreAngleComponents` is true | "Class 1 analytical oracle, auto slip system" | -| 3 | *Preflight* | Skip `Phis`/`Lambdas` when `StoreAngleComponents` is false | "options, skip path and phase guards" / "StoreAngleComponents == false does not create Phis/Lambdas" | -| 4 | *Preflight* | Reject a slip direction not lying in the slip plane (`-13500`) | "override slip system path" / "preflight rejects a slip direction that is not in the slip plane" | -| 5 | *Execute* | Zero the Feature-0 sentinel row | "Class 1 analytical oracle, auto slip system" | -| 6 | *Execute* | Normalize the loading direction | "Class 1 analytical oracle, auto slip system" / loading `[3,6,9]` | -| 7 | *Execute* | Normalize the user slip plane and direction (`OverrideSystem` true) | "override slip system path" | -| 8 | *Execute - per-Feature* | Negative phase id guard (`-13502`) | "options, skip path and phase guards" / "a negative phase id is an error" | -| 9 | *Execute - per-Feature* | Out-of-range phase id guard (`-13501`) | "options, skip path and phase guards" / "a phase id beyond the ensemble count is an error" | -| 10 | *Execute - per-Feature* | Skip a Feature whose Laue class is at or beyond `LaueGroupEnd` | "options, skip path and phase guards" / "features whose Laue class is beyond LaueGroupEnd get defined zeros" | -| 11 | *Execute - per-Feature* | Auto slip-system path (`getSchmidFactorAndSS(load, ...)`) | "Class 1 analytical oracle, auto slip system"; "orientation-matrix convention and equivariance" | -| 12 | *Execute - per-Feature* | `m_ShouldCancel` early return | *Not directly tested. No cancel-signal injection infrastructure exists for algorithm classes; same gap as the rest of this plugin.* | +| 1 | *Preflight* | Reject unequal Feature-array tuple counts (`-13503`) | "preflight input validation" / "Feature Phases and Average Quaternions must have equal tuple counts" | +| 2 | *Preflight* | Reject empty Feature arrays (`-13504`) | "preflight input validation" / "Feature arrays must contain the sentinel tuple" | +| 3 | *Preflight* | Reject an empty Crystal Structures array (`-13505`) | "preflight input validation" / "Crystal Structures must contain at least one ensemble" | +| 4 | *Preflight* | Reject a zero Loading Direction (`-13506`) | "preflight input validation" / "Loading Direction must be non-zero" | +| 5 | *Preflight* | Create the three unconditional output arrays with a zero fill | all executing V&V cases | +| 6 | *Preflight* | Create `Phis`/`Lambdas` when `StoreAngleComponents` is true | "Class 1 analytical oracle, auto slip system" | +| 7 | *Preflight* | Skip `Phis`/`Lambdas` when `StoreAngleComponents` is false | "options, skip path and phase guards" / "StoreAngleComponents == false does not create Phis/Lambdas" | +| 8 | *Preflight* | Reject a zero override Slip Plane (`-13507`) | "preflight input validation" / "Override Slip Plane must be non-zero" | +| 9 | *Preflight* | Reject a zero override Slip Direction (`-13508`) | "preflight input validation" / "Override Slip Direction must be non-zero" | +| 10 | *Preflight* | Reject a slip direction not lying in the slip plane (`-13500`) | "override slip system path" / "preflight rejects a slip direction that is not in the slip plane" | +| 11 | *Execute* | Zero the Feature-0 sentinel row | "Class 1 analytical oracle, auto slip system" | +| 12 | *Execute* | Normalize the loading direction | "Class 1 analytical oracle, auto slip system" / loading `[3,6,9]` | +| 13 | *Execute* | Normalize the user slip plane and direction (`OverrideSystem` true) | "override slip system path" | +| 14 | *Execute - per-Feature* | Negative phase id guard (`-13502`) | "options, skip path and phase guards" / "a negative phase id is an error" | +| 15 | *Execute - per-Feature* | Out-of-range phase id guard (`-13501`) | "options, skip path and phase guards" / "a phase id beyond the ensemble count is an error" | +| 16 | *Execute - per-Feature* | Skip a Feature whose Laue class is at or beyond `LaueGroupEnd` | "options, skip path and phase guards" / "Features whose Laue class is beyond LaueGroupEnd get defined zeros" | +| 17 | *Execute - per-Feature* | Valid Laue class with no enumerated slip systems | "options, skip path and phase guards" / "Valid Laue classes without slip systems keep zero Schmid outputs and computed Poles" | +| 18 | *Execute - per-Feature* | Auto slip-system path (`getSchmidFactorAndSS(load, ...)`) | "Class 1 analytical oracle, auto slip system"; "orientation-matrix convention and equivariance" | +| 19 | *Execute - per-Feature* | Override slip-system path (`getSchmidFactorAndSS(load, plane, direction, ...)`) | "override slip system path" | +| 20 | *Execute - per-Feature* | Store or omit angle components | auto, override, and StoreAngleComponents=false sections | +| 21 | *Execute - per-Feature* | `m_ShouldCancel` early return | *Not directly tested. No cancel-signal injection infrastructure exists for algorithm classes; same gap as the rest of this plugin.* | ## Test inventory | Test case | Status | Notes | |---|---|---| -| `"OrientationAnalysis::ComputeSchmidsFilter"` | kept, modified | Consumes `6_6_stats_test_v2.tar.gz`. The two integer arrays (`SlipSystems`, `Poles`) still assert exact equality with the archive — both are provably invariant under the SC-2 fix. The three float arrays now assert the archive equals the freshly computed value times its predicted per-component bias factor, because the archive predates EbsdLib 3.1.1. 75 assertions. See *Exemplar archive*. | -| `"OrientationAnalysis::ComputeSchmidsFilter: Class 1 analytical oracle, auto slip system"` | new-for-V&V | 5 `DYNAMIC_SECTION` loadings x (5 outputs + 6 Class 4 invariants + the Feature-0 sentinel row). 221 assertions. | -| `"OrientationAnalysis::ComputeSchmidsFilter: orientation-matrix convention and equivariance"` | new-for-V&V | 4 `DYNAMIC_SECTION` quaternions pinning the `qu2om` convention, the no-transpose property, negative `Poles` truncation and cubic-symmetry invariance. 105 assertions. | -| `"OrientationAnalysis::ComputeSchmidsFilter: override slip system path"` | new-for-V&V | 3 `DYNAMIC_SECTION` loadings on the second EbsdLib overload, including the degenerate zero-Schmid case, plus the radians-versus-cosines assertion (SC-3) and the `-13500` preflight rejection. 84 assertions. | -| `"OrientationAnalysis::ComputeSchmidsFilter: options, skip path and phase guards"` | new-for-V&V | `StoreAngleComponents == false`; the `LaueGroupEnd` skip row asserting zeros over a 20 000-Feature dirty-heap fixture (SC-1); the `-13501` and `-13502` guards (SC-4). 83 assertions. | +| `"OrientationAnalysis::ComputeSchmidsFilter"` | retired | Removed during adversarial review because `6_6_stats_test_v2.tar.gz` stores prior ComputeSchmids output and is a circular oracle. Its useful algorithmic behavior is covered by the inline Class 1 and Class 4 fixtures. | +| `"OrientationAnalysis::ComputeSchmidsFilter: Class 1 analytical oracle, auto slip system"` | new-for-V&V | 5 `DYNAMIC_SECTION` loadings x (5 outputs + Class 4 invariants + the Feature-0 sentinel row). 236 assertions. | +| `"OrientationAnalysis::ComputeSchmidsFilter: orientation-matrix convention and equivariance"` | new-for-V&V | 4 `DYNAMIC_SECTION` quaternions pinning the `qu2om` convention, the no-transpose property, negative `Poles` truncation and cubic-symmetry invariance. 109 assertions. | +| `"OrientationAnalysis::ComputeSchmidsFilter: override slip system path"` | new-for-V&V | 3 `DYNAMIC_SECTION` loadings on the second EbsdLib overload, including the degenerate zero-Schmid case, plus the radians-versus-cosines assertion and the `-13500` preflight rejection. 93 assertions. | +| `"OrientationAnalysis::ComputeSchmidsFilter: options, skip path and phase guards"` | new-for-V&V | `StoreAngleComponents == false`; a 20 000-Feature `LaueGroupEnd` skip fixture; a valid no-slip-system Laue class; and the `-13501`/`-13502` phase-id guards. 108 assertions. | +| `"OrientationAnalysis::ComputeSchmidsFilter: preflight input validation"` | new-for-V&V | Mismatched/empty Feature arrays, empty Crystal Structures, and zero loading/override vectors (`-13503` through `-13508`). 19 assertions. | | `"OrientationAnalysis::ComputeSchmidsFilter: SIMPL Backwards Compatibility"` | kept, untouched | 2 `DYNAMIC_SECTION` conversion fixtures (SIMPL 6.5 UUID, SIMPL 6.4 Filter_Name). 33 assertions. | -**601 assertions total** (75 + 221 + 105 + 84 + 83 + 33), taken from a `ctest --verbose` run rather than tallied by hand. All 6 pass at the verified commit in the in-core build. The out-of-core build run is **waived** (requester decision, 2026-08-18). - -Full `OrientationAnalysis::` regression: 269 of 269 unit tests pass. 28 `PIPELINE::` / `PY::` tests fail in this build directory for an environment reason, not a code reason — the `NX-Com-Qt69-Vtk96-Rel-EbsdLib` directory was built with only the `SimplnxCore` and `OrientationAnalysis` plugin targets and no Python bindings (2 of 6 plugins, no `simplnx.cpython-312-darwin.so`), so the first pipeline in each chain fails with `ModuleNotFoundError: No module named 'simplnx'` and every downstream pipeline then fails with `Input file does not exist`. - -## Mutation verification - -Six mutations, each applied to the fixed tree, rebuilt, run, and reverted; the transcript is `ww_work/ComputeSchmids/mutation_transcript.txt` and the driver is `mutate.sh`. Both repositories were proven byte-identical to the pre-mutation snapshot afterwards, and the baseline re-ran green. The tree the matrix ran against is byte-identical to the committed tree except for the single `vcpkg.json` manifest line bumping `ebsdlib` from `3.1.0` to `3.1.1`, which was applied last (see *Merge dependency*) and which affects only dependency resolution, no compiled code. - -The `Killed by` column gives `ctest` ordinals as they stood in `NX-Com-Qt69-Vtk96-Rel-EbsdLib` at the time of the run: 163 = the archive-consuming test, 164 = Class 1 auto, 165 = orientation-matrix convention, 166 = override path, 167 = options/guards, 168 = SIMPL BC. Ordinals are not stable — later building `EbsdLibUnitTest` into the same directory shifted them to 570-575. The test **names** are the stable identifiers. - -| # | Mutation | Killed by | Verdict | -|---|---|---|---| -| M1 | Transpose the orientation matrix before rotating the loading direction | tests 163, 165 | killed | -| M2 | Swap quaternion components x and y on read | tests 163, 165 | killed | -| M3 | Round the `Poles` scaling instead of truncating | tests 163, 164, 165, 166, 167 | killed | -| M4 | Slip-system tie-break `>` becomes `>=` (EbsdLib `CubicOps`, all 11 comparisons) | test 164 | killed | -| M5 | Revert the SC-1 explicit zero fill | *nothing* | **SURVIVOR** — behaviour-preserving in-core; see below | -| M6 | Revert both SC-4 phase-id guards | test 167 | killed | - -M2 initially survived: the first three fixture quaternions all have `x == y`, so swapping them is a no-op on every one of them, and only the archive-consuming test caught it. A fourth quaternion, `(1,0,0,0)`, was added specifically to close that gap — swapping its x and y turns a 180 deg rotation about X into one about Y, moving `SlipSystems` from 1 to 4 and `Poles` from `(26,-53,-80)` to `(-26,53,-80)`. M2 is killed by the hand fixtures after that addition. - -**M5 survives, and the reason is now established rather than presumed.** The original write-up said only that no deterministic RED was obtainable on this platform, having tried two things that both produced nothing: poisoning the heap across six malloc size classes, and re-running under `MallocNanoZone=0 MallocPreScribble=1 MallocScribble=1`. Two null results with no mechanism is not a finding, so the decisive variant was run. +**598 assertions total** (236 + 109 + 93 + 108 + 19 + 33), taken from a `ctest --verbose` run rather than tallied by hand. All 6 pass on the reviewed branch. -**Experiment.** The `LaueGroupEnd` skip fixture was enlarged from 3 Features to **20 000**, so every output array is 78 KB or more rather than 8-36 bytes, with same-size blocks poisoned to `0xAB` and freed immediately before the run; the explicit `"0"` fill was then removed from all five `CreateArrayAction` calls and the case re-run. **Result: still all zeros — 0 non-zero values across all five arrays and all 19 999 uncomputed rows.** A standalone probe over the same size ladder confirms the allocator is not the reason: `new float[n]` after a same-size dirty free returns `0xABABABAB` for every `n` at or above 4096 elements (16 KB) and zeros below it, which both explains why the earlier 8-36-byte attempts saw nothing and rules out "libmalloc was kind" as the explanation at 20 000. +Full `OrientationAnalysis::` regression after the internal-review fixes: **268 of 268 unit tests passed** in the EbsdLib 3.1.1 build. -**Mechanism.** The zeros are written by SIMPLNX. `CoreDataIOManager::addDataStoreFnc()` constructs every in-core store as `std::make_unique(tupleShape, componentShape, 0.0f)` — a hard-coded zero `initValue`, independent of the action's `fillValue` string — and `DataStore`'s constructor unconditionally `std::fill_n`s the buffer with it (`DataStore.hpp:66-69`). `CreateArrayAction`'s `fillValue` only adds a second, redundant `store->fill()`. So SC-1's premise, that an empty `fillValue` leaves the buffer default-initialized, is **false for the in-core store**, and M5 cannot be killed because there is nothing to kill: the mutation is behaviour-preserving in-core. +## Test sensitivity verification -**What that changes.** SC-1 is reclassified from an active defect to a **latent** one: the dropped `initValue` is a genuine source-level port regression, but no in-core release produced indeterminate output because of it. The explicit fill is kept as a defensive, self-documenting statement of the filter's requirement and as the only guarantee for a store implementation whose factory does not hard-code a zero. The enlarged fixture is kept because it makes the assertion non-vacuous: at 20 000 Features it would catch a store that stopped filling, which the 3-Feature version provably could not. *Evidence class: executed (the negative result and the probe), plus source for the mechanism.* +Test sensitivity verification introduces one temporary defect at a time and confirms that the applicable test fails. -The SC-2 mutation needs no separate entry — the entire pre-fix baseline run is it. Against EbsdLib 3.1.0 the new fixtures failed with exactly the predicted pre-fix values (`m = 0.4083219171` against an expected `0.4082482905`, `Phis = 0.5773671865` against `0.5773502692`, `Lambdas = 0.7072135806` against `0.7071067812`), recorded in `ww_work/ComputeSchmids/red_transcript.txt`. +Seven temporary defects were evaluated. Each defect caused the expected V&V test to fail. -## Exemplar archive - -`6_6_stats_test_v2.tar.gz`, SHA512 `e84999dec914d81efce4fc4237c49c9bf32e48381b1e79f58aa4df934f0d7606cd7a948f9a5e7b17a126a7944cc531b531cfdc70756ca3e2207b20734e089723`, matching `src/Plugins/OrientationAnalysis/test/CMakeLists.txt:130`. **Neither the archive nor its `download_test_data()` line was changed.** Provenance sidecar: `vv/provenance/6_6_stats_test_v2.md`. - -The archive's `Schmids`, `Schmid_Phis` and `Schmid_Lambdas` arrays were generated with EbsdLib <= 3.1.0 and are therefore stale by a known amount. Rather than retire the comparison — which would discard 214 real regression checks — or loosen it into meaninglessness, the test now asserts the **relationship** between archive and fresh output: - -| Array | archived / freshly computed | Source of the factor | +| Temporary defect | Test that detected the defect | Result | |---|---|---| -| `Schmid_Phis` | 1.0000293384785181 | `sqrt(3) / 1.732f` | -| `Schmid_Lambdas` | 1.0001510099261918 | `sqrt(2) / 1.414f` | -| `Schmids` | 1.0001803528351113 | `sqrt(6) / (1.732f * 1.414f)` | - -That is strictly stronger than the equality it replaces: it pins both the new values and the exact size and shape of the change, and it fails if the fix perturbed anything the bias analysis did not account for. The predicted `Schmid_Lambdas` factor `1.00015100993` was met by the observed `1.00015102083` — agreement to `1.1e-8` relative, across the whole Small IN100 Feature set. Regenerating the archive against EbsdLib >= 3.1.1 would set all three factors to 1.0; that is deliberately left for a future cycle so this PR does not also churn shared test data. - -## Deviations from DREAM3D 6.5.171 +| Transpose the orientation matrix before rotating the loading direction. | Orientation-matrix convention and equivariance | Detected | +| Swap quaternion components X and Y when the filter reads the quaternion. | Orientation-matrix convention and equivariance | Detected | +| Round the `Poles` values instead of truncating them. | Class 1 analytical oracle, orientation-matrix convention, override path, and options and guards | Detected | +| Change the EbsdLib cubic slip-system tie-break from `>` to `>=`. | Class 1 analytical oracle | Detected | +| Remove both phase-index guards. | Options, skip path, and phase guards | Detected | +| Disable the Feature-array tuple-count validation. | Preflight input validation | Detected | +| Disable all three zero-vector guards. | Preflight input validation | Detected | -Run 2026-08-20. Legacy binary `~/Applications/DREAM3D.app/Contents/Bin/PipelineRunner` (6.5.171); NX binary `DREAM3D-Build/NX-Com-Qt69-Vtk96-Rel-EbsdLib/Bin/nxrunner`, built against the fixed local EbsdLib. One shared hand-built legacy-format input carrying six Features — a sentinel, the four oracle quaternions, and one Feature whose phase has no slip systems — driven through 10 pipeline pairs (5 auto-path loadings, one `StoreAngleComponents = false`, 3 override-path loadings, 1 negative case). Predictions were written to `ww_work/ComputeSchmids/predictions.txt` **before** `compare.py` was run. - -Result: **93 predicted relationships confirmed, 0 unexplained differences, 1 prediction amended.** - -`SlipSystems` and `Poles` were bit-identical on all five auto-path fixtures. Every auto-path float matched its predicted bias factor to better than `1e-7` relative. On the override path, where neither implementation uses the truncated literals, the floats agreed to `1.2e-7` relative with no systematic bias — the control that proves the D1 divergence is the normalizer and nothing else. - -**No 6.5.172 surgical patch was produced for D1, deliberately.** Legacy's Schmid arithmetic lives in `OrientationLib/LaueOps/CubicOps.cpp`, not in the filter, so patching the filter could not bring the two into alignment and patching `OrientationLib` would be a library change rather than the "smallest possible diff, one filter per patch" the protocol asks for. The alignment proof here is instead that **fixed-EbsdLib NX reproduces the exact-arithmetic oracle** to `1e-6` on all 12 value-asserting Class 1 fixtures, which the unit tests establish directly. +## Exemplar archive -- `ComputeSchmidsFilter-D1` — Schmid factor and angle components inflated by a uniform +0.018% in 6.5.171 (and in NX before EbsdLib 3.1.1). Trust SIMPLNX. -- `ComputeSchmidsFilter-D2` — skipped Features: `Phis`/`Lambdas` read `-301` in 6.5.171 and `0` in NX; pre-fix NX read `0` by way of the in-core store factory's hard-coded zero rather than the filter's own intent (latent port regression — see the D2 entry). Trust SIMPLNX. -- `ComputeSchmidsFilter-D3` — EbsdLib Laue ops left `angleComps` and, for `HexagonalLowOps`, `schmidfactor`/`slipsys` undefined. Fixed in EbsdLib 3.1.1-staging plus an NX-side mitigation. D3 also records **twelve known-open SC-2-class divisors** in the hexagonal plane-normal geometry (`0.8164` for `2/sqrt(6)`, `1.154` for `2/sqrt(3)`), deferred because they can move the argmax and so need a hexagonal oracle of their own, and the new hexagonal `slipsys = 0` sentinel. -- `ComputeSchmidsFilter-D4` — `Phis`/`Lambdas` change units with `OverrideSystem` (cosines versus radians), and the default array names differ between versions. Documented, not changed. -- `ComputeSchmidsFilter-D5` — on the override path `SlipSystems` is a table-relative symmetry-operator index; the two libraries order the same 24-element group differently and six operators tie at the maximum, so 6.5.171 reports 8 where NX reports 3 for an identical physical answer. Either acceptable; documented. +The existing test archive contained output from an earlier run of this filter. It was a circular oracle and was retired for this filter. The replacement tests use inline Class 1 analytical data and Class 4 invariants. -## Documentation changes +## Deviations from DREAM.3D 6.5.171 -`docs/ComputeSchmidsFilter.md` gained: the loading-direction normalization and sample-frame semantics; the Feature-0 and skipped-Feature zero behaviour; the tie-break rule and that it is enumeration-order rather than physics; the two incompatible `SlipSystems` numbering schemes and the table-relative, tie-arbitrary nature of the override index (D5); the `Phis`/`Lambdas` unit flip (D4); and a correction of the claim implicit in the name `Poles` — it is a fixed-point encoding of the crystal-frame unit loading direction, truncated toward zero, not a Miller index. The *Example Pipelines* citation was corrected from `(05) SmallIN100 Crystallographic Statistics` to the actual file name, `(04) Small IN100 Crystallographic Statistics`. The `SlipSystems` section additionally now states the cubic-0 versus hexagonal-1 numbering base and the two situations in which a reported `0` means "no slip system found" rather than slip system number 0, including the new hexagonal case introduced by the D3 fix. +See `vv/deviations/ComputeSchmidsFilter.md` for the root cause, affected users, and recommendation for each deviation. -**Title convention.** The filter's `humanName()` is **"Compute Schmid Factors"**, and the user-facing doc is titled that way. This report, the deviations file and the commit title use the short form **"Compute Schmids"**, matching the class name `ComputeSchmidsFilter` and the short-form convention used across this V&V batch. Both names refer to the same filter, UUID `b4681855-0a3d-4237-97f2-5aec509115c4`. +| Deviation | Observed difference | +|---|---| +| `ComputeSchmidsFilter-D1` | DREAM.3D 6.5.171 and DREAM3D-NX through v7.4.1 used truncated cubic normalizers. The calculated Schmid factor and angle components were too large. | +| `ComputeSchmidsFilter-D2` | DREAM.3D 6.5.171 initialized uncomputed angle values to `-301`. DREAM3D-NX initializes these values to zero. | +| `ComputeSchmidsFilter-D3` | The orientation libraries did not initialize all Schmid output values. The filter could return undefined or previous Feature values. | +| `ComputeSchmidsFilter-D4` | The angle outputs use cosines on the automatic path and radians on the override path. The default output names and error messages also differ. | +| `ComputeSchmidsFilter-D5` | The override path returns an index from the library symmetry table. Different table orders can return different indices for the same physical result. | +| `ComputeSchmidsFilter-D6` | SIMPLNX rejects invalid phase values, malformed arrays, and zero direction vectors. DREAM.3D 6.5.171 accepted these inputs and could produce invalid output or invalid memory access. | diff --git a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeSchmidsFilter.md b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeSchmidsFilter.md index c4de2dd212..6f7141bc99 100644 --- a/src/Plugins/OrientationAnalysis/vv/deviations/ComputeSchmidsFilter.md +++ b/src/Plugins/OrientationAnalysis/vv/deviations/ComputeSchmidsFilter.md @@ -30,7 +30,7 @@ Measured against 6.5.171 on hand-built fixtures, the observed ratios matched the **Root cause:** Library. `OrientationLib/LaueOps/CubicOps.cpp:850-868` in 6.5.171 normalizes the `{111}` plane-normal dot products by the literal `1.732f` and the `<110>` slip-direction dot products by `1.414f`, instead of by `sqrt(3)` and `sqrt(2)`. Both literals are four-significant-digit truncations and both are smaller than the constants they stand in for, so every direction cosine comes out too large. EbsdLib carried the identical defect through version 3.1.0, which means **SIMPLNX built against EbsdLib <= 3.1.0 reproduces this deviation exactly** — this is a shared library defect, not a 6.5.171-only one. -Fixed in EbsdLib on `topic/3_1_1_staging` (commit `4a56725`, `BUG: Use exact sqrt(3)/sqrt(2) normalizers in CubicOps::getSchmidFactorAndSS`) by replacing both literals with the full-precision `ebsdlib::constants::k_Sqrt3D` and `k_Sqrt2D`. The `double` type matches the surrounding arithmetic, which is entirely `double` — those two `float` literals were the only precision loss in the function. +Fixed in EbsdLib 3.1.1 by replacing both literals with the full-precision `ebsdlib::constants::k_Sqrt3D` and `k_Sqrt2D`. The `double` type matches the surrounding arithmetic, which is entirely `double` — those two `float` literals were the only precision loss in the function. Note that the *override* slip-system path (`Override Default Slip System` = on) was never affected in either implementation: it normalizes the user-supplied plane and direction with `std::sqrt` and never used the truncated literals. The A/B run confirms this — override-path floats agree between the two builds to `1.2e-7` relative with no systematic bias, which is the control that isolates the normalizer as the sole cause of the auto-path divergence. @@ -50,15 +50,17 @@ No surgical patch to the legacy source was produced for this deviation, delibera | **Filter UUID** | `b4681855-0a3d-4237-97f2-5aec509115c4` | | **Status** | active | -**Symptom:** For Features the filter does not compute — Feature 0, which is the conventional unassigned-Feature sentinel, and any Feature whose phase maps to a crystal structure with no enumerated slip systems — the two angle-component arrays read **`-301.0`** in 6.5.171 and **`0.0`** in SIMPLNX. `Schmids`, `SlipSystems` and `Poles` read `0` in both. +**Symptom:** For Features the filter skips entirely — Feature 0, which is the conventional unassigned-Feature sentinel, and any Feature whose crystal-structure value is at or beyond `LaueGroupEnd` — the two angle-component arrays read **`-301.0`** in 6.5.171 and **`0.0`** in SIMPLNX. `Schmids`, `SlipSystems` and `Poles` read `0` in both. + +Valid Laue classes that do not enumerate slip systems are a different path. Their Schmid factor, slip-system index and angle components are zero after the D3 fix, but `Poles` is still computed from the transformed loading direction. **Root cause:** Bug in 6.5.171. `FindSchmids.cpp:234` and `:242` create the `SchmidPhis` and `SchmidLambdas` arrays with `createNonPrereqArrayFromPath, AbstractFilter, float>(this, tempPath, -301, cDims)`. The third argument of that template is the array's `initValue`, not an error code; `-301` is an error code that was pasted into the wrong parameter slot. The same call for `Schmids`, `SlipSystems` and `Poles` correctly passes `0`. Because the execute loop writes the angle components only inside `if(xtal < LaueGroupEnd)`, uncomputed rows keep the `-301` sentinel. SIMPLNX created all five arrays with `CreateArrayAction`'s default empty fill value, where 6.5.171 passed `initValue 0` for three of them. Preflight now passes an explicit `"0"` fill for all five arrays, and the algorithm additionally writes the Feature-0 row explicitly. -**How much exposure that actually carried, measured rather than assumed.** The obvious reading — that without a fill the uncomputed rows hold indeterminate memory — does **not** hold for the in-core store. `CoreDataIOManager::addDataStoreFnc()` constructs every in-core `DataStore` with `static_cast(0)` as its `initValue` irrespective of the action's `fillValue` string, and `DataStore`'s constructor unconditionally `std::fill_n`s the buffer with it (`DataStore.hpp:66-69`). Removing the explicit fill and re-running a 20 000-Feature fixture with same-size heap blocks poisoned to `0xAB` and freed immediately before the run still yields all zeros; a standalone probe confirms that on the same platform a 20 000-element `new float[]` after a same-size dirty free *does* return the poison, so the zeros are written by SIMPLNX and not inherited from the allocator. The explicit fill is therefore **defensive on a reachable path, not merely hypothetical**: `DataIOCollection::checkStoreDataFormat()` silently rewrites an empty `dataFormat` to the preferred large-data format whenever the array exceeds the large-data threshold and such a store factory is registered (e.g. the external `SimplnxOoc` plugin), and nothing guarantees that factory hard-codes a zero `initValue`. The fill restores what legacy stated explicitly and is the only in-repo guarantee on that path, which this cycle did not verify (logged open). The SIMPLNX side of this is a **latent port regression** — the `initValue` was genuinely dropped, but no in-core release ever produced indeterminate output because of it. +**How much exposure that actually carried, measured rather than assumed.** The obvious reading — that without a fill the uncomputed rows hold indeterminate memory — does not hold for the default store. `CoreDataIOManager::addDataStoreFnc()` constructs every `DataStore` with `static_cast(0)` as its `initValue` irrespective of the action's `fillValue` string, and `DataStore`'s constructor unconditionally `std::fill_n`s the buffer with it (`DataStore.hpp:66-69`). Removing the explicit fill and re-running a 20 000-Feature fixture with same-size heap blocks poisoned to `0xAB` and freed immediately before the run still yielded all zeros; a standalone probe confirmed that a same-size raw allocation did return the poison. The zeros were therefore written by SIMPLNX rather than inherited from the allocator. This is a **latent port regression**: the explicit initialization requirement was genuinely dropped even though the tested store masked it. -**Affected users:** Anyone whose data contains a phase whose crystal structure has no slip systems enumerated (anything other than Cubic-High, Cubic-Low, Hexagonal-High or Hexagonal-Low, or an unindexed/unknown phase), and anyone reading Feature 0. Under 6.5.171 those rows are a recognisable but undocumented `-301` sentinel. Under SIMPLNX they read `0` — before this cycle by way of the store factory's hard-coded zero rather than by the filter's own intent, and now by both. +**Affected users:** Anyone reading Feature 0 or a Feature whose phase maps to an unknown or out-of-range crystal-structure value. Under 6.5.171 those skipped angle rows are a recognisable but undocumented `-301` sentinel. Under SIMPLNX they read `0` — before this cycle by way of the store factory's hard-coded zero rather than by the filter's own intent, and now by both. **Recommendation:** **Trust SIMPLNX.** Zero is the meaningful "not computed" value here and is consistent with what the other three output arrays already do for the same rows. Filter uncomputed Features by phase or by `Schmids == 0`, not by looking for `-301`. @@ -80,7 +82,7 @@ SIMPLNX created all five arrays with `CreateArrayAction`'s default empty fill va Case 2 is the more damaging in practice because it is silent and produces plausible-looking output. A caller that hoists one `angleComps[2]` buffer outside its per-Feature loop — the natural way to write the loop, and what both `FindSchmids` and `ComputeSchmids` did — gets the **previous Feature's** angle components attributed to the current Feature whenever the current Feature's Laue class is one of the seven stubs. The Schmid factor correctly reads 0 while the two angle columns read as real measurements carried over from an unrelated Feature. -**Root cause:** Bug, in the library. Fixed in EbsdLib on `topic/3_1_1_staging` (commit `2c84f2a`, `BUG: Define every getSchmidFactorAndSS output on all paths`) by seeding all four outputs at the top of every affected overload. That changes no value on any path that already computed a result; it only replaces indeterminate or stale output with a defined zero. `CubicOps` and `CubicLowOps` already wrote all four unconditionally and were left alone, as were all eleven plane/direction overloads, which already zeroed their outputs on entry. +**Root cause:** Bug, in the library. Fixed in EbsdLib 3.1.1 by seeding all four outputs at the top of every affected overload. That changes no value on any path that already computed a result; it only replaces indeterminate or stale output with a defined zero. `CubicOps` and `CubicLowOps` already wrote all four unconditionally and were left alone, as were all eleven plane/direction overloads, which already zeroed their outputs on entry. Independently of the library fix, SIMPLNX now declares `schmid`, `angleComps` and `slipSystem` **inside** the per-Feature loop, so the stale-carryover half of this defect cannot occur regardless of which EbsdLib the filter is linked against. @@ -88,17 +90,6 @@ One consequence of the fix, and three observations deliberately **not** changed, **A new `slipsys = 0` "no candidate" sentinel for the hexagonal classes.** `HexagonalOps` and `HexagonalLowOps` assign `slipsys` values **1 through 6** when a candidate wins the strict `>`. Seeding `slipsys = 0` therefore introduces a value *outside* that documented range, and it is now the reportable signal that no candidate ever beat the seeded `schmidfactor = 0.0` — the hexagonal analogue of the override path's `slipsys = 0`. Callers reading a hexagonal `SlipSystems` value must treat `0` as "no slip system found", not as a slip-system number. Before the fix the same situation produced whatever the caller had passed in. Recorded in `docs/ComputeSchmidsFilter.md`. -**KNOWN-OPEN: twelve further SC-2 instances in the hexagonal plane-normal geometry.** Six of the hexagonal slip-plane normals compute their `z` component by dividing by a truncated **sqrt-derived normalizer held as a bare decimal literal**. Structurally these are the *same* defect as D1 — a four-significant-digit stand-in for an irrational constant inside otherwise all-`double` arithmetic — not merely an approximate direction cosine: - -| Literal | Constant it stands for | Relative error | Sites (EbsdLib `topic/3_1_1_staging`) | -|---|---|---|---| -| `0.8164` | `2/sqrt(6)` = 0.81649658 | −1.18e-4 | `HexagonalOps.cpp:758, 765, 772, 779`; `HexagonalLowOps.cpp:671, 678, 685, 692` | -| `1.154` | `2/sqrt(3)` = 1.15470054 | −6.07e-4 | `HexagonalOps.cpp:786, 793`; `HexagonalLowOps.cpp:699, 706` | - -The `1.154` bias is **3.4x** the `sqrt(6)/(1.732f * 1.414f)` cubic bias that D1 was raised for. Status is **known-open, deferred**, not benign. The reason for deferring is specific: unlike D1's uniform positive scale factor, these literals perturb the *direction* of a plane normal — `lnz` is one component of a vector that is renormalized by its own computed norm a few lines later, so the error tilts the normal instead of cancelling, and fixing it can move the argmax. A fix therefore changes both the reported hexagonal Schmid factor **and** the reported slip system, and needs its own hexagonal Class 1 oracle first — exactly the work that preceded the cubic fix. Follow-up work, with the twelve line numbers above. - -**Genuinely benign truncated literals, kept — and why they are different.** The remaining hexagonal literals (`0.707`, `0.57735`, the `0.4472`/`0.8944` pair, and the `0.4082`/`0.8164` pair in the direction-cosine triples at `HexagonalOps.cpp:664-673` / `HexagonalLowOps.cpp:577-586` — distinct occurrences from the twelve divisor sites above, where `0.8164` also appears) appear only as *proportional triples* that are divided by their own computed norm a few lines later, and the truncations preserve the component **ratios** exactly: the `0.707` and `0.57735` triples are equal-magnitude, `0.4472 / 0.8944 = 0.5` exactly, which is the exact `(1/sqrt(5)) / (2/sqrt(5))`, and `0.4082 / 0.8164 = 0.5` exactly, matching `(1/sqrt(6)) / (2/sqrt(6))`. A common relative error cancels identically in the renormalization, so these change no output bit. That is precisely the distinction from the twelve above, where the literal is a **divisor applied to one component only** and so survives renormalization. Two further items are noted but out of scope: `0.866025` for `sqrt(3)/2` in the hexagonal-to-Cartesian basis transform (relative error −4.6e-7, three orders of magnitude below the twelve), and `caratio` hard-coded to `1.633` — a modelling limitation rather than a precision one, since it ignores the actual `c/a` of the phase. - **Slip-system numbering base.** `HexagonalOps` and `HexagonalLowOps` number their reported slip systems from **1**, while `CubicOps` numbers from **0**. Changing either would break existing user data. Documented, unfixed. **Affected users:** Anyone whose data contains a Hexagonal-Low phase (defect 1), and anyone whose data mixes a Cubic or Hexagonal phase with any of Triclinic, Monoclinic, Orthorhombic, Tetragonal-Low/High or Trigonal-Low/High while `Store Angle Components of Schmid Factor` is on (defect 2). @@ -117,11 +108,11 @@ The `1.154` bias is **3.4x** the `sqrt(6)/(1.732f * 1.414f)` cubic bias that D1 **Symptom:** Three interface-level differences that are stable across both implementations but easy to trip over. -1. **The angle-component arrays change units with a boolean toggle.** With `Override Default Slip System` **off**, `Phis` and `Lambdas` hold the direction **cosines** `cos phi` and `cos lambda` — dimensionless, in `[0, 1]`. With it **on**, the same two arrays hold the **angles** `phi` and `lambda` themselves, in **radians**. The two code paths call different overloads: the auto path stores the raw dot products, the override path stores `std::acos` of them. Both parameter descriptions call these "the angle between ...", which is only true in the second case. Present identically in 6.5.171 and SIMPLNX. Consequence: with the toggle off the Schmid factor equals the product of the two stored values; with it on it equals the product of their cosines. +1. **The angle-component arrays change units with a boolean toggle.** With `Override Default Slip System` **off**, `Phis` and `Lambdas` hold the direction **cosines** `cos phi` and `cos lambda` — dimensionless, in `[0, 1]`. With it **on**, the same two arrays hold the **angles** `phi` and `lambda` themselves, in **radians**. The two code paths call different overloads: the auto path stores the raw dot products, the override path stores `std::acos` of them. Present identically in 6.5.171 and SIMPLNX. Consequence: with the toggle off the Schmid factor equals the product of the two stored values; with it on it equals the product of their cosines. SIMPLNX parameter descriptions now state these mode-dependent units explicitly. 2. **The default angle-array names differ between versions.** 6.5.171 defaults to `SchmidPhis` and `SchmidLambdas`; SIMPLNX defaults to `Schmid_Phis` and `Schmid_Lambdas`. Pipelines converted from SIMPL carry their explicit names across correctly, so this bites only when comparing outputs or hand-writing a downstream path. -3. **The preflight rejection code differs.** A slip direction that does not lie in the slip plane is rejected at preflight by both, with the same message text (`"Slip Plane and Slip Direction must be normal"`) but different codes: `-1001` in 6.5.171, `-13500` in SIMPLNX. Verified on a dedicated negative fixture. +3. **The preflight rejection differs.** A slip direction that does not lie in the slip plane is rejected at preflight by both. 6.5.171 reports `-1001` with `"Slip Plane and Slip Direction must be normal"`; SIMPLNX reports `-13500` and includes both vectors and their actual dot product so the user can correct the input. Verified on a dedicated negative fixture. **Root cause:** Algorithmic choice / order of operations, for (1); naming and error-code renumbering during the port for (2) and (3). @@ -157,3 +148,21 @@ This is distinct from the auto path, where `SlipSystems` **is** a genuine slip-s **Affected users:** Anyone who uses `Override Default Slip System` and records, filters on, or compares the `SlipSystems` value — particularly anyone migrating stored results or scripts from DREAM3D 6.5.x. **Recommendation:** **Either acceptable** — both values identify a correct maximizing symmetry operator. Where a choice is needed, **trust SIMPLNX**, because the SIMPLNX index is the one addressable through EbsdLib's own `getMatSymOp()` accessor, so a user can retrieve the actual matrix the index refers to. Do not treat this value as portable across versions, and do not treat it as uniquely determined even within one version. `docs/ComputeSchmidsFilter.md` now states both caveats, and the unit test that asserts index 3 carries a comment recording that it pins EbsdLib's table order rather than a physical label. + +--- + +## ComputeSchmidsFilter-D6 + +| Field | Value | +|---|---| +| **Deviation ID** | `ComputeSchmidsFilter-D6` | +| **Filter UUID** | `b4681855-0a3d-4237-97f2-5aec509115c4` | +| **Status** | active | + +**Symptom:** SIMPLNX now rejects malformed inputs before allocating or normalizing data: unequal tuple counts between `Feature Phases` and `Average Quaternions`, empty Feature or Ensemble arrays, a zero loading direction, and zero override slip-plane or slip-direction vectors. DREAM3D 6.5.171 accepts the zero-vector cases. In the independent A/B run, a zero loading direction produced `NaN` Schmid factors and angle components in 6.5.171, while zero override vectors silently produced all-zero Schmid outputs. Before this guard was added, SIMPLNX behaved the same way. A mismatched pair of Feature arrays was more severe in SIMPLNX: the algorithm indexed past both the shorter input and shorter output arrays, corrupting memory and causing `nxrunner` to exit with signal status 139 while writing the result. + +**Root cause:** Bug shared with legacy, plus an added SIMPLNX validation. Both implementations normalized vectors without first checking their magnitude and assumed the two Feature arrays had identical tuple counts. SIMPLNX preflight now returns `-13503` through `-13508` with the offending paths, tuple counts, or vector components. The parameter version is 2 because the parameter descriptions now state the non-zero-vector constraints and mode-dependent output semantics. + +**Affected users:** Pipelines with malformed Feature-array selections or zero direction vectors. Valid pipelines are unaffected. Invalid pipelines that previously completed can now fail preflight, but the previous outputs were undefined, non-finite, or the result of memory corruption. + +**Recommendation:** **Trust SIMPLNX.** Correct the selected array paths so `Feature Phases` and `Average Quaternions` have equal non-zero tuple counts, provide at least one Crystal Structures tuple, and use non-zero direction vectors. Do not consume the legacy zero-vector outputs. diff --git a/vcpkg-configuration.json b/vcpkg-configuration.json index ccee7ad6d0..e7bb6f8efe 100644 --- a/vcpkg-configuration.json +++ b/vcpkg-configuration.json @@ -8,7 +8,7 @@ { "kind": "git", "repository": "https://github.com/bluequartzsoftware/simplnx-registry", - "baseline": "223c58872a56dd25313b6461ddef410267f61873", + "baseline": "7c57db23a5b5ed8eb3dd784a5649404ea9e19534", "packages": [ "benchmark", "blosc", diff --git a/vcpkg.json b/vcpkg.json index d0348c335a..1487410660 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -83,7 +83,7 @@ "dependencies": [ { "name": "ebsdlib", - "version>=": "3.1.1" + "version>=": "3.1.2" } ] },