You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
VV: Compute Average C-Axis Orientations fully V&V'ed
Summary:
- Confirmed no SIMPLNX-side bugs (algorithm verified correct as-is); applied 2 algorithm refinements during the Phase-7 review (counter==0 → NaN at finalize +
final per-feature normalize) — these are intentional design alignments that surface as D1 and D2 deviations vs DREAM3D 6.5.171 but match the intended
algorithm and are bit-identical to the 6.5.172 backport;
- documented 2 deviations from DREAM3D 6.5.171 (D1 counter==0 → NaN at finalize replaces legacy's (0,0,1) rescue at F0/F5/F6; D2 precision-sensitive direction
+ unit-vector vs unnormalized magnitude at F7 antipodal-flip cancellation boundary);
- retired 1 test (legacy-by-reputation exemplar consumer of 7_2_AvgCAxis.tar.gz — confirmed circular oracle per policy line 33: reference values were produced
by a "special build of DREAM3D 6.6.379 with micro-texture bug fixes," not by an independent oracle);
- unit tests replaced with 3 tests: 1 Class 1 (Analytical) valid-execution exemplar against a hand-built 11-cell/8-feature dataset + 1 negative all-non-hex
error test + 1 SIMPL 6.4/6.5 backwards-compat DYNAMIC_SECTION — covers 8/12 enumerated code paths (gaps: all-hex-ensemble preflight, background-voxel skip, 2
cancel-check paths);
- added 3 V&V source-tree deliverables (report, deviations, provenance — provenance file attributes the dataset design to MAJ with subsequent design changes
and Python generation-script integration by Claude Opus 4.7, signed off by MAJ);
- published new compute_avg_c_axis.tar.gz (replaces retired 7_2_AvgCAxis.tar.gz) with hand-built Class 1 dataset + Class 4 (Invariant) ||AvgCAxes|| == 1.0
unit-vector contract for F7; empirical three-way A/B (SIMPLNX vs 6.5.171 official vs 6.5.172 backport) confirmed SIMPLNX bit-identical to 6.5.172 across all 8
features, conclusively isolating D1 and D2 root causes;
- updated user-facing doc with new ComputeAvgCAxes_HexagonalCAxis figure (SVG + PNG) explaining the c-axis-in-sample-frame convention.
Copy file name to clipboardExpand all lines: src/Plugins/OrientationAnalysis/docs/ComputeAvgCAxesFilter.md
+29-14Lines changed: 29 additions & 14 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -6,29 +6,44 @@ Statistics (Crystallography)
6
6
7
7
## Description
8
8
9
-
This **Filter**determines the average C-axis location of each **Feature**by the following algorithm:
9
+
This **Filter**computes the average C-axis direction for each **Feature**(grain) in hexagonal materials. The result is a unit vector per **Feature** that indicates where the grain's C-axis points in the sample reference frame.
10
10
11
-
1. Gather all **Elements** that belong to the **Feature**
12
-
2. Determine the location of the c-axis in the sample *reference frame* for the rotated quaternions for all **Elements**. This is achieved by converting the input quaternion to
13
-
an orientation matrix (which represents a passive transform matrix), taking the transpose of the matrix to convert it from passive to active, and then multiplying the
14
-
transposed matrix by the crystallographic C-Axis direction vector <001>.
15
-
3. Average the locations and store the average for the **Feature**
11
+
### What is the C-Axis?
16
12
17
-
*Note:* This **Filter** will only work properly for *Hexagonal* materials. The **Filter** does not apply any symmetry
18
-
operators because there is only one c-axis (<001>) in *Hexagonal* materials and thus all symmetry operators will leave
19
-
the c-axis in the same position in the sample *reference frame*. However, in *Cubic* materials, for example, the {100}
20
-
family of directions are all equivalent and the <001> direction will change location in the sample *reference frame* when
21
-
symmetry operators are applied.
13
+
In hexagonal crystal structures (such as titanium, magnesium, and zinc), the *C-axis* is the unique crystallographic direction that runs along the long axis of the hexagonal unit cell (the [001] direction). This axis is important because many mechanical and physical properties of hexagonal materials vary depending on whether they are measured along or perpendicular to the C-axis.
22
14
23
-
This filter will error out if **ALL** phases are non-hexagonal. Any non-hexagonal phases will have their computed values set to NaN value.
15
+

