scale_merge: compute French-Wilson amplitudes at end of merge, write them alongside intensities

Merged reflections carried only intensities; a naive sqrt(max(I,0)) turns every weak or
negative measurement into a biased (or zero) amplitude. Add a French-Wilson step so the
merged reflections themselves carry proper Bayesian amplitudes.

New FrenchWilson.{h,cpp}: ApplyFrenchWilson() fills F and sigmaF on each MergedReflection
with the posterior mean |F| given I and its sigma under the Wilson prior:
  - correct centric vs acentric prior (gemmi is_reflection_centric);
  - epsilon (symmetry-enhancement) multiplicity: the shell mean is <I/eps> and the prior
    mean for a reflection is eps * <I/eps>;
  - numerically stable log-shift integration of the posterior;
  - strong reflections (I > 4 sigma) short-circuit to sqrt(I) where the FW bias is negligible;
  - unusable I/sigma falls back to sqrt(max(I,0)).

It runs as the last step of the merge routine (both MergeOnTheFly and RotationScaleMerge),
so F/sigmaF are part of the merged result and downstream consumers (file writing AND, later,
map calculation) all use the same amplitudes rather than each recomputing FW and risking
divergence. The writers just emit the fields; extended additively:
  - MTZ: F (F) + SIGF (Q) columns;
  - mmCIF: _refln.F_meas_au / _refln.F_meas_sigma_au;
  - HKL text: F sigmaF appended (rfree flag kept in place for back-compat).
Existing intensity columns are unchanged, so current readers keep working while
phenix.refine (etc.) can now refine against the amplitudes.

Verified on lyso (1.3 A): all 173 negative-intensity reflections get F>0, strong reflections
have F/sqrt(I)=1.000, no NaN, and the mmCIF _refln loop is well-formed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 21:27:44 +02:00
co-authored by Claude Opus 4.8
parent 4c95a289b6
commit 87af0f419b
7 changed files with 174 additions and 2 deletions
+11 -2
View File
@@ -181,6 +181,8 @@ void WriteMmcifReflections(const std::vector<MergedReflection> &reflections,
out << "_refln.index_l\n";
out << "_refln.intensity_meas\n";
out << "_refln.intensity_sigma\n";
out << "_refln.F_meas_au\n";
out << "_refln.F_meas_sigma_au\n";
out << "_refln.status_free\n";
out << "_refln.status\n";
@@ -190,6 +192,8 @@ void WriteMmcifReflections(const std::vector<MergedReflection> &reflections,
<< std::setw(5) << r.l << " "
<< std::setw(14) << Fmt(r.I, 4) << " "
<< std::setw(14) << Fmt(r.sigma, 4) << " "
<< std::setw(14) << Fmt(r.F, 4) << " "
<< std::setw(14) << Fmt(r.sigmaF, 4) << " "
<< (r.rfree_flag ? 1 : 0) << " "
<< "o" // 'o' = observed
<< "\n";
@@ -224,13 +228,15 @@ void WriteMtzReflections(const std::vector<MergedReflection> &reflections,
mtz.add_column("L", 'H', dataset_id, -1, false);
mtz.add_column("IMEAN", 'J', dataset_id, -1, false);
mtz.add_column("SIGIMEAN", 'Q', dataset_id, -1, false);
mtz.add_column("F", 'F', dataset_id, -1, false); // French-Wilson amplitude
mtz.add_column("SIGF", 'Q', dataset_id, -1, false);
mtz.add_column("FreeR_flag", 'I', dataset_id, -1, false);
// Number of rows
mtz.nreflections = reflections.size();
// Allocate data table
mtz.data.reserve(reflections.size() * 6);
mtz.data.reserve(reflections.size() * 8);
for (const auto& r : reflections) {
mtz.data.push_back(static_cast<float>(r.h));
@@ -238,6 +244,8 @@ void WriteMtzReflections(const std::vector<MergedReflection> &reflections,
mtz.data.push_back(static_cast<float>(r.l));
mtz.data.push_back(r.I);
mtz.data.push_back(r.sigma);
mtz.data.push_back(r.F);
mtz.data.push_back(r.sigmaF);
mtz.data.push_back(r.rfree_flag ? 1 : 0);
}
@@ -254,7 +262,8 @@ void WriteHKLReflections(const std::vector<MergedReflection> &reflections,
for (const auto &r: reflections)
hkl_file << r.h << " " << r.k << " " << r.l << " "
<< r.I << " " << r.sigma << " "
<< (r.rfree_flag ? 1 : 0) << std::endl;
<< (r.rfree_flag ? 1 : 0) << " "
<< r.F << " " << r.sigmaF << std::endl;
hkl_file.close();
}