scaling: smooth per-frame scale G across frames before the rot3d combine
ScaleOnTheFly fits each frame's scale G independently with no neighbour coupling, so the few partials of one rocking event are weight-summed in combine3D on inconsistent scales - jitter that never enters the full's counting sigma and instead surfaces as scatter between symmetry mates, inflating the error-model b (low ISa). A centered moving average of log(G) over a small frame window (default 9, on for rot3d) removes it, mirroring XDS's smooth scaling. Complementary to --scale-fulls (which rescales between fulls, after the combine): smoothing fixes within-event scale, scale-fulls fixes between-full. On the rotation lysozyme set (1.4A, merged, with --scale-fulls): ISa 11.7 -> 15.0, R_meas 10.0% -> 8.3%, CCref stable, chi2 ~0.97 (honest). Anomalous (full-res): ANODE S-peak 0.61x -> 0.80x of XDS. --smooth-g[=window] tunes/disables it (=0 off); --mosaicity <deg> is a diagnostic that fixes the scaling mosaicity for sweeps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,44 @@ namespace {
|
||||
}
|
||||
logger.Info("Scaled fulls (XDS order, Unity model)");
|
||||
}
|
||||
|
||||
// Smooth the per-frame scale G across frames before the rot3d combine. ScaleOnTheFly fits each
|
||||
// frame's G independently, so the few partials of one rocking event get inconsistent scales when
|
||||
// weight-summed; a centered moving average of log(G) over a small odd frame window removes that
|
||||
// jitter. Only G changed, so each reflection's image_scale_corr (proportional to 1/G) is rescaled
|
||||
// by g_old/g_smooth. Frames without a fitted G (n<MIN_REFLECTIONS) are skipped on both sides.
|
||||
void SmoothImageScaleG(std::vector<IntegrationOutcome> &outcomes, int window, Logger &logger) {
|
||||
const int n = static_cast<int>(outcomes.size());
|
||||
const int half = window / 2;
|
||||
std::vector<double> g_smooth(n, NAN);
|
||||
for (int o = 0; o < n; o++) {
|
||||
double sum_log = 0.0;
|
||||
int count = 0;
|
||||
for (int j = std::max(0, o - half); j <= std::min(n - 1, o + half); j++) {
|
||||
const auto &g = outcomes[j].image_scale_g;
|
||||
if (g && std::isfinite(*g) && *g > 0.0) {
|
||||
sum_log += std::log(*g);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
if (count > 0)
|
||||
g_smooth[o] = std::exp(sum_log / count);
|
||||
}
|
||||
size_t n_smoothed = 0;
|
||||
for (int o = 0; o < n; o++) {
|
||||
const auto &g_old = outcomes[o].image_scale_g;
|
||||
if (!g_old || !std::isfinite(*g_old) || *g_old <= 0.0 || !std::isfinite(g_smooth[o]))
|
||||
continue;
|
||||
const double factor = *g_old / g_smooth[o];
|
||||
for (auto &r : outcomes[o].reflections)
|
||||
if (std::isfinite(r.image_scale_corr))
|
||||
r.image_scale_corr = static_cast<float>(r.image_scale_corr * factor);
|
||||
outcomes[o].image_scale_g = g_smooth[o];
|
||||
n_smoothed++;
|
||||
}
|
||||
logger.Info("Smoothed per-frame scale G over a {}-frame window ({} of {} frames)",
|
||||
window, n_smoothed, n);
|
||||
}
|
||||
}
|
||||
|
||||
JFJochProcess::JFJochProcess(JFJochHDF5Reader &reader, DiffractionExperiment experiment,
|
||||
@@ -436,6 +474,11 @@ ProcessResult JFJochProcess::Run(JFJochProcessObserver *observer) {
|
||||
// merging, so the error model sees counting statistics (high ISa) instead of
|
||||
// rocking-curve slicing scatter.
|
||||
const bool rot3d = experiment_.GetScalingSettings().GetCombine3D();
|
||||
if (rot3d && experiment_.GetScalingSettings().GetSmoothGWindow() > 0) {
|
||||
phase("Smoothing per-frame scale G");
|
||||
SmoothImageScaleG(indexer->GetIntegrationOutcome(),
|
||||
experiment_.GetScalingSettings().GetSmoothGWindow(), logger);
|
||||
}
|
||||
std::vector<IntegrationOutcome> combined;
|
||||
if (rot3d) {
|
||||
phase("Combining 3D partials");
|
||||
|
||||
Reference in New Issue
Block a user