Files
Jungfraujoch/writer/WriteRugnuxFiles.cpp
T
leonarski_fandClaude Opus 5.5 aca27be862 jfjoch_writer writes rugnux's .mtz, _maps.mtz and _model.cif from the END blocks
New gemmi-only library JFJochRugnuxFiles (writer/), linked by JFJochWriter, so
the writer needs no analysis library and builds with JFJOCH_WRITER_ONLY:
- WriteMergedMtz / WriteMapCoefficientsMtz / PlacedModelStructure make the
  files from the structured blocks; WriteRugnuxResultFiles writes whichever
  blocks END carries beside the master, each under a temporary name and renamed
  into place, an existing file replaced only when the run's overwrite is set.
  A file that fails costs only itself (logged), not the run.
- FillRugnuxResultBlocks reads the blocks back from the files rugnux wrote at
  a prefix (p.mtz, p_maps.mtz, p_model.cif), for the broker to fill END.

WriteMtzReflections now goes through WriteMergedMtz (MergedReflectionsForWriter
builds the block); its output is byte-identical to before, checked by md5 on a
synthetic set in both the Friedel-merged and the anomalous layout.

StreamWriter writes the files after the master and before the END ACK, so the
ACK means they are in place; END receipt and each rename are logged at INFO
with the wall-clock time, for last-frame-to-files latency. The broker's direct
HDF5 pusher writes them too.

Test: rugnux's three files -> blocks -> CBOR -> writer reproduces all of them
(and _model.pdb) byte for byte; a ~25k-reflection END is 2.7 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 12:19:51 +02:00

