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)
104 lines
3.9 KiB
C++
104 lines
3.9 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "RugnuxModelFile.h"
|
|
|
|
#include <fstream>
|
|
#include <iterator>
|
|
#include <stdexcept>
|
|
|
|
#include <zlib.h>
|
|
#include <nlohmann/json.hpp>
|
|
#include <gemmi/mmread.hpp>
|
|
#include <gemmi/calculate.hpp> // count_atom_sites
|
|
|
|
namespace {
|
|
|
|
// The bytes of a gzip stream inflated, every member of it.
|
|
std::string Gunzip(const std::string &bytes) {
|
|
z_stream s{};
|
|
if (inflateInit2(&s, 15 + 32) != Z_OK)
|
|
throw std::runtime_error("cannot start zlib");
|
|
std::string out;
|
|
char buf[1 << 16];
|
|
s.next_in = reinterpret_cast<Bytef *>(const_cast<char *>(bytes.data()));
|
|
s.avail_in = static_cast<uInt>(bytes.size());
|
|
int ret = Z_OK;
|
|
while (ret != Z_STREAM_END || s.avail_in > 0) {
|
|
if (ret == Z_STREAM_END)
|
|
inflateReset(&s);
|
|
s.next_out = reinterpret_cast<Bytef *>(buf);
|
|
s.avail_out = sizeof buf;
|
|
ret = inflate(&s, Z_NO_FLUSH);
|
|
if (ret != Z_OK && ret != Z_STREAM_END) {
|
|
inflateEnd(&s);
|
|
throw std::runtime_error("the gzip stream is damaged");
|
|
}
|
|
out.append(buf, sizeof buf - s.avail_out);
|
|
if (ret == Z_OK && s.avail_in == 0 && s.avail_out != 0) {
|
|
inflateEnd(&s);
|
|
throw std::runtime_error("the gzip stream is cut short");
|
|
}
|
|
}
|
|
inflateEnd(&s);
|
|
return out;
|
|
}
|
|
|
|
std::string ReadFile(const std::string &path) {
|
|
std::ifstream f(path, std::ios::binary);
|
|
if (!f)
|
|
throw std::runtime_error("cannot open " + path);
|
|
return {std::istreambuf_iterator<char>(f), std::istreambuf_iterator<char>()};
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::string GunzipIfCompressed(const std::string &bytes) {
|
|
return bytes.size() >= 2 && bytes[0] == '\x1f' && bytes[1] == '\x8b' ? Gunzip(bytes) : bytes;
|
|
}
|
|
|
|
RugnuxModel ParseRugnuxModel(const std::string &bytes, const std::string &name, const std::string &pae_json) {
|
|
RugnuxModel m;
|
|
m.name = name;
|
|
std::string text = GunzipIfCompressed(bytes);
|
|
gemmi::cif::Document doc;
|
|
m.structure = gemmi::read_structure_from_memory(text.data(), text.size(), name, gemmi::CoorFormat::Detect, &doc);
|
|
if (gemmi::count_atom_sites(m.structure) == 0)
|
|
throw std::runtime_error(name + " holds no atoms");
|
|
m.modelcif = m.structure.input_format == gemmi::CoorFormat::Mmcif && !doc.blocks.empty()
|
|
&& doc.blocks[0].has_mmcif_category("_ma_qa_metric");
|
|
if (!pae_json.empty()) {
|
|
try {
|
|
const nlohmann::json j = nlohmann::json::parse(pae_json);
|
|
const nlohmann::json &pae = j.is_array() ? j.at(0).at("predicted_aligned_error")
|
|
: j.at("predicted_aligned_error");
|
|
std::vector<std::vector<double>> rows;
|
|
for (const auto &row : pae)
|
|
rows.push_back(row.get<std::vector<double>>());
|
|
m.pae = std::move(rows);
|
|
} catch (const std::exception &) {
|
|
}
|
|
}
|
|
return m;
|
|
}
|
|
|
|
RugnuxModel ReadRugnuxModelFile(const std::string &path) {
|
|
const std::string pae_path = PaePathFor(path);
|
|
std::string pae_json;
|
|
if (!pae_path.empty() && std::ifstream(pae_path))
|
|
pae_json = ReadFile(pae_path);
|
|
return ParseRugnuxModel(ReadFile(path), path, pae_json);
|
|
}
|
|
|
|
std::string PaePathFor(const std::string &model_path) {
|
|
const size_t slash = model_path.find_last_of("/\\");
|
|
const std::string dir = slash == std::string::npos ? std::string() : model_path.substr(0, slash + 1);
|
|
const std::string name = model_path.substr(dir.size());
|
|
const size_t at = name.rfind("-model_v");
|
|
if (at == std::string::npos)
|
|
return {};
|
|
const size_t dot = name.find('.', at);
|
|
const std::string version = name.substr(at + 8, dot == std::string::npos ? std::string::npos : dot - at - 8);
|
|
return dir + name.substr(0, at) + "-predicted_aligned_error_v" + version + ".json";
|
|
}
|