Rotation: pass 2 divides partiality by the width pass 2 measured
The second pass of the two-pass rotation loop widens its prediction window to the frame-order-smoothed mosaicity the first pass fitted, taking the max with this frame's own estimate so no reflection is dropped. That widened value was then reported onward as the frame's mosaicity, so it also became the divisor RotationScaleMerge recomputes every partiality from - a number chosen for prediction safety, applied to the intensities. The two uses are not symmetric. Prediction only decides membership: a reflection just inside a generous window arrives with a partiality near zero and is weighted as such, and widening the window by 28% was measured to move the merged statistics by under half a percent. The divisor multiplies every partial, and forcing it 34% wide on a rotation crystal cost ISa 10.7 -> 8.0 and R_meas 12.7 -> 14.5%. Pass 2 has also just re-measured the rocking width against the post-refined cell, which is the better of the two numbers - the literature's own remedy for a mosaicity fitted against a stale cell is exactly to re-estimate it after post-refinement (XDS documents this as a manual second INTEGRATE/CORRECT round). So keep the widened value where it was wanted, on the prediction window, and let the partiality divide by what the frame measured. The predicted reflection population is unchanged. Measured on three rotation crystals at their XDS resolution limits, identical partial counts in every arm: a weak monoclinic ISa 6.3 -> 6.7, R_meas 19.1 -> 18.7%, <I/sigma> 3.3 -> 3.5; tetragonal lysozyme ISa 27.1 -> 27.4, R_meas unchanged at 4.5%; a second monoclinic ISa 13.9 -> 13.8, R_meas unchanged at 12.3%. The full 38-crystal rotation battery then says the defect was almost never active: 36 of 38 crystals are untouched, and the reported per-image mosaicity moves on exactly one of them (0.3380 -> 0.3385 deg). The three-crystal probe above does not reproduce against the current baseline - lysozyme reads 27.81 -> 27.80 - because the max() rarely bites: pass 1's per-frame width is already smooth in frame order, its median frame-to-frame step being 0.0005 deg against a run spread of 0.028, so it seldom exceeds what pass 2 measures for itself. The two crystals that do move are marginal ones whose two-pass lattice search takes a different branch (validation frames 25/60 -> 26/60 and 48/60 -> 49/60); on that evidence their merge numbers measure the branch, not this change. No space group moves. So this lands as a correctness fix with no measurable effect on today's data, not as an improvement: the widened window must not become the divisor, whether or not the two happen to coincide on the crystals we have. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -457,6 +457,13 @@ void IndexAndRefine::QuickPredictAndIntegrate(DataMessage &msg,
|
||||
// report that default onward: RotationScaleMerge averages the reported values in frame order to
|
||||
// recompute every partiality, and a placeholder entered there is read as a measurement.
|
||||
std::optional<float> mos_measured;
|
||||
// The width the PREDICTION window opens to, which is a different question from the width the
|
||||
// partiality divides by. Prediction only decides membership, and a reflection just inside a
|
||||
// generous window arrives with a partiality near zero and is weighted accordingly, so erring wide
|
||||
// there is close to free (measured: a 28% wider window moves the merged statistics by under half a
|
||||
// percent). The divisor is not free - it multiplies every partial - so it must stay on the width
|
||||
// this frame actually measured.
|
||||
std::optional<float> mos_predict;
|
||||
|
||||
if (experiment.GetGoniometer().has_value()) {
|
||||
// Full oscillation wedge of one frame; BraggPredictionRot halves it to the +/- half-wedge of the
|
||||
@@ -474,15 +481,10 @@ void IndexAndRefine::QuickPredictAndIntegrate(DataMessage &msg,
|
||||
// reflections and collapse the multiplicity. (A wider value only helps prediction cover the spot.)
|
||||
if (msg.number >= 0 && msg.number < static_cast<int64_t>(prediction_mosaicity_override_.size())
|
||||
&& std::isfinite(prediction_mosaicity_override_[msg.number])
|
||||
&& prediction_mosaicity_override_[msg.number] > 0.0f) {
|
||||
mos_measured = std::max(mos_measured.value_or(0.0f), prediction_mosaicity_override_[msg.number]);
|
||||
mosaicity[msg.number] = *mos_measured;
|
||||
}
|
||||
&& prediction_mosaicity_override_[msg.number] > 0.0f)
|
||||
mos_predict = std::max(mos_measured.value_or(0.0f), prediction_mosaicity_override_[msg.number]);
|
||||
}
|
||||
// The width the prediction window opens to. It is the measured sigma_M unless a fixed prediction
|
||||
// mosaicity was asked for: the reported (and therefore integrated-against) sigma_M below is left
|
||||
// alone either way, so the two uses of the number can be separated.
|
||||
float mos_deg = mos_measured.value_or(0.1f);
|
||||
float mos_deg = mos_predict.value_or(mos_measured.value_or(0.1f));
|
||||
if (const auto forced_pred = experiment.GetBraggIntegrationSettings().GetForcedPredictionMosaicity_deg())
|
||||
mos_deg = *forced_pred;
|
||||
|
||||
|
||||
@@ -149,7 +149,7 @@ void print_usage() {
|
||||
std::cout << " --capture-uncertainty <num> rot3d: systematic sigma ~num*(1-captured_fraction)*I on under-captured fulls (default: 1.0 for rot3d, 0 otherwise)" << std::endl;
|
||||
std::cout << " --min-captured-fraction <num> rot3d: drop a combined full whose rocking curve was captured below this fraction (edge-of-sweep truncated fulls) (default: 0.7 for rotation, 0 otherwise; 0 = off)" << std::endl;
|
||||
std::cout << " --mosaicity <num> Diagnostic: fix the scaling mosaicity (deg) instead of the per-image seed" << std::endl;
|
||||
std::cout << " --prediction-mosaicity <num> Diagnostic: fix the rocking width the PREDICTION window opens to (deg), leaving the partiality on the per-image sigma_M. Prediction and partiality are one number by default, so a sigma_M that moves takes the integrated reflection population with it" << std::endl;
|
||||
std::cout << " --prediction-mosaicity <num> Diagnostic: fix the rocking width the PREDICTION window opens to (deg), leaving the partiality on the per-image sigma_M. Prediction and partiality are one number by default, so a sigma_M that moves takes the integrated reflection population with it" << std::endl;
|
||||
std::cout << " --reject-outliers <num> Per-observation merge outlier rejection, N sigma from the per-reflection median (default: 6 for rot3d, XDS/DIALS-style; 0 = off)" << std::endl;
|
||||
std::cout << " --search-min-zeta <num> De-novo space-group search only: also search a merge of just the observations whose Lorentz geometry |zeta| reaches this, and keep whichever search found MORE symmetry (default: 0.85 for rotation, 0 = single search). Reflections crossing the Ewald sphere near-tangentially are measured worst and can make a real symmetry operator look like a twin law" << std::endl;
|
||||
std::cout << " --min-image-cc <num> Per-image CC limit in percent (default: no limit)" << std::endl;
|
||||
|
||||
Reference in New Issue
Block a user