rugnux --mode refine --model model.cif -o out data.mtz The input is an MTZ or a structure-factor mmCIF, gzipped or not, by content (writer/RugnuxReflectionFile: parsing in the file layer, the library reads no file). Columns are found, not named: F/SIGF-like amplitudes (calculated amplitudes and map coefficients passed over), else intensities made into amplitudes by French-Wilson, else the mean of the anomalous pairs; the free set from a FreeR_flag-like column (0 the test set, or the minority value), else rugnux's own 5 % set. RunRefineOnly (rugnux/RugnuxRefine) runs RefineModel; the outputs are --refine's (_refined_model.cif, _refined_maps.mtz, _blobs.json) and a report with the REFINE_ keys, the input's REFINE_FRENCH_WILSON / REFINE_FREE_FLAGS, and every term of the start score under REFINE_SCORE_ (also in --refine's section). --refine-score-only scores the model as given; --refine-d-min cuts the resolution. tools/battery/refine_parity.py runs REFMAC 5.8 at 0 cycles and rugnux --refine-score-only on the same files and prints the terms side by side, with R by resolution from both programs' model amplitudes on the same reflections. Tests: [refine_only] - column choice (observed over map coefficients, intensities, 1-marks-free flags, no flags, no usable column), an MTZ read from its bytes, and the mode end to end on a synthetic cell (French-Wilson, generated flags, score-only, the report keys). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu (cherry picked from commit 63ee830c39c988588cffb8c9d4638a678815ec2c) (cherry picked from commit d7397311e6ce0d3bb37ac91e8973b6052e38589f)
248 lines
10 KiB
C++
248 lines
10 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "RugnuxReflectionFile.h"
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <fstream>
|
|
#include <iterator>
|
|
#include <stdexcept>
|
|
|
|
#include <gemmi/cif2mtz.hpp>
|
|
#include <gemmi/read_cif.hpp>
|
|
#include <gemmi/refln.hpp>
|
|
|
|
#include "RugnuxModelFile.h" // GunzipIfCompressed
|
|
|
|
namespace {
|
|
|
|
std::string Lower(std::string s) {
|
|
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); });
|
|
return s;
|
|
}
|
|
|
|
// The sigma of column `c`: the type-Q column named SIG<label>, else the type-Q column right after it.
|
|
const gemmi::Mtz::Column *SigmaOf(const gemmi::Mtz &mtz, const gemmi::Mtz::Column &c, char type) {
|
|
if (const gemmi::Mtz::Column *s = mtz.column_with_label("SIG" + c.label, nullptr, type))
|
|
return s;
|
|
if (c.idx + 1 < mtz.columns.size() && mtz.columns[c.idx + 1].type == type)
|
|
return &mtz.columns[c.idx + 1];
|
|
return nullptr;
|
|
}
|
|
|
|
// Labels of amplitudes that are not observations: calculated structure factors and map coefficients.
|
|
bool CalculatedAmplitude(const std::string &label) {
|
|
const std::string l = Lower(label);
|
|
for (const char *p : {"fc", "fwt", "delfwt", "2fofc", "fofc", "f-model", "fmodel", "fcalc", "fpart", "fmask",
|
|
"fom", "fdiff", "fano", "dano", "fem"})
|
|
if (l.starts_with(p))
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
// A value column of `type` with its sigma: the names in `preferred` first, then any (that `accept` takes).
|
|
template <typename Accept>
|
|
std::pair<const gemmi::Mtz::Column *, const gemmi::Mtz::Column *>
|
|
FindPair(const gemmi::Mtz &mtz, char type, std::initializer_list<const char *> preferred, Accept accept) {
|
|
for (const char *label : preferred)
|
|
if (const gemmi::Mtz::Column *c = mtz.column_with_label(label, nullptr, type))
|
|
if (const gemmi::Mtz::Column *s = SigmaOf(mtz, *c, 'Q'))
|
|
return {c, s};
|
|
for (const gemmi::Mtz::Column &c : mtz.columns)
|
|
if (c.type == type && accept(c.label))
|
|
if (const gemmi::Mtz::Column *s = SigmaOf(mtz, c, 'Q'))
|
|
return {&c, s};
|
|
return {nullptr, nullptr};
|
|
}
|
|
|
|
// The free-set flag column: FreeR_flag and the other usual names first, then any type-I column with "free" in its name.
|
|
const gemmi::Mtz::Column *FindFree(const gemmi::Mtz &mtz) {
|
|
for (const char *label : {"FreeR_flag", "FREE", "FREER", "RFREE", "R-free-flags", "FreeRflag", "FREE_FLAG"})
|
|
if (const gemmi::Mtz::Column *c = mtz.column_with_label(label, nullptr, 'I'))
|
|
return c;
|
|
for (const gemmi::Mtz::Column &c : mtz.columns)
|
|
if (c.type == 'I' && Lower(c.label).find("free") != std::string::npos)
|
|
return &c;
|
|
return nullptr;
|
|
}
|
|
|
|
// The anomalous pair of `value_type` (G or K) with sigmas of `sigma_type` (L or M), named X(+) and X(-).
|
|
bool FindAnomalous(const gemmi::Mtz &mtz, char value_type, char sigma_type, const gemmi::Mtz::Column *out[4]) {
|
|
for (const gemmi::Mtz::Column &c : mtz.columns) {
|
|
if (c.type != value_type || !c.label.ends_with("(+)"))
|
|
continue;
|
|
const std::string base = c.label.substr(0, c.label.size() - 3);
|
|
const gemmi::Mtz::Column *m = mtz.column_with_label(base + "(-)", nullptr, value_type);
|
|
const gemmi::Mtz::Column *sp = SigmaOf(mtz, c, sigma_type);
|
|
const gemmi::Mtz::Column *sm = m != nullptr ? SigmaOf(mtz, *m, sigma_type) : nullptr;
|
|
if (m != nullptr && sp != nullptr && sm != nullptr) {
|
|
out[0] = &c;
|
|
out[1] = sp;
|
|
out[2] = m;
|
|
out[3] = sm;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// gemmi's SF-mmCIF -> MTZ conversion with _refln.status preferred to _refln.pdbx_r_free_flag for the free set
|
|
// where status marks any reflection free ('f'), as LoadFCalcFromMtz.cpp reads a deposition.
|
|
std::vector<std::string> SfCifSpec(const gemmi::cif::Loop &loop) {
|
|
bool status_has_free = false;
|
|
const int status = loop.find_tag("_refln.status");
|
|
if (status >= 0)
|
|
for (size_t i = status; i < loop.values.size(); i += loop.width())
|
|
if (gemmi::cif::as_string(loop.values[i]) == "f") {
|
|
status_has_free = true;
|
|
break;
|
|
}
|
|
std::vector<std::string> spec;
|
|
for (const char **line = gemmi::CifToMtz::default_spec(true); *line != nullptr; ++line) {
|
|
const std::string l = *line;
|
|
if ((status_has_free && l.starts_with("pdbx_r_free_flag ")) || (!status_has_free && l.starts_with("status ")))
|
|
continue;
|
|
spec.push_back(l);
|
|
}
|
|
return spec;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
RugnuxReflections RugnuxReflectionsFromMtz(const gemmi::Mtz &mtz, const std::string &name) {
|
|
RugnuxReflections out;
|
|
out.name = name;
|
|
out.cell = mtz.get_cell();
|
|
if (mtz.spacegroup == nullptr)
|
|
throw std::runtime_error(name + " names no space group");
|
|
out.space_group = mtz.spacegroup->xhm();
|
|
for (const gemmi::Mtz::Dataset &ds : mtz.datasets)
|
|
if (ds.wavelength > 0)
|
|
out.wavelength_A = ds.wavelength;
|
|
|
|
auto [value, sigma] = FindPair(mtz, 'F', {"F", "FP", "FOBS", "F-obs", "FMEAN", "F_meas", "Fobs"},
|
|
[](const std::string &l) { return !CalculatedAmplitude(l); });
|
|
if (value == nullptr) {
|
|
std::tie(value, sigma) = FindPair(mtz, 'J', {"IMEAN", "I", "IOBS", "I-obs", "Iobs", "I_meas"},
|
|
[](const std::string &) { return true; });
|
|
out.amplitudes = false;
|
|
}
|
|
const gemmi::Mtz::Column *anom[4] = {nullptr, nullptr, nullptr, nullptr};
|
|
if (value == nullptr) {
|
|
out.amplitudes = FindAnomalous(mtz, 'G', 'L', anom);
|
|
if (!out.amplitudes && !FindAnomalous(mtz, 'K', 'M', anom))
|
|
throw std::runtime_error(name + " holds no amplitudes or intensities with their sigmas");
|
|
out.value_column = anom[0]->label + "," + anom[2]->label;
|
|
out.sigma_column = anom[1]->label + "," + anom[3]->label;
|
|
} else {
|
|
out.value_column = value->label;
|
|
out.sigma_column = sigma->label;
|
|
}
|
|
|
|
const gemmi::Mtz::Column *free = FindFree(mtz);
|
|
std::vector<int> raw_free;
|
|
const size_t stride = mtz.columns.size();
|
|
for (size_t r = 0; r < static_cast<size_t>(mtz.nreflections); r++) {
|
|
const float *row = &mtz.data[r * stride];
|
|
float v = NAN, s = NAN;
|
|
if (value != nullptr) {
|
|
v = row[value->idx];
|
|
s = row[sigma->idx];
|
|
} else {
|
|
// the mean of the two hands where both were measured, the one measured otherwise
|
|
const float vp = row[anom[0]->idx], sp = row[anom[1]->idx], vm = row[anom[2]->idx], sm = row[anom[3]->idx];
|
|
if (std::isfinite(vp) && std::isfinite(vm)) {
|
|
v = 0.5f * (vp + vm);
|
|
s = 0.5f * std::sqrt(sp * sp + sm * sm);
|
|
} else if (std::isfinite(vp)) {
|
|
v = vp;
|
|
s = sp;
|
|
} else {
|
|
v = vm;
|
|
s = sm;
|
|
}
|
|
}
|
|
if (!std::isfinite(v))
|
|
continue;
|
|
MergedReflection m;
|
|
m.h = static_cast<int32_t>(row[0]);
|
|
m.k = static_cast<int32_t>(row[1]);
|
|
m.l = static_cast<int32_t>(row[2]);
|
|
m.d = static_cast<float>(out.cell.calculate_d({{m.h, m.k, m.l}}));
|
|
if (out.amplitudes) {
|
|
m.F = v;
|
|
m.sigmaF = s;
|
|
} else {
|
|
m.I = v;
|
|
m.sigma = s;
|
|
}
|
|
out.reflections.push_back(m);
|
|
if (free != nullptr) {
|
|
const float f = row[free->idx];
|
|
raw_free.push_back(std::isfinite(f) ? static_cast<int>(std::lround(f)) : -1);
|
|
}
|
|
}
|
|
if (out.reflections.empty())
|
|
throw std::runtime_error(name + " holds no reflection with a value in " + out.value_column);
|
|
|
|
if (free != nullptr) {
|
|
size_t zeros = 0, flagged = 0;
|
|
for (int f : raw_free) {
|
|
zeros += f == 0;
|
|
flagged += f >= 0;
|
|
}
|
|
const bool free_is_zero = zeros * 2 <= flagged;
|
|
for (size_t i = 0; i < out.reflections.size(); i++) {
|
|
const bool is_free = raw_free[i] >= 0 && (raw_free[i] == 0) == free_is_zero;
|
|
out.reflections[i].rfree_flag = is_free;
|
|
out.n_free += is_free;
|
|
}
|
|
// a column that marks nothing free is no free set
|
|
if (out.n_free > 0)
|
|
out.free_column = free->label;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
RugnuxReflections ParseRugnuxReflections(const std::string &bytes, const std::string &name) {
|
|
const std::string text = GunzipIfCompressed(bytes);
|
|
if (text.size() >= 4 && std::memcmp(text.data(), "MTZ ", 4) == 0) {
|
|
gemmi::Mtz mtz;
|
|
gemmi::MemoryStream stream(text.data(), text.size());
|
|
mtz.read_stream(stream, true);
|
|
RugnuxReflections out = RugnuxReflectionsFromMtz(mtz, name);
|
|
out.source = "MTZ";
|
|
return out;
|
|
}
|
|
gemmi::cif::Document doc = gemmi::read_cif_from_memory(text.data(), text.size(), name.c_str());
|
|
std::vector<gemmi::ReflnBlock> blocks = gemmi::as_refln_blocks(std::move(doc.blocks));
|
|
gemmi::Logger logger;
|
|
std::string why = name + " holds no merged reflection block";
|
|
for (gemmi::ReflnBlock &rb : blocks) {
|
|
if (rb.refln_loop == nullptr)
|
|
continue;
|
|
gemmi::CifToMtz converter;
|
|
converter.spec_lines = SfCifSpec(*rb.refln_loop);
|
|
try {
|
|
const gemmi::Mtz mtz = converter.convert_block_to_mtz(rb, logger);
|
|
RugnuxReflections out = RugnuxReflectionsFromMtz(mtz, name);
|
|
out.source = "mmCIF block " + rb.block.name;
|
|
return out;
|
|
} catch (const std::exception &e) {
|
|
why = e.what();
|
|
}
|
|
}
|
|
throw std::runtime_error(why);
|
|
}
|
|
|
|
RugnuxReflections ReadRugnuxReflectionFile(const std::string &path) {
|
|
std::ifstream f(path, std::ios::binary);
|
|
if (!f)
|
|
throw std::runtime_error("cannot open " + path);
|
|
const std::string bytes{std::istreambuf_iterator<char>(f), std::istreambuf_iterator<char>()};
|
|
return ParseRugnuxReflections(bytes, path);
|
|
}
|