SearchSpaceGroup: add space group search
Build Packages / build:rpm (ubuntu2204) (push) Has been cancelled
Build Packages / build:rpm (ubuntu2404) (push) Has been cancelled
Build Packages / Generate python client (push) Has been cancelled
Build Packages / Build documentation (push) Has been cancelled
Build Packages / Unit tests (push) Has been cancelled
Build Packages / Create release (push) Has been cancelled
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Has been cancelled
Build Packages / build:rpm (rocky8) (push) Has been cancelled
Build Packages / build:rpm (rocky8_nocuda) (push) Has been cancelled
Build Packages / build:rpm (rocky9) (push) Has been cancelled
Build Packages / build:rpm (rocky9_sls9) (push) Has been cancelled
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Has been cancelled
Build Packages / build:rpm (rocky9_nocuda) (push) Has been cancelled
Build Packages / build:rpm (rocky8_sls9) (push) Has been cancelled

This commit is contained in:
2026-03-06 20:53:12 +01:00
parent 5c86e9e524
commit d043c98c3c
6 changed files with 741 additions and 43 deletions
+3 -1
View File
@@ -1,2 +1,4 @@
ADD_LIBRARY(JFJochScaleMerge ScaleAndMerge.cpp ScaleAndMerge.h FrenchWilson.cpp FrenchWilson.h)
ADD_LIBRARY(JFJochScaleMerge ScaleAndMerge.cpp ScaleAndMerge.h FrenchWilson.cpp FrenchWilson.h
SearchSpaceGroup.cpp
SearchSpaceGroup.h)
TARGET_LINK_LIBRARIES(JFJochScaleMerge Ceres::ceres Eigen3::Eigen JFJochCommon)
@@ -0,0 +1,426 @@
#include "SearchSpaceGroup.h"
#include <algorithm>
#include <cmath>
#include <cctype>
#include <iomanip>
#include <limits>
#include <sstream>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <vector>
namespace {
struct MergedHKLKey {
int h = 0;
int k = 0;
int l = 0;
bool plus = true;
bool operator==(const MergedHKLKey& o) const noexcept {
return h == o.h && k == o.k && l == o.l && plus == o.plus;
}
};
struct MergedHKLKeyHash {
size_t operator()(const MergedHKLKey& key) const noexcept {
auto mix = [](uint64_t x) {
x ^= x >> 33;
x *= 0xff51afd7ed558ccdULL;
x ^= x >> 33;
x *= 0xc4ceb9fe1a85ec53ULL;
x ^= x >> 33;
return x;
};
return static_cast<size_t>(
mix(static_cast<uint64_t>(key.h)) ^
(mix(static_cast<uint64_t>(key.k)) << 1) ^
(mix(static_cast<uint64_t>(key.l)) << 2) ^
(mix(static_cast<uint64_t>(key.plus ? 1 : 0)) << 3));
}
};
bool IsIdentityOp(const gemmi::Op& op) {
return op.rot == gemmi::Op::identity().rot;
}
bool IsParityChanging(const gemmi::Op& op) {
return op.det_rot() < 0;
}
MergedHKLKey CanonicalizeMergedHKL(int h, int k, int l, bool merge_friedel) {
MergedHKLKey key{h, k, l, true};
if (merge_friedel)
return key;
const std::tuple<int, int, int> pos{h, k, l};
const std::tuple<int, int, int> neg{-h, -k, -l};
if (neg < pos) {
key.h = -h;
key.k = -k;
key.l = -l;
key.plus = false;
}
return key;
}
bool ReflectionPassesFilters(const MergedReflection& r, const SearchSpaceGroupOptions& opt) {
if (!std::isfinite(r.I))
return false;
if (!std::isfinite(r.sigma) || r.sigma <= 0.0)
return false;
if (!std::isfinite(r.d) || r.d <= 0.0)
return false;
if (opt.d_min_limit_A > 0.0 && r.d < opt.d_min_limit_A)
return false;
if (opt.min_i_over_sigma > 0.0 && r.I / r.sigma < opt.min_i_over_sigma)
return false;
return true;
}
std::unordered_map<MergedHKLKey, const MergedReflection*, MergedHKLKeyHash>
BuildMergedLookup(const std::vector<MergedReflection>& merged, const SearchSpaceGroupOptions& opt) {
std::unordered_map<MergedHKLKey, const MergedReflection*, MergedHKLKeyHash> out;
out.reserve(merged.size());
for (const auto& r : merged) {
if (!ReflectionPassesFilters(r, opt))
continue;
const auto key = CanonicalizeMergedHKL(r.h, r.k, r.l, opt.merge_friedel);
out.emplace(key, &r);
}
return out;
}
double PearsonCC(const std::vector<double>& x, const std::vector<double>& y) {
if (x.size() != y.size() || x.size() < 2)
return std::numeric_limits<double>::quiet_NaN();
double sx = 0.0, sy = 0.0;
double sxx = 0.0, syy = 0.0, sxy = 0.0;
for (size_t i = 0; i < x.size(); ++i) {
sx += x[i];
sy += y[i];
sxx += x[i] * x[i];
syy += y[i] * y[i];
sxy += x[i] * y[i];
}
const double n = static_cast<double>(x.size());
const double cov = sxy - sx * sy / n;
const double vx = sxx - sx * sx / n;
const double vy = syy - sy * sy / n;
if (vx <= 0.0 || vy <= 0.0)
return std::numeric_limits<double>::quiet_NaN();
return cov / std::sqrt(vx * vy);
}
std::optional<SpaceGroupOperatorScore> ScoreOperator(
const gemmi::Op& op,
const std::vector<MergedReflection>& merged,
const std::unordered_map<MergedHKLKey, const MergedReflection*, MergedHKLKeyHash>& lookup,
const SearchSpaceGroupOptions& opt) {
std::vector<double> x;
std::vector<double> y;
x.reserve(merged.size() / 2);
y.reserve(merged.size() / 2);
std::unordered_set<MergedHKLKey, MergedHKLKeyHash> used;
used.reserve(merged.size() / 2);
for (const auto& r : merged) {
if (!ReflectionPassesFilters(r, opt))
continue;
const auto key1 = CanonicalizeMergedHKL(r.h, r.k, r.l, opt.merge_friedel);
if (used.find(key1) != used.end())
continue;
const gemmi::Op::Miller hkl{{r.h, r.k, r.l}};
const auto hkl2 = op.apply_to_hkl(hkl);
const auto key2 = CanonicalizeMergedHKL(hkl2[0], hkl2[1], hkl2[2], opt.merge_friedel);
if (key1.h == key2.h && key1.k == key2.k && key1.l == key2.l && key1.plus == key2.plus)
continue;
auto it = lookup.find(key2);
if (it == lookup.end())
continue;
const auto* mate = it->second;
if (mate == nullptr)
continue;
x.push_back(r.I);
y.push_back(mate->I);
used.insert(key1);
used.insert(key2);
}
if (x.empty())
return std::nullopt;
SpaceGroupOperatorScore out;
out.op_triplet_hkl = op.as_hkl().triplet('h');
out.compared = static_cast<int>(x.size());
out.cc = PearsonCC(x, y);
out.accepted = (out.compared >= opt.min_pairs_per_operator &&
std::isfinite(out.cc) &&
out.cc >= opt.min_operator_cc);
return out;
}
SpaceGroupAbsenceScore ScoreSystematicAbsences(
const gemmi::GroupOps& gops,
const std::vector<MergedReflection>& merged,
const SearchSpaceGroupOptions& opt) {
SpaceGroupAbsenceScore out;
if (!opt.test_systematic_absences)
return out;
double sum_i_over_sigma = 0.0;
for (const auto& r : merged) {
if (!ReflectionPassesFilters(r, opt))
continue;
const gemmi::Op::Miller hkl{{r.h, r.k, r.l}};
if (!gops.is_systematically_absent(hkl))
continue;
out.violating_reflections += 1;
sum_i_over_sigma += std::max(0.0, r.I / r.sigma);
}
if (out.violating_reflections > 0)
out.mean_i_over_sigma = sum_i_over_sigma / out.violating_reflections;
else
out.mean_i_over_sigma = 0.0;
out.accepted = (out.violating_reflections <= opt.max_absent_violations) &&
(out.mean_i_over_sigma <= opt.max_mean_absent_i_over_sigma);
return out;
}
bool IsCenteringCompatible(char requested, char candidate) {
if (requested == '\0')
return true;
return std::toupper(static_cast<unsigned char>(requested)) ==
std::toupper(static_cast<unsigned char>(candidate));
}
bool IsCandidateSpaceGroup(const gemmi::SpaceGroup& sg,
const std::optional<gemmi::CrystalSystem>& crystal_system,
char centering) {
if (!sg.is_reference_setting())
return false;
if (!sg.is_sohncke())
return false;
if (crystal_system.has_value() && sg.crystal_system() != crystal_system.value())
return false;
if (!IsCenteringCompatible(centering, sg.centring_type()))
return false;
return true;
}
std::vector<gemmi::SpaceGroup> EnumerateCandidateSpaceGroups(
const std::optional<gemmi::CrystalSystem>& crystal_system,
char centering) {
std::vector<gemmi::SpaceGroup> out;
for (const auto& sg : gemmi::spacegroup_tables::main) {
if (!IsCandidateSpaceGroup(sg, crystal_system, centering))
continue;
out.push_back(sg);
}
std::sort(out.begin(), out.end(),
[](const gemmi::SpaceGroup& a, const gemmi::SpaceGroup& b) {
const int order_a = a.operations().derive_symmorphic().order();
const int order_b = b.operations().derive_symmorphic().order();
if (order_a != order_b)
return order_a < order_b;
return a.number < b.number;
});
return out;
}
}
SearchSpaceGroupResult SearchSpaceGroup(
const std::vector<MergedReflection>& merged,
const SearchSpaceGroupOptions& opt) {
SearchSpaceGroupResult result;
if (merged.empty())
return result;
const auto lookup = BuildMergedLookup(merged, opt);
const auto candidates = EnumerateCandidateSpaceGroups(opt.crystal_system, opt.centering);
for (const auto& sg : candidates) {
SpaceGroupCandidateScore score{.space_group = sg};
const gemmi::GroupOps gops_full = sg.operations();
const gemmi::GroupOps gops_rot = gops_full.derive_symmorphic();
double cc_sum = 0.0;
int cc_count = 0;
int compared_total = 0;
double min_cc = std::numeric_limits<double>::infinity();
for (const auto& op : gops_rot.sym_ops) {
if (IsIdentityOp(op))
continue;
if (IsParityChanging(op))
continue;
auto op_score = ScoreOperator(op, merged, lookup, opt);
if (!op_score.has_value())
continue;
compared_total += op_score->compared;
score.operator_scores.push_back(*op_score);
if (op_score->compared >= opt.min_pairs_per_operator && std::isfinite(op_score->cc)) {
cc_sum += op_score->cc;
min_cc = std::min(min_cc, op_score->cc);
cc_count += 1;
if (op_score->accepted)
score.accepted_operators += 1;
}
}
score.absence_score = ScoreSystematicAbsences(gops_full, merged, opt);
score.tested_operators = static_cast<int>(score.operator_scores.size());
score.compared_total = compared_total;
score.mean_cc = (cc_count > 0) ? (cc_sum / cc_count) : 0.0;
score.min_cc = std::isfinite(min_cc) ? min_cc : 0.0;
const bool trivial_group = (gops_rot.order() <= 1);
const bool rotationally_accepted =
trivial_group ||
((score.tested_operators > 0) &&
(score.accepted_operators == score.tested_operators) &&
(score.compared_total >= opt.min_total_compared));
score.accepted = rotationally_accepted && score.absence_score.accepted;
result.candidates.push_back(std::move(score));
}
std::sort(result.candidates.begin(), result.candidates.end(),
[](const SpaceGroupCandidateScore& a, const SpaceGroupCandidateScore& b) {
if (a.accepted != b.accepted)
return a.accepted > b.accepted;
if (a.absence_score.mean_i_over_sigma != b.absence_score.mean_i_over_sigma)
return a.absence_score.mean_i_over_sigma < b.absence_score.mean_i_over_sigma;
if (a.absence_score.violating_reflections != b.absence_score.violating_reflections)
return a.absence_score.violating_reflections < b.absence_score.violating_reflections;
const int order_a = a.space_group.operations().derive_symmorphic().order();
const int order_b = b.space_group.operations().derive_symmorphic().order();
if (order_a != order_b)
return order_a > order_b;
if (a.accepted_operators != b.accepted_operators)
return a.accepted_operators > b.accepted_operators;
if (a.min_cc != b.min_cc)
return a.min_cc > b.min_cc;
if (a.mean_cc != b.mean_cc)
return a.mean_cc > b.mean_cc;
return a.space_group.number < b.space_group.number;
});
for (const auto& cand : result.candidates) {
if (cand.accepted) {
result.best_space_group = cand.space_group;
break;
}
}
return result;
}
std::string SearchSpaceGroupResultToText(const SearchSpaceGroupResult& result,
size_t max_candidates_to_print) {
std::ostringstream os;
os << "Space-group candidates\n";
os << " "
<< std::setw(10) << "SG"
<< " "
<< std::setw(4) << "Acc"
<< " "
<< std::setw(8) << "<CC>"
<< " "
<< std::setw(8) << "minCC"
<< " "
<< std::setw(9) << "compared"
<< " "
<< std::setw(7) << "ops"
<< " "
<< std::setw(8) << "absent"
<< " "
<< std::setw(10) << "<I/sig>abs"
<< "\n";
os << " "
<< std::setw(10) << "----------"
<< " "
<< std::setw(4) << "----"
<< " "
<< std::setw(8) << "--------"
<< " "
<< std::setw(8) << "--------"
<< " "
<< std::setw(9) << "---------"
<< " "
<< std::setw(7) << "-------"
<< " "
<< std::setw(8) << "--------"
<< " "
<< std::setw(10) << "----------"
<< "\n";
const size_t n = std::min(max_candidates_to_print, result.candidates.size());
for (size_t i = 0; i < n; ++i) {
const auto& c = result.candidates[i];
os << " "
<< std::setw(10) << c.space_group.short_name()
<< " "
<< std::setw(4) << (c.accepted ? "yes" : "no")
<< " "
<< std::setw(8) << std::fixed << std::setprecision(3) << c.mean_cc
<< " "
<< std::setw(8) << std::fixed << std::setprecision(3) << c.min_cc
<< " "
<< std::setw(9) << c.compared_total
<< " "
<< std::setw(3) << c.accepted_operators << "/" << std::setw(3) << c.tested_operators
<< " "
<< std::setw(8) << c.absence_score.violating_reflections
<< " "
<< std::setw(10) << std::fixed << std::setprecision(2) << c.absence_score.mean_i_over_sigma
<< "\n";
}
if (result.best_space_group.has_value())
os << "Best space group: " << result.best_space_group->short_name() << "\n";
else
os << "Best space group: none accepted\n";
return os.str();
}
@@ -0,0 +1,80 @@
#pragma once
#include <optional>
#include <string>
#include <vector>
#include "ScaleAndMerge.h"
#include "gemmi/symmetry.hpp"
struct SpaceGroupOperatorScore {
std::string op_triplet_hkl;
double cc = 0.0;
int compared = 0;
bool accepted = false;
};
struct SpaceGroupAbsenceScore {
int violating_reflections = 0;
double mean_i_over_sigma = 0.0;
bool accepted = true;
};
struct SpaceGroupCandidateScore {
gemmi::SpaceGroup space_group;
double mean_cc = 0.0;
double min_cc = 0.0;
int compared_total = 0;
int accepted_operators = 0;
int tested_operators = 0;
bool accepted = false;
SpaceGroupAbsenceScore absence_score;
std::vector<SpaceGroupOperatorScore> operator_scores;
};
struct SearchSpaceGroupOptions {
// If not set, search all crystal systems.
std::optional<gemmi::CrystalSystem> crystal_system;
// '\0' means: search all centerings compatible with the candidate system.
char centering = '\0';
// If true, Friedel mates are treated as equivalent when matching HKLs.
bool merge_friedel = true;
// Ignore reflections beyond this resolution (higher-resolution means smaller d).
// Set to 0 to disable the cutoff.
double d_min_limit_A = 0.0;
// Optional I/sigma filter on merged reflections.
double min_i_over_sigma = 0.0;
// Acceptance thresholds for each tested rotational operator.
double min_operator_cc = 0.80;
int min_pairs_per_operator = 20;
// Minimum total number of compared reflection pairs for a candidate SG.
int min_total_compared = 100;
// Test systematic absences from centering / screws / glides.
bool test_systematic_absences = true;
// Candidate is rejected if forbidden reflections are too strong on average.
double max_mean_absent_i_over_sigma = 1.0;
// Hard cap on number of observed forbidden reflections.
int max_absent_violations = 0;
};
struct SearchSpaceGroupResult {
std::optional<gemmi::SpaceGroup> best_space_group;
std::vector<SpaceGroupCandidateScore> candidates;
};
SearchSpaceGroupResult SearchSpaceGroup(
const std::vector<MergedReflection>& merged,
const SearchSpaceGroupOptions& opt = {});
std::string SearchSpaceGroupResultToText(
const SearchSpaceGroupResult& result,
size_t max_candidates_to_print = 20);
+1
View File
@@ -66,6 +66,7 @@ ADD_EXECUTABLE(jfjoch_test
HKLKeyTest.cpp
TCPImagePusherTest.cpp
BraggIntegrate2DTest.cpp
SearchSpaceGroupTest.cpp
)
target_link_libraries(jfjoch_test Catch2WithMain JFJochBroker JFJochReceiver JFJochReader JFJochWriter JFJochImageAnalysis JFJochCommon JFJochHLSSimulation JFJochPreview)
+143
View File
@@ -0,0 +1,143 @@
#include <catch2/catch_all.hpp>
#include "../image_analysis/scale_merge/SearchSpaceGroup.h"
#include "gemmi/symmetry.hpp"
#include <cmath>
#include <cstdint>
#include <string>
#include <tuple>
#include <unordered_set>
#include <vector>
namespace {
struct HKL {
int h = 0;
int k = 0;
int l = 0;
bool operator==(const HKL& o) const noexcept {
return h == o.h && k == o.k && l == o.l;
}
};
struct HKLHash {
size_t operator()(const HKL& x) const noexcept {
auto mix = [](uint64_t v) {
v ^= v >> 33;
v *= 0xff51afd7ed558ccdULL;
v ^= v >> 33;
v *= 0xc4ceb9fe1a85ec53ULL;
v ^= v >> 33;
return v;
};
return static_cast<size_t>(
mix(static_cast<uint64_t>(x.h)) ^
(mix(static_cast<uint64_t>(x.k)) << 1) ^
(mix(static_cast<uint64_t>(x.l)) << 2));
}
};
double CalcSyntheticD(int h, int k, int l) {
const double q2 = static_cast<double>(h * h + k * k + l * l);
return 40.0 / std::sqrt(q2 + 1.0);
}
double SyntheticIntensityFromAsu(const gemmi::Op::Miller& asu) {
uint64_t x = static_cast<uint64_t>((asu[0] + 31) * 73856093u) ^
static_cast<uint64_t>((asu[1] + 37) * 19349663u) ^
static_cast<uint64_t>((asu[2] + 41) * 83492791u);
x ^= x >> 13;
x *= 0x9e3779b97f4a7c15ULL;
x ^= x >> 17;
return 100.0 + static_cast<double>(x % 500);
}
std::vector<MergedReflection> GenerateMergedReflectionsForSpaceGroup(
const gemmi::SpaceGroup& sg,
int hmax = 8) {
std::vector<MergedReflection> merged;
std::unordered_set<HKL, HKLHash> added;
const gemmi::GroupOps gops = sg.operations();
const gemmi::ReciprocalAsu rasu(&sg);
for (int h = -hmax; h <= hmax; ++h) {
for (int k = -hmax; k <= hmax; ++k) {
for (int l = -hmax; l <= hmax; ++l) {
if (h == 0 && k == 0 && l == 0)
continue;
gemmi::Op::Miller hkl{{h, k, l}};
if (gops.is_systematically_absent(hkl))
continue;
const auto [asu, sign_plus] = rasu.to_asu_sign(hkl, gops);
if (!sign_plus)
continue;
const HKL key{h, k, l};
if (added.find(key) != added.end())
continue;
added.insert(key);
merged.push_back(MergedReflection{
.h = h,
.k = k,
.l = l,
.I = SyntheticIntensityFromAsu(asu),
.sigma = 1.0,
.d = CalcSyntheticD(h, k, l)
});
}
}
}
return merged;
}
}
TEST_CASE("SearchSpaceGroup detects synthetic space groups") {
struct Case {
std::string input_name;
std::string expected_short_name;
};
const std::vector<Case> cases = {
{"P 1", "P1"},
{"P 1 2 1", "P2"},
{"P 3 2 1", "P321"},
{"P 4 2 2", "P422"},
{"P 4 3 2", "P432"},
{"P 6 2 2", "P622"},
{"C 1 2 1", "C2"},
{"C 2 2 2", "C222"},
{"I 4 3 2", "I432"},
};
for (const auto& tc : cases) {
DYNAMIC_SECTION(tc.expected_short_name) {
const gemmi::SpaceGroup& sg = gemmi::get_spacegroup_by_name(tc.input_name);
const auto merged = GenerateMergedReflectionsForSpaceGroup(sg);
SearchSpaceGroupOptions opt;
opt.crystal_system.reset();
opt.centering = '\0';
opt.merge_friedel = true;
opt.d_min_limit_A = 2.0;
opt.min_operator_cc = 0.99;
opt.min_pairs_per_operator = 20;
opt.min_total_compared = 80;
opt.test_systematic_absences = true;
opt.max_mean_absent_i_over_sigma = 0.1;
opt.max_absent_violations = 0;
const auto result = SearchSpaceGroup(merged, opt);
INFO(SearchSpaceGroupResultToText(result));
REQUIRE(result.best_space_group.has_value());
CHECK(result.best_space_group->short_name() == tc.expected_short_name);
}
}
}
+88 -42
View File
@@ -10,6 +10,7 @@
#include <atomic>
#include <chrono>
#include <fstream>
#include <sstream>
#include "../reader/JFJochHDF5Reader.h"
#include "../common/Logger.h"
@@ -25,7 +26,8 @@
#include "../receiver/JFJochReceiverPlots.h"
#include "../compression/JFJochCompressor.h"
#include "../image_analysis/scale_merge/FrenchWilson.h"
#include "../image_analysis/WriteMmcif.h" // (actually include at top of file)
#include "../image_analysis/scale_merge/SearchSpaceGroup.h"
#include "../image_analysis/WriteMmcif.h"
void print_usage(Logger &logger) {
logger.Info("Usage ./jfjoch_analysis {<options>} <input.h5>");
@@ -517,6 +519,7 @@ int main(int argc, char **argv) {
logger.Info("Rotation Indexing found lattice");
}
// --- Optional: run scaling (mosaicity refinement) on accumulated reflections ---
// --- Optional: run scaling (mosaicity refinement) on accumulated reflections ---
if (run_scaling) {
logger.Info("Running scaling (mosaicity refinement) ...");
@@ -528,6 +531,8 @@ int main(int argc, char **argv) {
scale_opts.merge_friedel = !anomalous_mode;
scale_opts.d_min_limit_A = d_min_scale_merge.value_or(0.0);
const bool fixed_space_group = space_group || experiment.GetGemmiSpaceGroup().has_value();
if (space_group)
scale_opts.space_group = *space_group;
else
@@ -538,46 +543,91 @@ int main(int argc, char **argv) {
auto scale_end = std::chrono::steady_clock::now();
double scale_time = std::chrono::duration<double>(scale_end - scale_start).count();
if (scale_result && !fixed_space_group) {
logger.Info("Searching for space group from P1-merged reflections ...");
SearchSpaceGroupOptions sg_opts;
sg_opts.crystal_system.reset();
sg_opts.centering = '\0';
sg_opts.merge_friedel = !anomalous_mode;
sg_opts.d_min_limit_A = d_min_scale_merge.value_or(0.0);
sg_opts.min_i_over_sigma = 0.0;
sg_opts.min_operator_cc = 0.80;
sg_opts.min_pairs_per_operator = 20;
sg_opts.min_total_compared = 100;
sg_opts.test_systematic_absences = true;
sg_opts.max_mean_absent_i_over_sigma = 1.5;
sg_opts.max_absent_violations = 3;
const auto sg_search = SearchSpaceGroup(scale_result->merged, sg_opts);
logger.Info("");
{
std::istringstream iss(SearchSpaceGroupResultToText(sg_search));
std::string line;
while (std::getline(iss, line)) {
if (!line.empty())
logger.Info("{}", line);
}
}
logger.Info("");
if (sg_search.best_space_group.has_value()) {
logger.Info("Re-running scaling in detected space group {}", sg_search.best_space_group->short_name());
scale_opts.space_group = *sg_search.best_space_group;
auto rescale_start = std::chrono::steady_clock::now();
auto refined_scale_result = indexer.ScaleRotationData(scale_opts);
auto rescale_end = std::chrono::steady_clock::now();
if (refined_scale_result) {
scale_result = std::move(refined_scale_result);
scale_time += std::chrono::duration<double>(rescale_end - rescale_start).count();
}
} else {
logger.Warning("No space group accepted; keeping P1-merged result");
}
}
if (scale_result) {
end_msg.scale_factor = scale_result->image_scale_g;
logger.Info("Scaling completed in {:.2f} s ({} unique reflections)",
scale_time, scale_result->merged.size());
logger.Info("Scaling completed in {:.2f} s ({} unique reflections)",
scale_time, scale_result->merged.size());
// Print resolution-shell statistics table
// Print resolution-shell statistics table
{
const auto &stats = scale_result->statistics;
logger.Info("");
logger.Info(" {:>8s} {:>8s} {:>8s} {:>8s} {:>8s} {:>10s}",
"d_min", "N_obs", "N_uniq", "Rmeas", "<I/sig>", "Complete");
logger.Info(" {:->8s} {:->8s} {:->8s} {:->8s} {:->8s} {:->10s}",
"", "", "", "", "", "");
for (const auto &sh: stats.shells) {
if (sh.unique_reflections == 0)
continue;
std::string compl_str = (sh.completeness > 0.0)
? fmt::format("{:8.1f}%", sh.completeness * 100.0)
: " N/A";
logger.Info(" {:8.2f} {:8d} {:8d} {:8.3f}% {:8.1f} {:>10s}",
sh.d_min, sh.total_observations, sh.unique_reflections,
sh.rmeas * 100, sh.mean_i_over_sigma, compl_str);
}
{
const auto& stats = scale_result->statistics;
logger.Info("");
logger.Info(" {:>8s} {:>8s} {:>8s} {:>8s} {:>8s} {:>10s}",
"d_min", "N_obs", "N_uniq", "Rmeas", "<I/sig>", "Complete");
const auto &ov = stats.overall;
logger.Info(" {:->8s} {:->8s} {:->8s} {:->8s} {:->8s} {:->10s}",
"", "", "", "", "", "");
for (const auto& sh : stats.shells) {
if (sh.unique_reflections == 0)
continue;
std::string compl_str = (sh.completeness > 0.0)
? fmt::format("{:8.1f}%", sh.completeness * 100.0)
: " N/A";
logger.Info(" {:8.2f} {:8d} {:8d} {:8.3f}% {:8.1f} {:>10s}",
sh.d_min, sh.total_observations, sh.unique_reflections,
sh.rmeas * 100, sh.mean_i_over_sigma, compl_str);
}
// Overall
{
const auto& ov = stats.overall;
logger.Info(" {:->8s} {:->8s} {:->8s} {:->8s} {:->8s} {:->10s}",
"", "", "", "", "", "");
std::string compl_str = (ov.completeness > 0.0)
? fmt::format("{:8.1f}%", ov.completeness * 100.0)
: " N/A";
logger.Info(" {:>8s} {:8d} {:8d} {:8.3f}% {:8.1f} {:>10s}",
"Overall", ov.total_observations, ov.unique_reflections,
ov.rmeas * 100, ov.mean_i_over_sigma, compl_str);
}
logger.Info("");
std::string compl_str = (ov.completeness > 0.0)
? fmt::format("{:8.1f}%", ov.completeness * 100.0)
: " N/A";
logger.Info(" {:>8s} {:8d} {:8d} {:8.3f}% {:8.1f} {:>10s}",
"Overall", ov.total_observations, ov.unique_reflections,
ov.rmeas * 100, ov.mean_i_over_sigma, compl_str);
}
logger.Info("");
}
// Write image.dat (image_id mosaicity_deg K)
{
const std::string img_path = output_prefix + "_image.dat";
std::ofstream img_file(img_path);
@@ -586,13 +636,13 @@ int main(int argc, char **argv) {
} else {
img_file << "# image_id mosaicity_deg K\n";
for (size_t i = 0; i < scale_result->mosaicity_deg.size(); ++i) {
img_file << i << " " << scale_result->mosaicity_deg[i] << " " << scale_result->image_scale_g[i] << "\n";
img_file << i << " " << scale_result->mosaicity_deg[i] << " " << scale_result->image_scale_g[i]
<< "\n";
}
img_file.close();
}
}
// --- French-Wilson: convert I → F ---
{
FrenchWilsonOptions fw_opts;
fw_opts.acentric = true; // typical for MX
@@ -601,7 +651,6 @@ int main(int argc, char **argv) {
auto fw = FrenchWilson(scale_result->merged, fw_opts);
{
{
// Write scaled.hkl (h k l I sigma)
const std::string hkl_path = output_prefix + "_amplitudes.hkl";
std::ofstream hkl_file(hkl_path);
if (!hkl_file) {
@@ -619,23 +668,20 @@ int main(int argc, char **argv) {
}
MmcifMetadata cif_meta;
// Unit cell — from rotation indexing result or experiment setting
if (rotation_indexer_ret.has_value()) {
cif_meta.unit_cell = rotation_indexer_ret->lattice.GetUnitCell();
} else if (experiment.GetUnitCell().has_value()) {
cif_meta.unit_cell = experiment.GetUnitCell().value();
}
// Space group
if (auto sg = experiment.GetGemmiSpaceGroup(); sg.has_value()) {
if (scale_opts.space_group.has_value()) {
cif_meta.space_group_name = scale_opts.space_group->hm;
cif_meta.space_group_number = scale_opts.space_group->number;
} else if (auto sg = experiment.GetGemmiSpaceGroup(); sg.has_value()) {
cif_meta.space_group_name = sg->hm;
cif_meta.space_group_number = sg->number;
} else if (space_group) {
cif_meta.space_group_name = space_group->hm;
cif_meta.space_group_number = space_group->number;
}
// Detector & experiment info
cif_meta.detector_name = experiment.GetDetectorDescription();
cif_meta.wavelength_A = experiment.GetWavelength_A();
cif_meta.detector_distance_mm = experiment.GetDetectorDistance_mm();