Skip to content

Commit 5c8c993

Browse files
committed
BUG: CubicOps - Recover precision for misorientations on cubic sym ops
In CubicOps::calculateMisorientationInternal, compute the explicit reduced quaternion's vector components for each of the three sym-op candidates and extract the angle as 2*atan2(|v|, w) instead of 2*acos(w). The acos(w)-near-1 form was catastrophically ill-conditioned for cubic misorientations that lie on a 4-fold (90° about c-axis), 3-fold (120° about [111]), or 2-fold sym op. With float32-stored input quaternions promoted to double, (qco.z()+qco.w())/sqrt(2) lands at ~1-1.7e-8 instead of 1.0; acos then amplifies the ULP-level noise to ~2e-4 rad ≈ 0.02° residual. The new form recovers the precision: explicit components like (qco.z - qco.w) evaluate to exactly 0 in IEEE 754 when qco.z == qco.w, regardless of upstream float32 truncation noise. The sqrt(1 - w*w) form inherits the lost precision; the explicit-component form does not.
1 parent edde3bf commit 5c8c993

1 file changed

Lines changed: 58 additions & 52 deletions

File tree

Source/EbsdLib/LaueOps/CubicOps.cpp

Lines changed: 58 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -520,69 +520,75 @@ AxisAngleDType CubicOps::calculateMisorientationInternal(const std::vector<QuatD
520520
}
521521
}
522522
}
523-
wmin = qco.w();
524-
if(((qco.z() + qco.w()) / (ebsdlib::constants::k_Sqrt2D)) > wmin)
525-
{
526-
wmin = ((qco.z() + qco.w()) / (ebsdlib::constants::k_Sqrt2D));
527-
type = 2;
528-
}
529-
if(((qco.x() + qco.y() + qco.z() + qco.w()) / 2) > wmin)
530-
{
531-
wmin = ((qco.x() + qco.y() + qco.z() + qco.w()) / 2);
532-
type = 3;
533-
}
534-
if(wmin < -1.0)
535-
{
536-
// wmin = -1.0;
537-
wmin = ebsdlib::constants::k_ACosNeg1D;
538-
sin_wmin_over_2 = std::sin(wmin);
539-
}
540-
else if(wmin > 1.0)
541-
{
542-
// wmin = 1.0;
543-
wmin = ebsdlib::constants::k_ACos1D;
544-
sin_wmin_over_2 = std::sin(wmin);
523+
// Three candidate symmetry-op reductions of qco. For each "type" (sym op class),
524+
// compute BOTH the candidate cos(half-angle) AND the vector components (vx,vy,vz)
525+
// of the reduced quaternion explicitly. Choosing the largest cos(half-angle)
526+
// picks the sym op that minimizes the misorientation angle.
527+
//
528+
// The angle is then 2 * atan2(|v|, w) using the EXPLICIT v from the reduced
529+
// quaternion -- rather than sqrt(1 - w*w) -- which preserves precision through
530+
// symmetry-op cancellations. When qco encodes a sym op exactly, the v components
531+
// collapse to subtractions of identical floats (e.g., (qco.z - qco.w) when
532+
// qco.z == qco.w), which yield exactly 0 in IEEE 754. The sqrt(1 - w*w) form
533+
// loses this precision because w is computed via sums/divisions that do not
534+
// preserve the ULP structure of the inputs -- a real concern when AvgQuats
535+
// is stored as float32 and promoted to double inside the calculation.
536+
537+
// Type 1: identity (no sym op applied) -- reduced quaternion is qco itself
538+
double wCand = qco.w();
539+
double vx = qco.x();
540+
double vy = qco.y();
541+
double vz = qco.z();
542+
type = 1;
543+
544+
// Type 2: 90 deg about z; sym op (0, 0, 1, 1) / sqrt(2)
545+
{
546+
const double w2 = (qco.z() + qco.w()) / ebsdlib::constants::k_Sqrt2D;
547+
if(w2 > wCand)
548+
{
549+
wCand = w2;
550+
vx = (qco.x() - qco.y()) / ebsdlib::constants::k_Sqrt2D;
551+
vy = (qco.x() + qco.y()) / ebsdlib::constants::k_Sqrt2D;
552+
vz = (qco.z() - qco.w()) / ebsdlib::constants::k_Sqrt2D;
553+
type = 2;
554+
}
545555
}
546-
else
556+
557+
// Type 3: 120 deg about [1, 1, 1]; sym op (1, 1, 1, 1) / 2
547558
{
548-
wmin = acos(wmin);
549-
sin_wmin_over_2 = std::sin(wmin);
559+
const double w3 = (qco.x() + qco.y() + qco.z() + qco.w()) / 2.0;
560+
if(w3 > wCand)
561+
{
562+
wCand = w3;
563+
vx = (qco.x() - qco.y() + qco.z() - qco.w()) / 2.0;
564+
vy = (qco.x() + qco.y() - qco.z() - qco.w()) / 2.0;
565+
vz = (-qco.x() + qco.y() + qco.z() - qco.w()) / 2.0;
566+
type = 3;
567+
}
550568
}
551569

570+
// Stable angle: 2 * atan2(|v|, w). Clamp w only to defend against ULP excess
571+
// above 1.0 from float roundoff (this does NOT affect the precision-recovery
572+
// path -- precision is recovered by computing |v| from the explicit v terms).
573+
sin_wmin_over_2 = std::sqrt(vx * vx + vy * vy + vz * vz);
574+
const double clampedW = std::clamp(wCand, -1.0, 1.0);
575+
wmin = 2.0 * std::atan2(sin_wmin_over_2, clampedW);
576+
577+
// Axis = v / |v|. When |v| == 0 the reduced quaternion is identity (angle 0,
578+
// axis undefined); use [0, 0, 1] by convention.
552579
double n1 = 0.0;
553580
double n2 = 0.0;
554581
double n3 = 0.0;
555-
if(type == 1)
556-
{
557-
n1 = qco.x() / sin_wmin_over_2;
558-
n2 = qco.y() / sin_wmin_over_2;
559-
n3 = qco.z() / sin_wmin_over_2;
560-
}
561-
if(type == 2)
582+
if(sin_wmin_over_2 > 0.0)
562583
{
563-
n1 = ((qco.x() - qco.y()) / (ebsdlib::constants::k_Sqrt2D)) / sin_wmin_over_2;
564-
n2 = ((qco.x() + qco.y()) / (ebsdlib::constants::k_Sqrt2D)) / sin_wmin_over_2;
565-
n3 = ((qco.z() - qco.w()) / (ebsdlib::constants::k_Sqrt2D)) / sin_wmin_over_2;
584+
n1 = vx / sin_wmin_over_2;
585+
n2 = vy / sin_wmin_over_2;
586+
n3 = vz / sin_wmin_over_2;
566587
}
567-
if(type == 3)
568-
{
569-
n1 = ((qco.x() - qco.y() + qco.z() - qco.w()) / (2.0)) / sin_wmin_over_2;
570-
n2 = ((qco.x() + qco.y() - qco.z() - qco.w()) / (2.0)) / sin_wmin_over_2;
571-
n3 = ((-qco.x() + qco.y() + qco.z() - qco.w()) / (2.0)) / sin_wmin_over_2;
572-
}
573-
double denom = sqrt((n1 * n1 + n2 * n2 + n3 * n3));
574-
n1 = n1 / denom;
575-
n2 = n2 / denom;
576-
n3 = n3 / denom;
577-
if(denom == 0)
578-
{
579-
n1 = 0.0, n2 = 0.0, n3 = 1.0;
580-
}
581-
if(wmin == 0)
588+
else
582589
{
583-
n1 = 0.0, n2 = 0.0, n3 = 1.0;
590+
n3 = 1.0;
584591
}
585-
wmin = 2.0f * wmin;
586592

587593
AxisAngleDType axisAngle(n1, n2, n3, wmin);
588594
return axisAngle;

0 commit comments

Comments
 (0)