Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m53s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m27s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 15m0s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 20m4s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m33s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 22m4s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 25m16s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 26m14s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 27m26s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 30m26s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 22m20s
Build Packages / Generate python client (push) Successful in 32s
Build Packages / Build documentation (push) Successful in 1m20s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 23m28s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 26m23s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m35s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 25m59s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 26m27s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 22m15s
Build Packages / Unit tests (push) Successful in 1h26m38s
Build Packages / Validation Battery (push) Successful in 32m11s
* jfjoch_broker: Faster live Rugnux analysis, with the GPU indexers prepared at the start of a collection and kept between collections; Rugnux no longer writes `.ccp4` maps (the map coefficients are in `<prefix>_maps.mtz`). Reviewed-on: #87 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
159 lines
7.2 KiB
C++
159 lines
7.2 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "RugnuxOutputFiles.h"
|
|
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
|
|
#include "../writer/FileWriter.h"
|
|
#include "../writer/WriteModel.h"
|
|
#include "../writer/WriteRugnuxFiles.h"
|
|
|
|
namespace {
|
|
|
|
// The files of one output pass at `prefix`, all but its _process.h5.
|
|
void WriteOutputs(const RugnuxOutputs &o, const std::string &prefix, const OutputProvenance &provenance,
|
|
size_t nthreads, Logger &logger) {
|
|
if (o.merged)
|
|
WriteMergedOutput(*o.merged, prefix, provenance, nthreads, /*hkl=*/true);
|
|
if (o.twin)
|
|
WriteMergedOutput(*o.twin, prefix, provenance, nthreads, /*hkl=*/false);
|
|
if (o.model_prior)
|
|
WriteMergedOutput(*o.model_prior, prefix, provenance, nthreads, /*hkl=*/false);
|
|
if (o.maps)
|
|
WriteModelMaps(*o.maps, prefix, logger);
|
|
if (o.model_prior_maps)
|
|
WriteModelMaps(*o.model_prior_maps, prefix + "_mr", logger);
|
|
if (o.placed_model)
|
|
WritePlacedModel(*o.placed_model->model, o.placed_model->cell, o.placed_model->space_group, prefix, logger);
|
|
if (o.model_prior_placed_model)
|
|
WritePlacedModel(*o.model_prior_placed_model->model, o.model_prior_placed_model->cell,
|
|
o.model_prior_placed_model->space_group, prefix + "_mr", logger);
|
|
if (o.unmerged) {
|
|
WriteMtz(*o.unmerged, prefix + "_unmerged.mtz");
|
|
logger.Info("Unmerged observations written to {}_unmerged.mtz", prefix);
|
|
}
|
|
if (o.unmerged_partials) {
|
|
WriteMtz(*o.unmerged_partials, prefix + "_unmerged_partials.mtz");
|
|
logger.Info("Unmerged partials written to {}_unmerged_partials.mtz", prefix);
|
|
}
|
|
if (!o.detector_jpeg.empty()) {
|
|
const std::string path = prefix + "_detector.jpg";
|
|
std::ofstream f(path, std::ofstream::out | std::ofstream::trunc | std::ofstream::binary);
|
|
if (f) {
|
|
f.write(o.detector_jpeg.data(), static_cast<std::streamsize>(o.detector_jpeg.size()));
|
|
logger.Info("Detector diagnostic image written to {}", path);
|
|
} else
|
|
logger.Warning("Cannot open {} for writing", path);
|
|
}
|
|
if (o.plot) {
|
|
WritePerImagePlot(prefix, *o.plot);
|
|
logger.Info("Per-image table written to {}_plot.txt", prefix);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ProcessFileSink::ProcessFileSink(std::string prefix) : prefix_(std::move(prefix)) {}
|
|
|
|
ProcessFileSink::~ProcessFileSink() = default;
|
|
|
|
void ProcessFileSink::Start(const StartMessage &start, const std::string &label) {
|
|
writer_.reset();
|
|
StartMessage s = start;
|
|
s.file_prefix = prefix_ + label;
|
|
writer_ = std::make_unique<FileWriter>(s, /*check_overwrite_at_start=*/true, /*trusted_path=*/true);
|
|
}
|
|
|
|
void ProcessFileSink::Write(const DataMessage &msg) {
|
|
writer_->Write(msg);
|
|
}
|
|
|
|
void ProcessFileSink::End(const EndMessage &end) {
|
|
writer_->WriteHDF5(end);
|
|
writer_->Finalize();
|
|
writer_.reset();
|
|
}
|
|
|
|
void WriteRugnuxOutputs(const ProcessResult &result, const std::string &prefix, const OutputProvenance &provenance,
|
|
size_t nthreads, Logger &logger) {
|
|
if (result.outputs) {
|
|
WriteOutputs(*result.outputs, prefix, provenance, nthreads, logger);
|
|
if (result.outputs->process_start) {
|
|
StartMessage start = *result.outputs->process_start;
|
|
start.file_prefix = prefix;
|
|
FileWriter writer(start, /*check_overwrite_at_start=*/true, /*trusted_path=*/true);
|
|
writer.WriteHDF5(result.end_message);
|
|
writer.Finalize();
|
|
}
|
|
}
|
|
if (result.doubled_cell) {
|
|
const ProcessResult::DoubledCell &h = *result.doubled_cell;
|
|
if (h.other_outputs)
|
|
WriteOutputs(*h.other_outputs, prefix + h.other_suffix, provenance, nthreads, logger);
|
|
// The doubled cell's pass put its per-image analysis under a name of its own; where it is the answer,
|
|
// the two _process.h5 files trade names, as the other files of the two passes are named.
|
|
std::error_code ec;
|
|
if (h.adopted && std::filesystem::exists(prefix + "_doubled_process.h5", ec)) {
|
|
if (std::filesystem::exists(prefix + "_process.h5", ec))
|
|
std::filesystem::rename(prefix + "_process.h5", prefix + h.other_suffix + "_process.h5", ec);
|
|
std::filesystem::rename(prefix + "_doubled_process.h5", prefix + "_process.h5", ec);
|
|
}
|
|
}
|
|
}
|
|
|
|
void WriteMergedOutput(const MergedOutput &merged, const std::string &prefix, const OutputProvenance &provenance,
|
|
size_t nthreads, bool hkl) {
|
|
// An MTZ, an mmCIF and a SHELX HKLF-4 .hkl - each has its uses downstream (MTZ for the CCP4 / phenix
|
|
// reflection tools, mmCIF for deposition and as the self-describing native format, HKLF-4 for SHELXL
|
|
// refinement and SHELXC / ANODE substructure solution). The .hkl holds the unmerged scaled fulls where the
|
|
// merge provides them (rotation), the merged reflections otherwise (stills).
|
|
const std::string file = prefix + merged.suffix;
|
|
WriteMtz(merged.mtz, file + ".mtz");
|
|
WriteMmcifReflections(merged.reflections, merged.cell, merged.experiment, merged.statistics, merged.error_model,
|
|
merged.twinning, provenance, file + ".cif", nthreads);
|
|
if (!hkl)
|
|
return;
|
|
if (!merged.scaled_fulls.empty())
|
|
WriteShelxHklReflections(merged.scaled_fulls, file + ".hkl", nthreads);
|
|
else
|
|
WriteShelxHklReflections(merged.reflections, merged.experiment, file + ".hkl", nthreads);
|
|
}
|
|
|
|
void WriteModelMaps(const ModelMaps &maps, const std::string &prefix, Logger &logger) {
|
|
WriteMtz(maps.coefficients, prefix + "_maps.mtz");
|
|
logger.Info("Model validation: wrote {}_maps.mtz", prefix);
|
|
}
|
|
|
|
void WritePerImagePlot(const std::string &prefix, const PerImagePlot &plot) {
|
|
const std::string path = prefix + "_plot.txt";
|
|
std::ofstream file(path, std::ofstream::out | std::ofstream::trunc);
|
|
if (!file)
|
|
throw JFJochException(JFJochExceptionCategory::FileWriteError, "Cannot open " + path + " for writing");
|
|
file << PerImagePlotText(plot);
|
|
}
|
|
|
|
ReportDocument WriteResultReport(const std::string &output_prefix,
|
|
const std::string &input_file,
|
|
const DiffractionExperiment &experiment,
|
|
const ProcessResult &result,
|
|
Logger &logger,
|
|
const RunProvenance &provenance) {
|
|
ReportDocument doc = BuildReportDocument(output_prefix, input_file, experiment, result, provenance);
|
|
if (output_prefix.empty())
|
|
return doc; // "compute the statistics, persist nothing"
|
|
|
|
const std::string filename = output_prefix + "_report.txt";
|
|
// The report is unconditional, so it must never be the reason a run fails: a run that produced a
|
|
// good .mtz must survive an unwritable path or a full disk. Report the failure and carry on.
|
|
try {
|
|
std::ofstream file(filename);
|
|
file.exceptions(std::ios::failbit | std::ios::badbit);
|
|
file << RenderReportText(doc, provenance.developer);
|
|
} catch (const std::exception &e) {
|
|
logger.Warning("Could not write the results report {}: {}", filename, e.what());
|
|
}
|
|
return doc;
|
|
}
|