diff --git a/frontend/src/components/FileWriterSettings.tsx b/frontend/src/components/FileWriterSettings.tsx
index ba1b5340..f3a7cd9e 100644
--- a/frontend/src/components/FileWriterSettings.tsx
+++ b/frontend/src/components/FileWriterSettings.tsx
@@ -75,8 +75,6 @@ function FileWriterSettings({s: serverS}: MyProps) {
-
-
diff --git a/image_analysis/LoadFCalcFromMtz.cpp b/image_analysis/LoadFCalcFromMtz.cpp
index 6eb815df..87be5062 100644
--- a/image_analysis/LoadFCalcFromMtz.cpp
+++ b/image_analysis/LoadFCalcFromMtz.cpp
@@ -39,18 +39,15 @@ const gemmi::Mtz::Column* SelectDefaultColumn(const gemmi::Mtz& mtz, bool& squar
// Fall back to a plain structure-factor amplitude (type F): a deposited/observed FP or FOBS, or a
// lone FC, squared to an intensity like F-model. Lets a reference MTZ that carries only amplitudes
// (no F-model or IMEAN/I - e.g. a deposition that has only FP) still seed CCref
- // without an explicit --reference-column.
+ // without an explicit --reference-column. Only these named observed/calculated labels are matched:
+ // a bare "first type-F column" catch-all would silently pick up map coefficients (FWT, DELFWT,
+ // FC_ALL) and seed CCref from the wrong quantity - require --reference-column in that case instead.
for (const char* label : {"FP", "FOBS", "F", "FC", "Fobs", "F-obs"}) {
if (const auto* col = mtz.column_with_label(label, nullptr, 'F')) {
square = true;
return col;
}
}
- for (const auto& c : mtz.columns)
- if (c.type == 'F') {
- square = true;
- return &c;
- }
return nullptr;
}
diff --git a/image_analysis/WriteReflections.cpp b/image_analysis/WriteReflections.cpp
index fffb4ab1..18d3d6cd 100644
--- a/image_analysis/WriteReflections.cpp
+++ b/image_analysis/WriteReflections.cpp
@@ -286,141 +286,121 @@ void WriteMtzReflections(const std::vector &reflections,
const int dataset_id = ds.id;
- // In anomalous mode the merge keeps the two Friedel mates as separate rows (I+ under the ASU
- // representative hkl, I- under -hkl). Emitting those verbatim gives a file with two rows per
- // reflection that downstream tools have to re-collapse. Instead pair the mates into one row per
- // reflection with the standard CCP4 anomalous layout (IMEAN + I(+)/I(-), and the same split for
- // the French-Wilson amplitude), which aimless / ctruncate / mtz2sca / ANODE read directly.
- if (experiment.GetScalingSettings().GetMergeFriedel()) {
- // Friedel-merged output: IMEAN is the primary intensity. I(+)/I(-) are written by default too
- // when the merge kept a Bijvoet split (rotation always does; the split is scaled non-anomalously),
- // so a weak anomalous signal is preserved without reprocessing. A reflection with only one mate,
- // or a centric, gets a missing value (NaN) for the absent hand. When no reflection has an
- // anomalous split (e.g. the stills path, which does not compute one) the columns are omitted.
- const bool has_anom = std::any_of(reflections.begin(), reflections.end(),
- [](const MergedReflection& r){ return std::isfinite(r.I_plus) || std::isfinite(r.I_minus); });
- mtz.add_column("H", 'H', dataset_id, -1, false);
- mtz.add_column("K", 'H', dataset_id, -1, false);
- 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);
- if (has_anom) {
- mtz.add_column("I(+)", 'K', dataset_id, -1, false);
- mtz.add_column("SIGI(+)", 'M', dataset_id, -1, false);
- mtz.add_column("I(-)", 'K', dataset_id, -1, false);
- mtz.add_column("SIGI(-)", 'M', 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);
- if (has_anom) {
- mtz.add_column("F(+)", 'G', dataset_id, -1, false);
- mtz.add_column("SIGF(+)", 'L', dataset_id, -1, false);
- mtz.add_column("F(-)", 'G', dataset_id, -1, false);
- mtz.add_column("SIGF(-)", 'L', dataset_id, -1, false);
- }
- mtz.add_column("FreeR_flag", 'I', dataset_id, -1, false);
-
- mtz.nreflections = static_cast(reflections.size());
- mtz.data.reserve(reflections.size() * (has_anom ? 16 : 8));
- for (const auto& r : reflections) {
- mtz.data.push_back(static_cast(r.h));
- mtz.data.push_back(static_cast(r.k));
- mtz.data.push_back(static_cast(r.l));
- mtz.data.push_back(r.I);
- mtz.data.push_back(r.sigma);
- if (has_anom) {
- mtz.data.push_back(r.I_plus);
- mtz.data.push_back(r.sigma_plus);
- mtz.data.push_back(r.I_minus);
- mtz.data.push_back(r.sigma_minus);
- }
- mtz.data.push_back(r.F);
- mtz.data.push_back(r.sigmaF);
- if (has_anom) {
- mtz.data.push_back(r.F_plus);
- mtz.data.push_back(r.sigmaF_plus);
- mtz.data.push_back(r.F_minus);
- mtz.data.push_back(r.sigmaF_minus);
- }
- mtz.data.push_back(r.rfree_flag ? 1.0f : 0.0f);
- }
- mtz.write_to_file(filename);
- return;
- }
-
- // Anomalous: group the two mates by their (shared) Friedel-merged ASU representative. A single
- // generator gives both the group key (its hkl, identical for +hkl and -hkl) and which mate this
- // row is (.plus).
- const HKLKeyGenerator key_gen(false, experiment.GetSpaceGroupNumber().value_or(1));
-
- struct AnomRow {
+ // The merge keeps the two Friedel mates as separate rows (I+ under the ASU representative hkl, I-
+ // under -hkl). Both output modes collapse them into one row per reflection with the standard CCP4
+ // anomalous layout (IMEAN + I(+)/I(-), and the same split for the French-Wilson amplitude), which
+ // aimless / ctruncate / mtz2sca / ANODE read directly. The modes differ only in how each row is
+ // formed, so they build one common row list and share the column/row emission below.
+ struct OutRow {
int h = 0, k = 0, l = 0;
- float Ip = NAN, sIp = NAN, Im = NAN, sIm = NAN;
- float Fp = NAN, sFp = NAN, Fm = NAN, sFm = NAN;
+ float Imean = NAN, sImean = NAN, Ip = NAN, sIp = NAN, Im = NAN, sIm = NAN;
+ float Fmean = NAN, sFmean = NAN, Fp = NAN, sFp = NAN, Fm = NAN, sFm = NAN;
int rfree = 0;
};
- std::map, AnomRow> rows;
- for (const auto& r : reflections) {
- const HKLKey key = key_gen(r);
- AnomRow& row = rows[{key.h, key.k, key.l}];
- row.h = key.h; row.k = key.k; row.l = key.l;
- row.rfree = r.rfree_flag ? 1 : 0;
- if (key.plus) { row.Ip = r.I; row.sIp = r.sigma; row.Fp = r.F; row.sFp = r.sigmaF; }
- else { row.Im = r.I; row.sIm = r.sigma; row.Fm = r.F; row.sFm = r.sigmaF; }
- }
+ std::vector out_rows;
+ bool has_anom = true;
- // Friedel-mean of the two mates by inverse variance (the single mate, if only one was measured).
- const auto combine = [](float a, float sa, float b, float sb, float& val, float& sig) {
- const bool ok_a = std::isfinite(a) && sa > 0.0f;
- const bool ok_b = std::isfinite(b) && sb > 0.0f;
- if (ok_a && ok_b) {
- const double wa = 1.0 / (static_cast(sa) * sa);
- const double wb = 1.0 / (static_cast(sb) * sb);
- val = static_cast((wa * a + wb * b) / (wa + wb));
- sig = static_cast(1.0 / std::sqrt(wa + wb));
- } else if (ok_a) { val = a; sig = sa; }
- else if (ok_b) { val = b; sig = sb; }
- else { val = NAN; sig = NAN; }
- };
+ if (experiment.GetScalingSettings().GetMergeFriedel()) {
+ // Friedel-merged: IMEAN is the already-merged intensity (r.I). I(+)/I(-) are carried verbatim
+ // from the Bijvoet split the merge kept (rotation always does; scaled non-anomalously), so a weak
+ // anomalous signal is preserved without reprocessing. A reflection with only one mate, or a
+ // centric, gets a missing value (NaN) for the absent hand. When NO reflection has an anomalous
+ // split (e.g. the stills path, which does not compute one) the anomalous columns are omitted.
+ has_anom = std::any_of(reflections.begin(), reflections.end(),
+ [](const MergedReflection& r){ return std::isfinite(r.I_plus) || std::isfinite(r.I_minus); });
+ out_rows.reserve(reflections.size());
+ for (const auto& r : reflections)
+ out_rows.push_back({r.h, r.k, r.l, r.I, r.sigma, r.I_plus, r.sigma_plus, r.I_minus,
+ r.sigma_minus, r.F, r.sigmaF, r.F_plus, r.sigmaF_plus, r.F_minus,
+ r.sigmaF_minus, r.rfree_flag ? 1 : 0});
+ } else {
+ // Anomalous: group the two mates by their (shared) Friedel-merged ASU representative, then form
+ // IMEAN / F as their inverse-variance Friedel mean. A single generator gives both the group key
+ // (its hkl, identical for +hkl and -hkl) and which mate this row is (.plus).
+ const HKLKeyGenerator key_gen(false, experiment.GetSpaceGroupNumber().value_or(1));
+ struct AnomRow {
+ int h = 0, k = 0, l = 0;
+ float Ip = NAN, sIp = NAN, Im = NAN, sIm = NAN;
+ float Fp = NAN, sFp = NAN, Fm = NAN, sFm = NAN;
+ int rfree = 0;
+ };
+ std::map, AnomRow> rows;
+ for (const auto& r : reflections) {
+ const HKLKey key = key_gen(r);
+ AnomRow& row = rows[{key.h, key.k, key.l}];
+ row.h = key.h; row.k = key.k; row.l = key.l;
+ row.rfree = r.rfree_flag ? 1 : 0;
+ if (key.plus) { row.Ip = r.I; row.sIp = r.sigma; row.Fp = r.F; row.sFp = r.sigmaF; }
+ else { row.Im = r.I; row.sIm = r.sigma; row.Fm = r.F; row.sFm = r.sigmaF; }
+ }
+
+ // Friedel-mean of the two mates by inverse variance (the single mate, if only one was measured).
+ const auto combine = [](float a, float sa, float b, float sb, float& val, float& sig) {
+ const bool ok_a = std::isfinite(a) && sa > 0.0f;
+ const bool ok_b = std::isfinite(b) && sb > 0.0f;
+ if (ok_a && ok_b) {
+ const double wa = 1.0 / (static_cast(sa) * sa);
+ const double wb = 1.0 / (static_cast(sb) * sb);
+ val = static_cast((wa * a + wb * b) / (wa + wb));
+ sig = static_cast(1.0 / std::sqrt(wa + wb));
+ } else if (ok_a) { val = a; sig = sa; }
+ else if (ok_b) { val = b; sig = sb; }
+ else { val = NAN; sig = NAN; }
+ };
+
+ out_rows.reserve(rows.size());
+ for (const auto& [hkl, row] : rows) {
+ float i_mean, sig_i_mean, f_mean, sig_f_mean;
+ combine(row.Ip, row.sIp, row.Im, row.sIm, i_mean, sig_i_mean);
+ combine(row.Fp, row.sFp, row.Fm, row.sFm, f_mean, sig_f_mean);
+ out_rows.push_back({row.h, row.k, row.l, i_mean, sig_i_mean, row.Ip, row.sIp, row.Im,
+ row.sIm, f_mean, sig_f_mean, row.Fp, row.sFp, row.Fm, row.sFm, row.rfree});
+ }
+ }
mtz.add_column("H", 'H', dataset_id, -1, false);
mtz.add_column("K", 'H', dataset_id, -1, false);
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("I(+)", 'K', dataset_id, -1, false);
- mtz.add_column("SIGI(+)", 'M', dataset_id, -1, false);
- mtz.add_column("I(-)", 'K', dataset_id, -1, false);
- mtz.add_column("SIGI(-)", 'M', dataset_id, -1, false);
- mtz.add_column("F", 'F', dataset_id, -1, false); // French-Wilson amplitude (mean)
+ if (has_anom) {
+ mtz.add_column("I(+)", 'K', dataset_id, -1, false);
+ mtz.add_column("SIGI(+)", 'M', dataset_id, -1, false);
+ mtz.add_column("I(-)", 'K', dataset_id, -1, false);
+ mtz.add_column("SIGI(-)", 'M', 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("F(+)", 'G', dataset_id, -1, false);
- mtz.add_column("SIGF(+)", 'L', dataset_id, -1, false);
- mtz.add_column("F(-)", 'G', dataset_id, -1, false);
- mtz.add_column("SIGF(-)", 'L', dataset_id, -1, false);
+ if (has_anom) {
+ mtz.add_column("F(+)", 'G', dataset_id, -1, false);
+ mtz.add_column("SIGF(+)", 'L', dataset_id, -1, false);
+ mtz.add_column("F(-)", 'G', dataset_id, -1, false);
+ mtz.add_column("SIGF(-)", 'L', dataset_id, -1, false);
+ }
mtz.add_column("FreeR_flag", 'I', dataset_id, -1, false);
- mtz.nreflections = static_cast(rows.size());
- mtz.data.reserve(rows.size() * 16);
- for (const auto& [hkl, row] : rows) {
- float i_mean, sig_i_mean, f_mean, sig_f_mean;
- combine(row.Ip, row.sIp, row.Im, row.sIm, i_mean, sig_i_mean);
- combine(row.Fp, row.sFp, row.Fm, row.sFm, f_mean, sig_f_mean);
+ mtz.nreflections = static_cast(out_rows.size());
+ mtz.data.reserve(out_rows.size() * (has_anom ? 16 : 8));
+ for (const auto& row : out_rows) {
mtz.data.push_back(static_cast(row.h));
mtz.data.push_back(static_cast(row.k));
mtz.data.push_back(static_cast(row.l));
- mtz.data.push_back(i_mean);
- mtz.data.push_back(sig_i_mean);
- mtz.data.push_back(row.Ip);
- mtz.data.push_back(row.sIp);
- mtz.data.push_back(row.Im);
- mtz.data.push_back(row.sIm);
- mtz.data.push_back(f_mean);
- mtz.data.push_back(sig_f_mean);
- mtz.data.push_back(row.Fp);
- mtz.data.push_back(row.sFp);
- mtz.data.push_back(row.Fm);
- mtz.data.push_back(row.sFm);
+ mtz.data.push_back(row.Imean);
+ mtz.data.push_back(row.sImean);
+ if (has_anom) {
+ mtz.data.push_back(row.Ip);
+ mtz.data.push_back(row.sIp);
+ mtz.data.push_back(row.Im);
+ mtz.data.push_back(row.sIm);
+ }
+ mtz.data.push_back(row.Fmean);
+ mtz.data.push_back(row.sFmean);
+ if (has_anom) {
+ mtz.data.push_back(row.Fp);
+ mtz.data.push_back(row.sFp);
+ mtz.data.push_back(row.Fm);
+ mtz.data.push_back(row.sFm);
+ }
mtz.data.push_back(static_cast(row.rfree));
}
mtz.write_to_file(filename);
diff --git a/image_analysis/scale_merge/ReindexAmbiguity.cpp b/image_analysis/scale_merge/ReindexAmbiguity.cpp
index 59e53558..8e13266d 100644
--- a/image_analysis/scale_merge/ReindexAmbiguity.cpp
+++ b/image_analysis/scale_merge/ReindexAmbiguity.cpp
@@ -13,12 +13,6 @@
namespace {
constexpr size_t MIN_REFLECTIONS = 20;
- double SafeInv(double x, double fallback) {
- if (!std::isfinite(x) || x == 0.0)
- return fallback;
- return 1.0 / x;
- }
-
struct BestReindex {
gemmi::Op op = gemmi::Op::identity();
bool is_identity = true;
@@ -150,7 +144,7 @@ double ReindexAmbiguityResolver::ReferenceCC(const std::vector &refl
const auto it = reference_data.find(hkl_key_generator(h[0], h[1], h[2]));
if (it == reference_data.end())
continue;
- const double x = static_cast(r.I) * SafeInv(r.rlp, 1.0) / r.partiality;
+ const double x = static_cast(r.I) * r.rlp / r.partiality;
const double y = it->second;
if (!std::isfinite(x) || !std::isfinite(y))
continue;
diff --git a/rugnux/RugnuxCommandLine.cpp b/rugnux/RugnuxCommandLine.cpp
index 10609e19..5e748279 100644
--- a/rugnux/RugnuxCommandLine.cpp
+++ b/rugnux/RugnuxCommandLine.cpp
@@ -108,6 +108,10 @@ std::string RugnuxCommandLine(const ProcessConfig &config,
add("--integration-radius", radii.str());
if (bragg.GetStillPartiality())
args.emplace_back("--still-partiality");
+ // Background trim defaults to 0.10 in the CLI, so emit it whenever the GUI value differs (a
+ // custom fraction, or 0 when the box is unchecked) to reproduce the GUI's choice faithfully.
+ if (bragg.GetBackgroundTrimFraction() != 0.10f)
+ add("--background-trim", num(bragg.GetBackgroundTrimFraction()));
if (config.rotation_indexing) {
if (config.two_pass_rotation)
@@ -147,6 +151,8 @@ std::string RugnuxCommandLine(const ProcessConfig &config,
args.emplace_back("-B");
if (sc.GetPartialityUncertaintyCoeff() > 0.0)
add("--partiality-uncertainty", num(sc.GetPartialityUncertaintyCoeff()));
+ if (sc.GetStillsModulation())
+ args.emplace_back("--stills-modulation");
// When merging, the CLI skips the large _process.h5 unless asked; emit the flag when it is
// wanted so a copied command matches the GUI's "Save _process.h5" choice. (write_merged has
// no CLI equivalent - the CLI always writes the .mtz/.cif when merging.)
diff --git a/rugnux/rugnux_cli.cpp b/rugnux/rugnux_cli.cpp
index 76dc654a..42711113 100644
--- a/rugnux/rugnux_cli.cpp
+++ b/rugnux/rugnux_cli.cpp
@@ -610,12 +610,11 @@ int main(int argc, char **argv) {
refine_geometry_disabled = true; // opt out of the stills default-on
break;
}
- int n = optarg ? atoi(optarg) : 200;
- if (n <= 0) {
- logger.Error("Invalid --refine-geometry value (positive frame count or 'off'): {}", optarg);
- exit(EXIT_FAILURE);
- }
- refine_geometry = n;
+ // Positive frame count fed to the bundle adjust; upper-bounded so refine_frames * 50 in
+ // RefineStillsGeometry cannot overflow int (1e6 is already far past any real dataset).
+ refine_geometry = optarg
+ ? parse_number_arg(optarg, "--refine-geometry", logger, 1, 1000000)
+ : 200;
break;
}
case OPT_FORCE_ROTATION_LATTICE: {