Anisotropy: do not report a shape the fit never produced, and read every axis
FitShape set out.shells before running the regressions and returned early if either weighted least-squares call failed, leaving NaN coefficients with flat and convex both false. AnalyzeAnisotropy tested exactly those two, so it read the failure as Linear and marked it stable: the report and _reflns.jfjoch_aniso_shape claimed LINEAR over a shell count that suggested it meant something, with nan printed beside it. ShapeFit now carries an explicit ok, set only once every regression has succeeded, and result.shape_shells reports the shells the signature was actually fitted over - none, where it was not - so the renderer and the rebinning caution skip it and the verdict stays Undetermined. The directional-limit line tested the first and third principal axes for a finite d_min but not the second. ConeLimits fills each independently, so a crystal whose outer two directions fall in cones too sparse to measure while the middle one is well covered suppressed the whole line, the d_min spread with it. All three are tested now. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016L1qig74oYQzfUJJZbbxFh
This commit is contained in:
@@ -612,6 +612,10 @@ namespace {
|
||||
|
||||
// ---------------------------------------------------------------- the s^2 signature
|
||||
struct ShapeFit {
|
||||
// The regressions below all ran. Distinct from shells: there can be plenty of shells and
|
||||
// still no fit, and a ShapeFit that returned early carries NaN coefficients with flat and
|
||||
// convex both false - which reads as a clean "Linear" to anyone testing those two alone.
|
||||
bool ok = false;
|
||||
int shells = 0;
|
||||
double c0 = NAN, c0_err = NAN, c1 = NAN, slope_through_origin = NAN;
|
||||
double curvature = NAN, curvature_err = NAN, curvature_share = NAN;
|
||||
@@ -771,6 +775,7 @@ namespace {
|
||||
out.convex = !out.flat && out.curvature > 0.0 && out.curvature_err > 0.0
|
||||
&& out.curvature / out.curvature_err > SHAPE_Z
|
||||
&& out.curvature_share > CONVEX_SHARE;
|
||||
out.ok = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -1236,14 +1241,17 @@ AnisotropyResult AnalyzeAnisotropy(const std::vector<MergedReflection> &merged,
|
||||
|
||||
// --- the resolution signature, and whether the binning decides it ---
|
||||
const ShapeFit shape = FitShape(cells, constrained.b);
|
||||
result.shape_shells = shape.shells;
|
||||
// A fit that did not run was fitted over no shells. Reported that way rather than as the shells
|
||||
// it had available, so the renderer and the cautions below do not print NaN coefficients beside a
|
||||
// shell count that suggests they mean something.
|
||||
result.shape_shells = shape.ok ? shape.shells : 0;
|
||||
result.shape_intercept = shape.c0;
|
||||
result.shape_intercept_z = shape.c0_err > 0.0 ? shape.c0 / shape.c0_err : NAN;
|
||||
result.shape_slope = shape.c1;
|
||||
result.shape_curvature_z = shape.curvature_err > 0.0 ? shape.curvature / shape.curvature_err : NAN;
|
||||
result.shape_curvature_share = shape.curvature_share;
|
||||
result.shape_residual = shape.residual;
|
||||
if (shape.shells >= MIN_SHAPE_SHELLS) {
|
||||
if (shape.ok && shape.shells >= MIN_SHAPE_SHELLS) {
|
||||
result.shape = shape.flat ? AnisotropyShape::Flat
|
||||
: shape.convex ? AnisotropyShape::Convex : AnisotropyShape::Linear;
|
||||
if (std::isfinite(shape.slope_through_origin) && shape.slope_through_origin != 0.0) {
|
||||
@@ -1262,7 +1270,7 @@ AnisotropyResult AnalyzeAnisotropy(const std::vector<MergedReflection> &merged,
|
||||
break;
|
||||
}
|
||||
const ShapeFit alt_shape = FitShape(alt_cells, alt_fit.b);
|
||||
if (alt_shape.shells < MIN_SHAPE_SHELLS || alt_shape.flat != shape.flat) {
|
||||
if (!alt_shape.ok || alt_shape.shells < MIN_SHAPE_SHELLS || alt_shape.flat != shape.flat) {
|
||||
result.shape_stable = false;
|
||||
break;
|
||||
}
|
||||
@@ -1341,7 +1349,7 @@ AnisotropyResult AnalyzeAnisotropy(const std::vector<MergedReflection> &merged,
|
||||
result.cautions.emplace_back(
|
||||
"the directional deficit does not follow exp(-1/2 s^T B s), so the fitted deltaB is a fit "
|
||||
"of the wrong functional form and may be an under-estimate");
|
||||
if (result.shape == AnisotropyShape::Undetermined && shape.shells >= MIN_SHAPE_SHELLS)
|
||||
if (result.shape == AnisotropyShape::Undetermined && result.shape_shells >= MIN_SHAPE_SHELLS)
|
||||
result.cautions.emplace_back(
|
||||
"the resolution signature changed when the shells were rebinned, so it is reported as "
|
||||
"undetermined rather than as a measurement");
|
||||
@@ -1391,7 +1399,11 @@ std::string AnisotropyToText(const AnisotropyResult &result) {
|
||||
os << "\n";
|
||||
if (std::isfinite(result.fold_weakening))
|
||||
os << " Strongest / weakest direction at the resolution limit: " << result.fold_weakening << "x\n";
|
||||
if (std::isfinite(result.d_min_axis[0]) || std::isfinite(result.d_min_axis[2])) {
|
||||
// Every axis, not just the outer two: ConeLimits fills each independently, so a crystal whose
|
||||
// first and third directions fall in cones too sparse to measure - but whose second is well
|
||||
// covered - would otherwise suppress the whole line, d_min_spread included.
|
||||
if (std::isfinite(result.d_min_axis[0]) || std::isfinite(result.d_min_axis[1])
|
||||
|| std::isfinite(result.d_min_axis[2])) {
|
||||
bool any_censored = false;
|
||||
os << " d_min along the principal directions (<I/sigma(I)> = " << CONE_I_OVER_SIGMA
|
||||
<< " in a " << CONE_HALF_ANGLE_DEG << " deg cone):";
|
||||
|
||||
Reference in New Issue
Block a user