Rotation: a diagnostic that opens the prediction window on its own width

Prediction and partiality read one number, the per-image sigma_M, so a sigma_M
that moves takes the integrated reflection population with it and there is no
way to ask which of the two uses carries a downstream difference.
--prediction-mosaicity fixes the width the prediction window opens to while the
partiality keeps using the measured sigma_M, which separates them.

Measured with it on a rotation crystal whose lattice search returns two
different cells a few tens of microns of detector distance apart: widening the
prediction window from the narrower branch's 0.203 deg to the wider branch's
0.272 deg adds 28% more partials and moves the merged statistics by less than
half a percent (I/sigma 6.1 -> 6.2, R_meas 12.7 -> 12.6%, ISa 10.7 -> 10.9);
narrowing the wide branch the other way removes 26% of its partials and
recovers nothing. On a tetragonal reference crystal a 4.7x over-wide window
costs 11%. The prediction window is not where a mosaicity difference turns into
a merged-data difference - the captured-fraction gate and the partiality
weighting downstream absorb a generous window.

Diagnostic only; off by default, so nothing changes unless it is asked for.
This commit is contained in:
2026-08-12 01:17:51 +02:00
parent 677ece7b59
commit a0604ff244
4 changed files with 42 additions and 1 deletions
+14
View File
@@ -77,6 +77,20 @@ std::optional<float> BraggIntegrationSettings::GetFixedProfileRadius_recipA() co
return fixed_profile_radius;
}
BraggIntegrationSettings & BraggIntegrationSettings::ForcedPredictionMosaicity_deg(std::optional<float> input) {
if (input) {
check_finite("Prediction mosaicity", input.value());
check_min("Prediction mosaicity [deg]", input.value(), 0.001);
check_max("Prediction mosaicity [deg]", input.value(), 10.0);
}
forced_prediction_mosaicity_deg = input;
return *this;
}
std::optional<float> BraggIntegrationSettings::GetForcedPredictionMosaicity_deg() const {
return forced_prediction_mosaicity_deg;
}
BraggIntegrationSettings &BraggIntegrationSettings::Integrator(IntegratorMode input) {
integrator_mode = input;
return *this;
+7
View File
@@ -52,6 +52,11 @@ class BraggIntegrationSettings {
// past it.
std::optional<float> d_min_limit_A;
std::optional<float> fixed_profile_radius;
// Diagnostic: the rocking width the PREDICTION window is opened to, in place of the per-image
// sigma_M. Prediction and partiality use one number today, so a sigma_M that moves takes the
// integrated reflection population with it; pinning this holds the population still while the
// partiality keeps using the measured sigma_M, which separates the two effects.
std::optional<float> forced_prediction_mosaicity_deg;
float minimum_sigma_in_regards_to_i = 0.02;
// The r2..r3 background ring is estimated with ONE of two robust means, never both: a high-side
// sigma-clip (bkg_clip_nsigma, the default) or a symmetric trimmed mean (bkg_trim_fraction). Setting
@@ -126,6 +131,7 @@ public:
BraggIntegrationSettings& StencilKSigma(float input);
BraggIntegrationSettings& DMinLimit_A(std::optional<float> input);
BraggIntegrationSettings& FixedProfileRadius_recipA(std::optional<float> input);
BraggIntegrationSettings& ForcedPredictionMosaicity_deg(std::optional<float> input);
BraggIntegrationSettings& Integrator(IntegratorMode input);
BraggIntegrationSettings& BackgroundTrimFraction(float input);
BraggIntegrationSettings& BackgroundClipNSigma(float input);
@@ -141,6 +147,7 @@ public:
[[nodiscard]] float GetR3() const;
[[nodiscard]] float GetStencilKSigma() const;
[[nodiscard]] std::optional<float> GetFixedProfileRadius_recipA() const;
[[nodiscard]] std::optional<float> GetForcedPredictionMosaicity_deg() const;
[[nodiscard]] std::optional<float> GetDMinLimit_A() const;
[[nodiscard]] float GetMinimumSigmaInRegardsToI() const;
+6 -1
View File
@@ -479,7 +479,12 @@ void IndexAndRefine::QuickPredictAndIntegrate(DataMessage &msg,
mosaicity[msg.number] = *mos_measured;
}
}
const float mos_deg = mos_measured.value_or(0.1f);
// 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);
if (const auto forced_pred = experiment.GetBraggIntegrationSettings().GetForcedPredictionMosaicity_deg())
mos_deg = *forced_pred;
IntegrationOutcome i_outcome{
.geom = outcome.experiment.GetDiffractionGeometry(),
+15
View File
@@ -149,6 +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 << " --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;
@@ -234,6 +235,7 @@ enum {
OPT_CAPTURE_UNCERTAINTY,
OPT_MIN_CAPTURED_FRACTION,
OPT_MOSAICITY,
OPT_PREDICTION_MOSAICITY,
OPT_SMOOTH_G,
OPT_RELATIVE_B,
OPT_NO_SCALING_CORRECTIONS,
@@ -327,6 +329,7 @@ static option long_options[] = {
{"capture-uncertainty", required_argument, nullptr, OPT_CAPTURE_UNCERTAINTY},
{"min-captured-fraction", required_argument, nullptr, OPT_MIN_CAPTURED_FRACTION},
{"mosaicity", required_argument, nullptr, OPT_MOSAICITY},
{"prediction-mosaicity", required_argument, nullptr, OPT_PREDICTION_MOSAICITY},
{"min-image-cc", required_argument, nullptr, OPT_MIN_IMAGE_CC},
{"search-min-zeta", required_argument, nullptr, OPT_SEARCH_MIN_ZETA},
{"scaling-iterations", required_argument, nullptr, OPT_SCALING_ITERATIONS},
@@ -614,6 +617,7 @@ static int RunRugnux(int argc, char **argv) {
std::optional<double> min_captured_fraction_arg; // explicit --min-captured-fraction; default depends on rotation
std::optional<double> capture_uncertainty_arg; // explicit --capture-uncertainty; default depends on rot3d
std::optional<double> forced_mosaicity_arg; // diagnostic: fix the scaling mosaicity (deg) instead of the per-image seed
std::optional<double> forced_prediction_mosaicity_arg; // diagnostic: fix the PREDICTION mosaicity (deg), leaving the partiality on the per-image value
double min_image_cc = 0.0;
std::optional<double> search_min_zeta_arg; // --search-min-zeta; rotation default below
int64_t scaling_iter = 3;
@@ -989,6 +993,9 @@ static int RunRugnux(int argc, char **argv) {
case OPT_MOSAICITY:
forced_mosaicity_arg = parse_double_arg(optarg, "--mosaicity", logger);
break;
case OPT_PREDICTION_MOSAICITY:
forced_prediction_mosaicity_arg = parse_double_arg(optarg, "--prediction-mosaicity", logger);
break;
case OPT_INTEGRATION_RADIUS:
integration_radius_arg = optarg;
break;
@@ -1895,6 +1902,14 @@ static int RunRugnux(int argc, char **argv) {
logger.Info("Stills integration radii default to r1=6.0 r2=8.0 r3=12.0 (override with --integration-radius)");
}
if (forced_prediction_mosaicity_arg) {
BraggIntegrationSettings bis = experiment.GetBraggIntegrationSettings();
bis.ForcedPredictionMosaicity_deg(static_cast<float>(*forced_prediction_mosaicity_arg));
experiment.ImportBraggIntegrationSettings(bis);
logger.Info("Prediction window fixed at a rocking width of {:.4f} deg; the partiality still uses "
"the per-image sigma_M", *forced_prediction_mosaicity_arg);
}
if (integration_stencil_arg) {
const float k = parse_number_arg<float>(integration_stencil_arg->c_str(), "--integration-stencil",
logger, 0.0f, 10.0f);