211 lines
9.5 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "WriteRugnuxFiles.h"
#include <chrono>
#include <filesystem>
#include <gemmi/mtz.hpp>
#include "WriteModel.h"
#include "../common/Logger.h"
void WriteMergedMtz(const MergedReflectionsMessage &merged, const std::string &filename) {
gemmi::Mtz mtz;
// Optional but recommended metadata
mtz.spacegroup = gemmi::find_spacegroup_by_name(merged.space_group_name);
mtz.set_cell_for_all(merged.unit_cell);
// Producing-software provenance in the MTZ header (title + HISTORY, the CCP4 convention).
mtz.title = "Rugnux merged reflections";
mtz.history = merged.history;
// Add dataset. HKL_base first, so the data dataset is id 1: a dataset written as id 0 is the
// reserved base dataset as far as CCP4's mtzlib is concerned, and the wavelength on it is
// dropped - mtzinfo then reports the Cu K-alpha default 1.54187 A, which is what a program that
// takes f'/f'' from the file would phase on.
mtz.add_base(); // the HKL_base dataset and the H K L columns
gemmi::Mtz::Dataset& ds = mtz.add_dataset("native");
ds.crystal_name = merged.crystal_name;
ds.wavelength = merged.wavelength_A;
const int dataset_id = ds.id;
// One row per reflection in the CCP4 anomalous layout (IMEAN + I(+)/I(-), and the same split for
// the French-Wilson amplitude), which aimless / ctruncate / mtz2sca / ANODE read directly.
const bool has_anom = !merged.i_plus.empty();
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);
const size_t n = merged.h.size();
mtz.nreflections = static_cast<int>(n);
mtz.data.reserve(n * 16);
for (size_t i = 0; i < n; i++) {
mtz.data.push_back(static_cast<float>(merged.h[i]));
mtz.data.push_back(static_cast<float>(merged.k[i]));
mtz.data.push_back(static_cast<float>(merged.l[i]));
mtz.data.push_back(merged.i_mean[i]);
mtz.data.push_back(merged.sigma_i_mean[i]);
if (has_anom) {
mtz.data.push_back(merged.i_plus[i]);
mtz.data.push_back(merged.sigma_i_plus[i]);
mtz.data.push_back(merged.i_minus[i]);
mtz.data.push_back(merged.sigma_i_minus[i]);
}
mtz.data.push_back(merged.f[i]);
mtz.data.push_back(merged.sigma_f[i]);
if (has_anom) {
mtz.data.push_back(merged.f_plus[i]);
mtz.data.push_back(merged.sigma_f_plus[i]);
mtz.data.push_back(merged.f_minus[i]);
mtz.data.push_back(merged.sigma_f_minus[i]);
}
mtz.data.push_back(static_cast<float>(merged.free_flag[i]));
}
mtz.write_to_file(filename);
}
void WriteMapCoefficientsMtz(const MapCoefficientsMessage &coefficients, const std::string &filename) {
gemmi::Mtz mtz(true);
mtz.spacegroup = gemmi::find_spacegroup_by_name(coefficients.space_group_name);
mtz.set_cell_for_all(coefficients.unit_cell);
mtz.add_dataset("model_validation");
mtz.datasets.back().wavelength = coefficients.wavelength_A;
mtz.add_column("FP", 'F', -1, -1, false);
mtz.add_column("FC", 'F', -1, -1, false);
mtz.add_column("PHIC", 'P', -1, -1, false);
mtz.add_column("FWT", 'F', -1, -1, false);
mtz.add_column("PHWT", 'P', -1, -1, false);
mtz.add_column("DELFWT", 'F', -1, -1, false);
mtz.add_column("PHDELWT", 'P', -1, -1, false);
// The figure of merit the coefficients carry, so the weighting can be read off - and undone -
// from the file rather than having to be taken on trust.
mtz.add_column("FOM", 'W', -1, -1, false);
mtz.add_column("FREE", 'I', -1, -1, false);
const size_t n = coefficients.h.size();
mtz.data.reserve(n * 12);
for (size_t i = 0; i < n; i++)
mtz.data.insert(mtz.data.end(), {static_cast<float>(coefficients.h[i]),
static_cast<float>(coefficients.k[i]),
static_cast<float>(coefficients.l[i]),
coefficients.fp[i], coefficients.fc[i], coefficients.phic[i],
coefficients.fwt[i], coefficients.phwt[i],
coefficients.delfwt[i], coefficients.phdelwt[i],
coefficients.fom[i],
static_cast<float>(coefficients.free_flag[i])});
mtz.nreflections = static_cast<int>(n);
mtz.write_to_file(filename);
}
gemmi::Structure PlacedModelStructure(const PlacedModelMessage &model) {
gemmi::Structure st;
st.name = model.name;
st.cell = model.unit_cell;
st.spacegroup_hm = model.model_space_group_name;
const bool has_aniso = !model.aniso.empty();
gemmi::Model *m = nullptr;
gemmi::Chain *ch = nullptr;
gemmi::Residue *res = nullptr;
for (size_t i = 0; i < model.atom_name.size(); i++) {
if (m == nullptr || m->num != model.model_number[i]) {
m = &st.models.emplace_back(model.model_number[i]);
ch = nullptr;
}
if (ch == nullptr || ch->name != model.chain[i]) {
ch = &m->chains.emplace_back(model.chain[i]);
res = nullptr;
}
const gemmi::SeqId seqid(model.seq_num[i], static_cast<char>(model.icode[i]));
if (res == nullptr || res->name != model.residue_name[i] || res->seqid != seqid
|| res->het_flag != static_cast<char>(model.het_flag[i])) {
res = &ch->residues.emplace_back(gemmi::ResidueId{seqid, {}, model.residue_name[i]});
res->het_flag = static_cast<char>(model.het_flag[i]);
}
gemmi::Atom &a = res->atoms.emplace_back();
a.name = model.atom_name[i];
a.element = gemmi::Element(model.element[i]);
a.altloc = static_cast<char>(model.altloc[i]);
a.charge = static_cast<signed char>(model.charge[i]);
a.pos = gemmi::Position(model.x[i], model.y[i], model.z[i]);
a.b_iso = model.b_iso[i];
a.occ = model.occupancy[i];
if (has_aniso)
a.aniso = {model.aniso[6 * i], model.aniso[6 * i + 1], model.aniso[6 * i + 2],
model.aniso[6 * i + 3], model.aniso[6 * i + 4], model.aniso[6 * i + 5]};
}
st.setup_cell_images();
return st;
}
namespace {
double WallClockSeconds() {
return std::chrono::duration<double>(std::chrono::system_clock::now().time_since_epoch()).count();
}
// Rename tmp onto final, unless final is there already and may not be overwritten.
void RenameIntoPlace(const std::string &tmp, const std::string &final_name, bool overwrite, Logger &logger) {
if (!overwrite && std::filesystem::exists(final_name)) {
std::filesystem::remove(tmp);
logger.Error("{} already exists and overwrite is off - not written", final_name);
return;
}
std::filesystem::rename(tmp, final_name);
logger.Info("{} written, renamed into place at wall clock {:.3f} s", final_name, WallClockSeconds());
}
} // namespace
void WriteRugnuxResultFiles(const EndMessage &end, const std::string &file_prefix, bool overwrite,
Logger &logger) {
// A file that cannot be written costs only itself: the master is already in place.
try {
if (end.merged) {
WriteMergedMtz(*end.merged, file_prefix + ".mtz.tmp");
RenameIntoPlace(file_prefix + ".mtz.tmp", file_prefix + ".mtz", overwrite, logger);
}
} catch (const std::exception &e) {
logger.Error("Cannot write {}.mtz: {}", file_prefix, e.what());
}
try {
if (end.map_coefficients) {
WriteMapCoefficientsMtz(*end.map_coefficients, file_prefix + "_maps.mtz.tmp");
RenameIntoPlace(file_prefix + "_maps.mtz.tmp", file_prefix + "_maps.mtz", overwrite, logger);
}
} catch (const std::exception &e) {
logger.Error("Cannot write {}_maps.mtz: {}", file_prefix, e.what());
}
try {
if (end.placed_model) {
const PlacedModelMessage &model = *end.placed_model;
// WritePlacedModel appends _model.cif and _model.pdb to the prefix it is given.
const std::string tmp_prefix = file_prefix + ".tmp";
WritePlacedModel(PlacedModelStructure(model), model.unit_cell,
*gemmi::find_spacegroup_by_name(model.space_group_name), tmp_prefix, logger);
for (const std::string suffix : {"_model.cif", "_model.pdb"})
if (std::filesystem::exists(tmp_prefix + suffix))
RenameIntoPlace(tmp_prefix + suffix, file_prefix + suffix, overwrite, logger);
}
} catch (const std::exception &e) {
logger.Error("Cannot write {}_model.cif: {}", file_prefix, e.what());
}
}