Files
Jungfraujoch/rugnux/ModelScaling.cpp
T
leonarski_fandClaude Opus 5 bfe95b4ede
Build Packages / Create release (push) Successful in 16s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m4s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m40s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m54s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m40s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m30s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m21s
Build Packages / build:windows:nocuda (push) Successful in 17m15s
Build Packages / build:windows:cuda (push) Successful in 19m56s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 15m10s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 24m33s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 17m31s
Build Packages / Generate python client (push) Successful in 45s
Build Packages / build:rugnux:windows (push) Successful in 11m0s
Build Packages / Build documentation (push) Successful in 1m21s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 19m34s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m30s
Build Packages / build:rpm (rocky8) (push) Successful in 17m7s
Build Packages / build:rpm (rocky9) (push) Successful in 17m50s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 14m50s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 13m28s
Build Packages / Unit tests (push) Successful in 1h39m47s
analysis: no sort ever sees a NaN key, and FFT kernel launches are checked
A production broker segfaulted inside std::sort in FilterFFTResults: a NaN
length key violates strict weak ordering, and libstdc++'s unguarded partition
scan then walks off the array (confirmed from the deployed binary's faulting
instruction). No legitimate producer of that NaN exists - both FFT back-ends
emit finite lengths - so the row was corrupted, most plausibly via the one
gap in the path: neither kernel launch in FFTIndexerGPU::ExecuteFFT was
error-checked, so a failed launch silently hands back uninitialised device
memory. An audit of every sort/nth_element site then found two more places
where a NaN key is reachable by construction.

- FFTIndexerGPU: cudaGetLastError after both kernel launches (the idiom
  every other GPU translation unit already follows).
- FilterFFTResults: drop non-finite rows when building the magnitude map;
  bit-identical in normal operation.
- SearchSpaceGroup: PearsonCC deliberately returns NaN for an unscorable
  operator (n_pairs < 2, zero variance) and the score sort consumed it
  unfiltered - ~24 operators on a cubic holohedry is past the introsort
  threshold, the same crash waiting to happen. Unscorable operators now
  rank last under a well-defined comparator.
- PostIndexingRefinement: a singular QR-solved cell puts inf into
  cell.inverse() and 0*inf = NaN into the residual norms fed to
  nth_element; non-finite distances now map to +inf, which says exactly
  "this spot does not index" and orders consistently.
- ModelScaling: a NaN R factor from the first grid point latched into
  best_r and won every later comparison; it is now skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 14:05:49 +02:00

86 lines
3.8 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "ModelScaling.h"
#include <algorithm>
#include <cmath>
namespace {
// R-factor of the current parameters over the fitted reflections. This is what the grid is
// selected on, and it is the quantity the scale exists to make small.
double RFactor(const gemmi::Scaling<float> &scaling) {
double num = 0, den = 0;
for (const auto &p : scaling.points) {
num += std::fabs(p.fobs - scaling.compute_value(p));
den += p.fobs;
}
return den > 0 ? num / den : 1.0;
}
} // namespace
// Following the phenix bulk-solvent and scaling procedure: k_sol and b_sol by a grid search, with
// the overall scale and the anisotropic B refitted at every grid point - Afonine, Grosse-Kunstleve
// & Adams, Acta Cryst. D61, 850-855, 2005, which searches b_sol over 10-80 A^2 in steps of 5.
// The fit is unweighted, as in both phenix and Refmac (Murshudov, Skubak, Lebedev, Pannu, Steiner,
// Nicholls, Winn, Long & Vagin, Acta Cryst. D67, 355-367, 2011, eq. 11). The physical range and the
// starting values are those of Fokine & Urzhumtsev, Acta Cryst. D58, 1387-1392, 2002.
//
// The point of the grid is that k_sol and b_sol cannot leave the physical box: gemmi's own
// fit_parameters() is an unbounded Levenberg-Marquardt, and on this corpus it reached b_sol of
// 1707 A^2 - a solvent term switched off in all but the lowest-resolution shell. Here the solvent
// pair is held fixed at each grid point and only the overall scale and the symmetry-constrained
// anisotropic B are refined, which is the well-conditioned half of the problem and is left to
// gemmi's solver rather than reimplemented.
ModelScaleReport FitModelScale(gemmi::Scaling<float> &scaling, ModelScaleBox box) {
ModelScaleReport report;
report.n_points = static_cast<int>(scaling.points.size());
if (scaling.points.size() < 20)
return report;
const bool had_solvent = scaling.use_solvent;
scaling.fix_k_sol = true; // the grid owns the solvent pair; the solver never sees it
scaling.fix_b_sol = true;
double best_r = -1, best_k_sol = 0.35, best_b_sol = 46.0, best_k_overall = 1.0;
gemmi::SMat33<double> best_b_star{0, 0, 0, 0, 0, 0};
auto try_point = [&](double k_sol, double b_sol) {
scaling.k_sol = k_sol;
scaling.b_sol = b_sol;
scaling.fit_isotropic_b_approximately(); // a fresh starting point for this solvent pair
scaling.fit_parameters(); // k_overall + anisotropic B only
++report.n_grid;
const double r = RFactor(scaling);
// A diverged fit gives r = NaN; latched as best_r it wins every later r < best_r.
if (std::isfinite(r) && (best_r < 0 || r < best_r)) {
best_r = r;
best_k_sol = k_sol;
best_b_sol = b_sol;
best_k_overall = scaling.k_overall;
best_b_star = scaling.b_star;
}
};
// Coarse pass over the whole box, then one refinement pass around the winner.
for (double ks = box.k_lo; ks <= box.k_hi + 1e-9; ks += 0.05)
for (double bs = box.b_lo; bs <= box.b_hi + 1e-9; bs += 10.0)
try_point(ks, bs);
const double k0 = best_k_sol, b0 = best_b_sol;
const double k_hi2 = std::min(box.k_hi, k0 + 0.05);
const double b_hi2 = std::min(box.b_hi, b0 + 10.0);
for (double ks = std::max(box.k_lo, k0 - 0.05); ks <= k_hi2 + 1e-9; ks += 0.025)
for (double bs = std::max(box.b_lo, b0 - 10.0); bs <= b_hi2 + 1e-9; bs += 5.0)
try_point(ks, bs);
scaling.k_sol = best_k_sol;
scaling.b_sol = best_b_sol;
scaling.k_overall = best_k_overall;
scaling.b_star = best_b_star;
scaling.use_solvent = had_solvent;
report.r_work_fit = best_r;
return report;
}