Merge hq-cure-merge into hq-pool3; R_MEAS stays the ordinary statistic, the weighted one is R_MEAS_WEIGHTED

R_MEAS is what users set beside XDS/AIMLESS, so its meaning does not change; the merge-weighted value
from hq-cure-merge is reported beside it (R_MEAS_WEIGHTED, REFRES_R_MEAS_WEIGHTED). The per-hand
table keeps the ordinary statistic.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1G8gJVAy6gp1K5Dz3NE5C
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-09-25 06:05:46 +02:00
commit 9e510816e0
10 files changed
+162 -27

No files matched your search

+2
View File
@@ -9,6 +9,8 @@
* Rugnux accepts a detector-modulation or absorption correction surface when held-out data support it on Fisher's z, and fits them in the order modulation, time, goniometer frame; this recovers corrections that were wrongly refused.
* Rugnux settles a disagreement between the file's and the measured beam centre over an axis harmonic in either direction, so a file centre off along the spindle no longer leaves the run on a doubled axis.
* Rugnux commits a post-refined geometry only when the held-out residual falls by more than its own noise, instead of by a fixed 2 %.
* Rugnux also reports `R_MEAS_WEIGHTED`, the R_meas with each observation weighted as the merge weights it, so weak frames kept in the merge at low weight can be told from real disagreement; `R_MEAS` is unchanged.
* Rugnux no longer stops per-frame scaling as unsettled because of a single frame that is about to be dropped as blank.
* `rugnux --polarization` is documented as the polarization degree (XDS `FRACTION_OF_POLARIZATION` = (1 + p)/2); the default 0.99 suits undulators.
### 1.0.0-rc.172
+9
View File
@@ -51,6 +51,15 @@ not `-A` was given, but where no Bijvoet pair could be split in both hands — a
`-A`, or too few pairs — the quantity does not exist and the key is absent; `COMPLETENESS=`,
`MULTIPLICITY=`, `I_OVER_SIGMA=`, `R_MEAS=`, `CC_HALF=` and `WILSON_B=` follow the same rule.
**`R_MEAS=`** is the ordinary R_meas, every observation counted once, as XDS and AIMLESS count it - the
one to set beside theirs. **`R_MEAS_WEIGHTED=`** (and `REFRES_R_MEAS_WEIGHTED=`) weights each
observation as the merge weights it - by 1/sigma^2 under the error model - with each reflection's
weights normalised to their effective number, so where a reflection's observations share one sigma it
equals `R_MEAS=`. A stretch the crystal barely diffracted over is in the merge at the small weight its
scaled-up counting error gives it, and is in `R_MEAS_WEIGHTED=` at that weight too, rather than setting
the number with its noise; a large gap between the two says weak frames are kept at low weight.
`MULTIPLICITY=` stays a count of observations.
The last blocks before `END OF REPORT` are the authorship and the acknowledgement: who wrote
rugnux, its licence (GPLv3 — free to use for academic institutions and commercial companies alike)
and where releases are published, then the credit to the X-ray research community whose methods
+13
View File
@@ -756,6 +756,19 @@ const char *SweepQualityReasonText(SweepQualityReason reason) {
return "unknown";
}
bool WeightedRmeasTerms(double sum_v_absdev, double sum_v_I, double sum_v, double sum_v2,
double &num, double &den) {
if (!(sum_v > 0.0) || !(sum_v2 > 0.0))
return false;
const double n_eff = sum_v * sum_v / sum_v2;
if (!(n_eff > 1.0))
return false;
const double scale = n_eff / sum_v;
num = std::sqrt(n_eff / (n_eff - 1.0)) * scale * sum_v_absdev;
den = scale * sum_v_I;
return true;
}
const char *FrameDispositionCode(FrameDisposition disposition) {
switch (disposition) {
case FrameDisposition::Merged: return "merged";
+12
View File
@@ -35,6 +35,10 @@ struct MergeStatisticsShell {
// Redundancy-independent merging R-factor (Diederichs & Karplus 1997), computed over the
// observations that enter the merge: R_meas = sum_hkl sqrt(n/(n-1)) sum_i|I_i-<I>| / sum I_i.
double r_meas = NAN;
// The same with each observation weighted as the rotation merge weights it, 1/sigma^2 under the
// error model (WeightedRmeasTerms); with equal sigmas it is r_meas. A frame kept at a small weight
// then no longer sets the number with its noise. NaN where it is not computed (stills, per hand).
double r_meas_weighted = NAN;
// Anomalous signal-to-noise (XDS "SigAno" / mmCIF pdbx_absDiff_over_sigma_anomalous):
// <|I(+)-I(-)|> / <sigma(I(+)-I(-))> over acentric reflections measured in both hands. NaN when
@@ -77,6 +81,14 @@ enum class FrameDisposition {
const char *FrameDispositionCode(FrameDisposition disposition); // "merged", "downgraded", "rejected"
// One reflection's share of R_meas when its observations carry weights v_i: given sum v|I_i - <I>|,
// sum v I_i, sum v and sum v^2, the numerator sqrt(n_eff/(n_eff-1)) sum v'|I_i - <I>| and the
// denominator sum v' I_i, with the weights rescaled to v' = v n_eff / sum v so they add up to Kish's
// effective count n_eff = (sum v)^2 / sum v^2. Equal weights give the ordinary R_meas terms. False when
// the reflection has no second effective observation (n_eff <= 1), which then counts in neither sum.
bool WeightedRmeasTerms(double sum_v_absdev, double sum_v_I, double sum_v, double sum_v2,
double &num, double &den);
struct SweepQualityRange {
// Inclusive, in processed-image ordinals - the numbering of <prefix>_plot.txt and of every other
// per-image array rugnux writes. With -s/--stride the source image is start + ordinal * stride.
@@ -1568,10 +1568,23 @@ RotationScaleMerge::ScalingLoopOutcome RotationScaleMerge::RunScalingLoop(
}
rescale(scaled_any, ratio);
if (it > 0) {
// A frame already below the credible floor is not part of the fit's answer: the window
// leaves it out (ComputeSmoothGWindow) and the guard after the loop drops it. Left in the
// step, a single such frame held the rms at 0.18 and the loop was stopped as unsettled after
// 16 rounds; with it left out the same loop settles in 23. On that sweep every loop that
// stopped unsettled was one that went on to drop a frame.
std::vector<double> fitted;
for (int f = 0; f < n_frames; ++f)
if (scaled_any[f] && g[f] > 0.0) fitted.push_back(g[f]);
double g_floor = 0.0;
if (!fitted.empty()) {
std::nth_element(fitted.begin(), fitted.begin() + fitted.size() / 2, fitted.end());
g_floor = fitted[fitted.size() / 2] * MIN_CREDIBLE_SCALE_RATIO;
}
double ss = 0.0, sw = 0.0;
int n = 0;
for (int f = 0; f < n_frames; ++f)
if (scaled_any[f] && g_prev[f] > 0.0 && g[f] > 0.0) {
if (scaled_any[f] && g_prev[f] > 0.0 && g[f] > 0.0 && g[f] >= g_floor) {
const double d = std::log(g[f] / g_prev[f]);
ss += frame_obs_count[f] * d * d;
sw += frame_obs_count[f];
@@ -4755,21 +4768,37 @@ RotationScaleMerge::Result RotationScaleMerge::MergeAndStats(int n_groups, bool
std::vector<double> rmeas_num(n_shells, 0.0), rmeas_den(n_shells, 0.0);
double rmeas_num_all = 0.0, rmeas_den_all = 0.0;
std::vector<double> urmeas_num(n_shells, 0.0), urmeas_den(n_shells, 0.0);
double urmeas_num_all = 0.0, urmeas_den_all = 0.0;
// R_meas and the observation counts behind the reported multiplicity. Report-only, and the last
// full pass over the observations there is - so, like the correction surfaces, it is not made on a
// merge whose numbers nobody reads.
if (full_stats) {
// R_meas: re-walk the fulls (Mask = cell only; no ice / error-model), accumulate
// |I_i - <I>| per reflection.
struct RmeasObs { double sum_abs_dev = 0, sum_I = 0; int n = 0, shell = -1; };
// R_meas: re-walk the fulls (Mask = cell only; no ice), accumulate |I_i - <I>| per
// reflection - each observation weighted as the merge weights it, v = 1/sigma^2 under the
// error model (corrected_sigma). The merge carries an observation from a frame the crystal
// barely diffracted on at the small weight its scaled-up counting error gives it; counted at
// full weight in R_meas, that frame's noise set the number while adding next to nothing to
// the intensities it describes. Where the error model's proportional term dominates - the
// strong reflections - the merge weights every frame alike, and so does this. Measured: a
// sweep keeping 115 weak frames (scale 0.1-0.2 of the run's) read the same CC1/2, <I/sigma>
// and CC to the model as the same sweep with 84 frames rejected, and R_meas 1.3-1.5x higher
// in every shell. Per reflection the weights are normalised to Kish's effective count
// n_eff = (sum v)^2 / sum v^2, which is n where the sigmas are equal, and there this is the
// ordinary R_meas = sum sqrt(n/(n-1)) sum|I_i - <I>| / sum I_i.
// The unweighted sums beside them give the ordinary r_meas; the weighted ones r_meas_weighted.
struct RmeasObs { double sum_abs_dev = 0, sum_I = 0, w_abs_dev = 0, w_I = 0, sum_v = 0, sum_v2 = 0;
int n = 0, shell = -1; };
std::vector<RmeasObs> rmeas(n_groups);
bool did_gpu_rmeas = false;
#ifdef JFJOCH_USE_CUDA
if (use_gpu_merge) {
// Per-group R_meas + usable count on the GPU; the shell is assigned per group (its fulls share d).
std::vector<double> rabsdev(n_groups), rsumI(n_groups);
std::vector<double> rabsdev(n_groups), rsumI(n_groups), rwabsdev(n_groups), rwsumI(n_groups),
rsumv(n_groups), rsumv2(n_groups);
std::vector<int32_t> rn(n_groups), rnusable(n_groups);
gpu_->MergeRmeas(merged_I.data(), rabsdev.data(), rsumI.data(), rn.data(), rnusable.data());
gpu_->MergeRmeas(merged_I.data(), rabsdev.data(), rsumI.data(), rwabsdev.data(), rwsumI.data(),
rsumv.data(), rsumv2.data(), rn.data(), rnusable.data());
for (int g = 0; g < n_groups; ++g) {
if (rnusable[g] == 0 || acc[g].d < d_floor) continue;
const auto shell = shells.GetShell(acc[g].d);
@@ -4783,7 +4812,9 @@ RotationScaleMerge::Result RotationScaleMerge::MergeAndStats(int n_groups, bool
sa[*shell].total_obs += static_cast<int>(acc[g].nh[0] + acc[g].nh[1]);
if (std::isfinite(merged_I[g]) && rn[g] > 0) {
auto &r = rmeas[g];
r.sum_abs_dev = rabsdev[g]; r.sum_I = rsumI[g]; r.n = rn[g]; r.shell = *shell;
r.sum_abs_dev = rabsdev[g]; r.sum_I = rsumI[g]; r.w_abs_dev = rwabsdev[g]; r.w_I = rwsumI[g];
r.sum_v = rsumv[g]; r.sum_v2 = rsumv2[g];
r.n = rn[g]; r.shell = *shell;
}
}
did_gpu_rmeas = true;
@@ -4806,15 +4837,24 @@ RotationScaleMerge::Result RotationScaleMerge::MergeAndStats(int n_groups, bool
sa[*shell].total_obs++;
if (std::isfinite(merged_I[o.group])) {
auto &r = rmeas[o.group];
r.sum_abs_dev += std::fabs(static_cast<double>(I_corr) - merged_I[o.group]);
r.sum_I += I_corr; r.n++; r.shell = *shell;
const double sc = corrected_sigma(o, I_corr, sigma_corr);
const double v = 1.0 / (sc * sc);
const double dev = std::fabs(static_cast<double>(I_corr) - merged_I[o.group]);
r.sum_abs_dev += dev; r.sum_I += I_corr;
r.w_abs_dev += v * dev; r.w_I += v * I_corr; r.sum_v += v; r.sum_v2 += v * v;
r.n++; r.shell = *shell;
}
}
for (const auto &r : rmeas) {
if (r.n < 2 || r.shell < 0 || r.shell >= n_shells) continue;
double num, den;
if (WeightedRmeasTerms(r.w_abs_dev, r.w_I, r.sum_v, r.sum_v2, num, den)) {
rmeas_num[r.shell] += num; rmeas_den[r.shell] += den;
rmeas_num_all += num; rmeas_den_all += den;
}
const double factor = std::sqrt(static_cast<double>(r.n) / (r.n - 1));
rmeas_num[r.shell] += factor * r.sum_abs_dev; rmeas_den[r.shell] += r.sum_I;
rmeas_num_all += factor * r.sum_abs_dev; rmeas_den_all += r.sum_I;
urmeas_num[r.shell] += factor * r.sum_abs_dev; urmeas_den[r.shell] += r.sum_I;
urmeas_num_all += factor * r.sum_abs_dev; urmeas_den_all += r.sum_I;
}
}
@@ -5027,7 +5067,8 @@ RotationScaleMerge::Result RotationScaleMerge::MergeAndStats(int n_groups, bool
ss.mean_i_over_sigma = sa[s].n_i_over_sigma > 0 ? sa[s].sum_i_over_sigma / sa[s].n_i_over_sigma : 0.0;
ss.cc_half = sa[s].cc_half.GetCC();
ss.cc_ref = NAN;
ss.r_meas = rmeas_den[s] > 0.0 ? rmeas_num[s] / rmeas_den[s] : NAN;
ss.r_meas = urmeas_den[s] > 0.0 ? urmeas_num[s] / urmeas_den[s] : NAN;
ss.r_meas_weighted = rmeas_den[s] > 0.0 ? rmeas_num[s] / rmeas_den[s] : NAN;
ss.abs_diff_over_sigma_anomalous = sig_den[s] > 0.0 ? sig_num[s] / sig_den[s] : NAN;
ss.cc_anom = cc_anom_shell[s].GetCC();
}
@@ -5042,7 +5083,8 @@ RotationScaleMerge::Result RotationScaleMerge::MergeAndStats(int n_groups, bool
overall.mean_i_over_sigma = n_ios > 0 ? sum_ios / n_ios : 0.0;
overall.cc_half = cc_half_overall.GetCC();
overall.cc_ref = NAN;
overall.r_meas = rmeas_den_all > 0.0 ? rmeas_num_all / rmeas_den_all : NAN;
overall.r_meas = urmeas_den_all > 0.0 ? urmeas_num_all / urmeas_den_all : NAN;
overall.r_meas_weighted = rmeas_den_all > 0.0 ? rmeas_num_all / rmeas_den_all : NAN;
overall.abs_diff_over_sigma_anomalous = sig_den_all > 0.0 ? sig_num_all / sig_den_all : NAN;
overall.cc_anom = cc_anom_overall.GetCC();
@@ -5062,6 +5104,7 @@ RotationScaleMerge::Result RotationScaleMerge::MergeAndStats(int n_groups, bool
? ha[sh].sum_i_over_sigma / ha[sh].n_i_over_sigma : 0.0;
hs.cc_half = ha[sh].cc_half.GetCC();
hs.r_meas = ha[sh].rmeas_den > 0.0 ? ha[sh].rmeas_num / ha[sh].rmeas_den : NAN;
hs.r_meas_weighted = NAN;
out.per_hand.overall.total_observations += ha[sh].total_obs;
out.per_hand.overall.unique_reflections += ha[sh].unique;
out.per_hand.overall.possible_unique_reflections += ha[sh].possible;
@@ -450,7 +450,7 @@ namespace {
double *a_swI, *a_sw, *a_swIh0, *a_swIh1, *a_swh0, *a_swh1, *a_swht0, *a_swht1, *a_d;
int32_t *a_nh0, *a_nh1, *a_rejected;
uint8_t *a_on_ice;
double *r_absdev, *r_sumI;
double *r_absdev, *r_sumI, *r_wabsdev, *r_wsumI, *r_sumv, *r_sumv2;
int32_t *r_n, *r_nusable;
uint8_t *rejected_obs; // per-full: set by MergeAccum (outlier-rejected), read by MergeRmeas
};
@@ -588,8 +588,10 @@ namespace {
}
}
// One thread per group: R_meas accumulators (sum|I_corr - merged_I|, sum_I, n) + the count of
// observations this looser walk accepted. Mirrors MergeAndStats' R_meas re-walk (cell-only filter).
// One thread per group: R_meas accumulators (sum|I_corr - merged_I|, sum I_corr, the same weighted by
// v = the observation's merge weight 1/sigma^2 under the error model (as MergeAccumKernel weights it),
// sum v, sum v^2, n) + the count of observations this looser walk accepted.
// Mirrors MergeAndStats' R_meas re-walk (cell-only filter).
// That count is NOT the per-shell total_observations - it is wider than the merge, so it would
// over-report multiplicity; the host uses it only to skip empty groups.
__global__ void MergeRmeasKernel(MergeParams p) {
@@ -597,7 +599,7 @@ namespace {
const int lo = p.gstart[g], hi = lo + p.gcount[g];
const double mI = p.merged_I[g];
const bool have = isfinite(mI);
double absdev = 0, sumI = 0;
double absdev = 0, sumI = 0, wabsdev = 0, wsumI = 0, sumv = 0, sumv2 = 0;
int n = 0, nusable = 0;
for (int q = lo; q < hi; ++q) {
const int i = p.gperm[q];
@@ -607,10 +609,19 @@ namespace {
++nusable;
if (have) {
const double I_corr = double(p.I[i]) * p.corr[i];
absdev += fabs(I_corr - mI); sumI += I_corr; ++n;
const float sigma_raw = p.sigma[i] * p.corr[i];
const double sc = p.error_model_active
? ModelSigma(i, p, isfinite(p.em_mean[g]) ? p.em_mean[g] : I_corr, sigma_raw)
: sigma_raw;
const double v = 1.0 / (sc * sc);
const double dev = fabs(I_corr - mI);
absdev += dev; sumI += I_corr;
wabsdev += v * dev; wsumI += v * I_corr; sumv += v; sumv2 += v * v; ++n;
}
}
p.r_absdev[g] = absdev; p.r_sumI[g] = sumI; p.r_n[g] = n; p.r_nusable[g] = nusable;
p.r_absdev[g] = absdev; p.r_sumI[g] = sumI; p.r_wabsdev[g] = wabsdev; p.r_wsumI[g] = wsumI;
p.r_sumv[g] = sumv; p.r_sumv2[g] = sumv2;
p.r_n[g] = n; p.r_nusable[g] = nusable;
}
}
@@ -670,10 +681,12 @@ struct RotationScaleMergeGPU::Impl {
CudaDevicePtr<uint8_t> a_on_ice; // per group: any contributing full on an ice ring
CudaDevicePtr<float> reject_median;
CudaDevicePtr<float> reject_var_add; // the pooled test's widening
CudaDevicePtr<double> merged_I, r_absdev, r_sumI; // R_meas per group (merged_I uploaded)
CudaDevicePtr<double> merged_I, r_absdev, r_sumI, r_wabsdev, r_wsumI, r_sumv, r_sumv2; // R_meas per group (merged_I uploaded)
CudaDevicePtr<int32_t> r_n, r_nusable;
int merge_for_search = 0; // filter context for one MergeAndStats call
double merge_min_part = 0.0;
double merge_em_a = 1.0, merge_em_b = 0.0; // the error model of the last MergeAccum, for MergeRmeas
int merge_em_active = 0;
// combine: extra per-obs inputs + the one-time raw-hkl run layout
CudaDevicePtr<float> bkg, var_bkg, image_number, d_obs, px_obs, py_obs;
@@ -954,6 +967,7 @@ void RotationScaleMergeGPU::MergeAccum(double error_model_a, double error_model_
Upload(d.m_half, half, d.n_fulls);
Upload(d.cc_factor, frame_cc_factor, d.n_frames);
d.merge_em_a = error_model_a; d.merge_em_b = error_model_b; d.merge_em_active = error_model_active ? 1 : 0;
MergeParams p{};
p.n_groups = ng; p.min_partiality = d.merge_min_part;
p.for_search = d.merge_for_search;
@@ -998,13 +1012,16 @@ void RotationScaleMergeGPU::MergeAccumRange(int g0, int n, double *swI, double *
dl(nh0, d.a_nh0); dl(nh1, d.a_nh1); dl(rejected, d.a_rejected); dl(on_ice_out, d.a_on_ice);
}
void RotationScaleMergeGPU::MergeRmeas(const double *merged_I, double *absdev, double *sumI,
int32_t *n, int32_t *nusable) {
void RotationScaleMergeGPU::MergeRmeas(const double *merged_I, double *absdev, double *sumI, double *wabsdev,
double *wsumI, double *sumv, double *sumv2, int32_t *n,
int32_t *nusable) {
DeviceGuard guard(impl_->device, impl_->available);
auto &d = *impl_;
const int ng = d.n_groups;
Upload(d.merged_I, merged_I, ng);
d.r_absdev = CudaDevicePtr<double>(std::max(1, ng)); d.r_sumI = CudaDevicePtr<double>(std::max(1, ng));
d.r_wabsdev = CudaDevicePtr<double>(std::max(1, ng)); d.r_wsumI = CudaDevicePtr<double>(std::max(1, ng));
d.r_sumv = CudaDevicePtr<double>(std::max(1, ng)); d.r_sumv2 = CudaDevicePtr<double>(std::max(1, ng));
d.r_n = CudaDevicePtr<int32_t>(std::max(1, ng)); d.r_nusable = CudaDevicePtr<int32_t>(std::max(1, ng));
MergeParams p{};
@@ -1014,6 +1031,10 @@ void RotationScaleMergeGPU::MergeRmeas(const double *merged_I, double *absdev, d
p.gperm = d.f_gperm.get(); p.gstart = d.f_gstart.get(); p.gcount = d.f_gcount.get();
p.merged_I = d.merged_I.get();
p.r_absdev = d.r_absdev.get(); p.r_sumI = d.r_sumI.get(); p.r_n = d.r_n.get(); p.r_nusable = d.r_nusable.get();
p.r_wabsdev = d.r_wabsdev.get(); p.r_wsumI = d.r_wsumI.get();
p.r_sumv = d.r_sumv.get(); p.r_sumv2 = d.r_sumv2.get();
p.error_model_a = d.merge_em_a; p.error_model_b = d.merge_em_b; p.error_model_active = d.merge_em_active;
p.em_mean = d.m_em_mean.get(); p.var_bkg = d.f_var_bkg.get(); p.var_per_I = d.f_var_per_I.get();
p.rejected_obs = d.m_rejected.get();
const int grp_blocks = std::min(65535, (ng + BLK - 1) / BLK);
@@ -1022,6 +1043,10 @@ void RotationScaleMergeGPU::MergeRmeas(const double *merged_I, double *absdev, d
CudaCheck(cudaDeviceSynchronize(), "merge rmeas sync");
CudaCheck(cudaMemcpy(absdev, d.r_absdev.get(), size_t(ng) * sizeof(double), cudaMemcpyDeviceToHost), "dl absdev");
CudaCheck(cudaMemcpy(sumI, d.r_sumI.get(), size_t(ng) * sizeof(double), cudaMemcpyDeviceToHost), "dl sumI");
CudaCheck(cudaMemcpy(wabsdev, d.r_wabsdev.get(), size_t(ng) * sizeof(double), cudaMemcpyDeviceToHost), "dl wabsdev");
CudaCheck(cudaMemcpy(wsumI, d.r_wsumI.get(), size_t(ng) * sizeof(double), cudaMemcpyDeviceToHost), "dl wsumI");
CudaCheck(cudaMemcpy(sumv, d.r_sumv.get(), size_t(ng) * sizeof(double), cudaMemcpyDeviceToHost), "dl sumv");
CudaCheck(cudaMemcpy(sumv2, d.r_sumv2.get(), size_t(ng) * sizeof(double), cudaMemcpyDeviceToHost), "dl sumv2");
CudaCheck(cudaMemcpy(n, d.r_n.get(), size_t(ng) * sizeof(int32_t), cudaMemcpyDeviceToHost), "dl rn");
CudaCheck(cudaMemcpy(nusable, d.r_nusable.get(), size_t(ng) * sizeof(int32_t), cudaMemcpyDeviceToHost), "dl rnusable");
}
@@ -114,10 +114,13 @@ public:
int32_t *nh0, int32_t *nh1, double *d_out,
int32_t *rejected, uint8_t *on_ice_out);
// Per-group R_meas accumulators (sum|I_corr-merged_I|, sum_I, n, and the count this looser walk
// accepted - which the host uses only to skip empty groups, NOT as the per-shell
// total_observations); merged_I is uploaded. All arrays length n_groups.
void MergeRmeas(const double *merged_I, double *absdev, double *sumI, int32_t *n, int32_t *nusable);
// Per-group R_meas accumulators: sum|I_corr-merged_I| and sum I, the same with each observation
// weighted by its merge weight v = 1/sigma^2 under the error model of the last MergeAccum,
// sum v, sum v^2, n, and the count this looser walk accepted - which the host uses only to skip
// empty groups, NOT as the per-shell total_observations; merged_I is uploaded. All arrays length
// n_groups.
void MergeRmeas(const double *merged_I, double *absdev, double *sumI, double *wabsdev, double *wsumI,
double *sumv, double *sumv2, int32_t *n, int32_t *nusable);
// Post-smooth per-frame diagnostic CC: recompute the group means from the resident (smoothed) corr
// and the Pearson CC of each frame's I*corr vs its group mean, downloading only the per-frame cc /
+4
View File
@@ -803,6 +803,8 @@ ReportDocument BuildReportDocument(const std::string &output_prefix,
Add(s, KeyReal("I_OVER_SIGMA", o.mean_i_over_sigma, "{:.2f}"));
if (std::isfinite(o.r_meas))
Add(s, KeyReal("R_MEAS", o.r_meas, "{:.4f}"));
if (std::isfinite(o.r_meas_weighted))
Add(s, KeyReal("R_MEAS_WEIGHTED", o.r_meas_weighted, "{:.4f}"));
if (std::isfinite(o.cc_half))
Add(s, KeyReal("CC_HALF", o.cc_half, "{:.4f}"));
if (std::isfinite(o.abs_diff_over_sigma_anomalous))
@@ -937,6 +939,8 @@ ReportDocument BuildReportDocument(const std::string &output_prefix,
}
if (std::isfinite(ro.r_meas))
Add(s, KeyReal("REFRES_R_MEAS", ro.r_meas, "{:.4f}"));
if (std::isfinite(ro.r_meas_weighted))
Add(s, KeyReal("REFRES_R_MEAS_WEIGHTED", ro.r_meas_weighted, "{:.4f}"));
if (std::isfinite(ro.cc_half))
Add(s, KeyReal("REFRES_CC_HALF", ro.cc_half, "{:.4f}"));
if (std::isfinite(ro.abs_diff_over_sigma_anomalous))
+21
View File
@@ -388,6 +388,27 @@ TEST_CASE("CCHalfFrameFactors_DeadStretchDoesNotSetTheTypicalScale") {
CHECK(factor[100] == Catch::Approx(1.0).epsilon(1e-3));
}
// Equal weights give the ordinary R_meas terms, whatever their common value.
TEST_CASE("WeightedRmeas_EqualWeightsAreTheOrdinaryRmeas") {
// Three observations 9, 10, 11 of a reflection with <I> = 10: sum|dev| = 2, sum I = 30.
double num, den;
REQUIRE(WeightedRmeasTerms(0.5 * 2.0, 0.5 * 30.0, 0.5 * 3, 0.25 * 3, num, den));
CHECK(num == Catch::Approx(std::sqrt(3.0 / 2.0) * 2.0));
CHECK(den == Catch::Approx(30.0));
}
// An observation that carries almost no information counts for almost nothing: the terms tend to those
// of the reflection without it, and a reflection left with one effective observation has none.
TEST_CASE("WeightedRmeas_NegligibleWeightDropsOut") {
// 9 and 11 at weight 1, <I> = 10; a third at 40 with weight 1e-6.
const double v = 1e-6;
double num, den;
REQUIRE(WeightedRmeasTerms(2.0 + v * 30.0, 20.0 + v * 40.0, 2.0 + v, 2.0 + v * v, num, den));
CHECK(num == Catch::Approx(std::sqrt(2.0) * 2.0).epsilon(1e-4));
CHECK(den == Catch::Approx(20.0).epsilon(1e-4));
CHECK_FALSE(WeightedRmeasTerms(0.0, 10.0, 1.0, 1.0, num, den));
}
// A fall-off region a logistic cannot follow: CC1/2 drops through the target and comes straight back
// up. The fitted crossing is then an extrapolation far past the bins it was made over, and reading
// the cut off it writes the data deep into the noise; the crossing the bins themselves show is where
+4 -1
View File
@@ -52,7 +52,7 @@ def read_report(path):
# REFRES_* keys of the report read as numbers, stored lower-case under the same name
REFRES_KEYS = ("refres_shells_past_limit", "refres_unique_reflections", "refres_completeness",
"refres_multiplicity", "refres_i_over_sigma", "refres_r_meas", "refres_cc_half",
"refres_isa")
"refres_isa", "refres_r_meas_weighted")
def fnum(s):
@@ -186,6 +186,9 @@ def judge(entry, rep, run_note):
"d_min": None, "d_min_ref": ref.get("dmin"), "d_min_ref_rule": ref.get("dmin_rule"),
"d_min_xds": ref.get("dmin_xds"), "res_gain_pct": None,
"r_meas": fnum(rep.get("R_MEAS")), "cc_half": fnum(rep.get("CC_HALF")),
# R_MEAS is counted as XDS counts it; the weighted one weights each observation as the
# merge does (absent from reports before the weighting)
"r_meas_weighted": fnum(rep.get("R_MEAS_WEIGHTED")),
"isa": fnum(rep.get("ISA")), "completeness": fnum(rep.get("COMPLETENESS")),
"multiplicity": fnum(rep.get("MULTIPLICITY")), "i_over_sigma": fnum(rep.get("I_OVER_SIGMA")),
"indexing_rate": fnum(rep.get("INDEXING_RATE")), "images": fnum(rep.get("IMAGES_PROCESSED")),