24
16
25
-
The output is a direction vector for each feature.
17
+
### How This Filter Works
18
+
19
+
Each **Cell** (voxel) in a grain has its own measured orientation. This filter determines where each cell's C-axis points in the physical sample coordinate system, then averages those directions across all cells belonging to the same **Feature**:
20
+
21
+
1. For each **Cell**, the filter uses the cell's orientation (quaternion) to rotate the crystal [001] direction into the sample reference frame.
22
+
2. The rotated C-axis directions are accumulated for each **Feature**, ensuring all directions point into the same hemisphere for a consistent average.
23
+
3. The accumulated directions are normalized to produce a unit vector for each **Feature**.
24
+
25
+
### Hexagonal Materials Only
26
+
27
+
This filter only produces valid results for hexagonal phases (6/mmm or 6/m symmetry). In hexagonal materials, the C-axis is unique -- there is only one [001] direction, so crystal symmetry does not create ambiguity.
28
+
29
+
In cubic materials, the [001], [010], and [100] directions are all crystallographically equivalent. Applying symmetry operators would move the [001] direction to different positions in the sample frame, making a simple average meaningless. For this reason, the filter **silently skips non-hexagonal cells** during accumulation. A **Feature** whose contributing cells are *all* non-hexagonal (or which has no cells assigned to it at all) will have its output set to **NaN**.
30
+
31
+
The filter will produce an error if no hexagonal phases are present in the data (`-76402`: "No phases that have a crystal symmetry of Hexagonal (6/mmm or 6/m) were found.").
32
+
33
+
### Required Input Sources
34
+
35
+
This filter operates on previously segmented data and requires that several prior operations have already been run:
36
+
37
+
-**Cell Quaternions** -- typically read from EBSD data via [Read H5EBSD](ReadH5EbsdFilter.md), [Read CTF Data](ReadCtfDataFilter.md), or [Read ANG Data](ReadAngDataFilter.md); can also be produced from Euler angles by [Convert Orientations](ConvertOrientationsFilter.md).
38
+
-**Cell Feature Ids** -- produced by a segmentation filter such as [Segment Features (Misorientation)](EBSDSegmentFeaturesFilter.md) or [Segment Features (C-Axis Misalignment)](CAxisSegmentFeaturesFilter.md).
39
+
-**Cell Phases** -- typically read from EBSD data alongside the quaternions.
40
+
-**Crystal Structures** -- ensemble-level array read from EBSD data or created by [Create Ensemble Info](CreateEnsembleInfoFilter.md).
26
41
27
42
% Auto generated parameter table will be inserted here
// If we got passed the last check this could happen if the cell points were
159
-
// masked out? Maybe?
160
-
if(counter[i] == 0)
147
+
// If the value of this feature's counter == 0, then there are 2 possibilities
148
+
// that could have happened:
149
+
// [1] The feature's phase was not hexagonal and therefore in the "totalPoints" loop above the counter never got incremented.
150
+
// [2] There is a featureId that does not have any voxels assigned to it. We need more information here to determine what to do.
151
+
// - If we had the "Feature-Phases array then we could look up the phase and then we would know. This would require another input from the user
152
+
// - If the featureId does not have any voxels assigned, then how would you generate an average anyway, so applying NaN to the value is the right move here
153
+
154
+
const usize cAxesIndex = 3 * currentFeatureId;
155
+
if(cellCount[currentFeatureId] == 0)
161
156
{
162
-
avgCAxes[tupleIndex] = 0;
163
-
avgCAxes[tupleIndex + 1] = 0;
164
-
avgCAxes[tupleIndex + 2] = 1;
157
+
avgCAxes[cAxesIndex] = NAN;
158
+
avgCAxes[cAxesIndex + 1] = NAN;
159
+
avgCAxes[cAxesIndex + 2] = NAN;
165
160
}
166
161
else
167
162
{
168
-
// Compute the final average c-axis value
169
-
float value = avgCAxes[3 * i];
170
-
value /= static_cast<float>(counter[i]);
171
-
avgCAxes[3 * i] = value;
172
-
173
-
value = avgCAxes[3 * i + 1];
174
-
value /= static_cast<float>(counter[i]);
175
-
avgCAxes[3 * i + 1] = value;
176
-
177
-
value = avgCAxes[3 * i + 2];
178
-
value /= static_cast<float>(counter[i]);
179
-
avgCAxes[3 * i + 2] = value;
163
+
// Divide the accumulated sum by the cell count, then normalize so the
164
+
// output is a unit-magnitude C-axis direction. The antipodal-flip rule
165
+
// guarantees |sum| >= sqrt(cellCount), so the divided vector's magnitude
166
+
// is >= 1/sqrt(cellCount) > 0 -- no near-zero guard needed.
0 commit comments