From e16db07ffd0dfc17f275cf6571a60deecb06f2e3 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sat, 17 Oct 2026 18:13:23 +0200 Subject: [PATCH] broker rugnux live: no files, END built from the result in memory, and review fixes No files. The receiver no longer makes a /tmp scratch directory or reads rugnux's files back: AddToEndMessage moves the merged MtzTable and the map coefficients out of ProcessResult::outputs and converts the placed model with PlacedModelFromStructure (now given the data cell, as WritePlacedModel is). FillRugnuxResultBlocks and MtzTableFromFile are gone; their test now builds the blocks from the same in-memory outputs and still checks the writer reproduces rugnux's own files byte for byte. The live status report is built from the result with the writer's file prefix as its label. AddToEndMessage never throws: a failure is logged and the END goes without the blocks. Review fixes: - MakeRugnuxLiveSetup sets nthreads_auto, as a bare `rugnux` does, so the per-image loop keeps to GPUWorkersPerCard per card instead of starting one GPU worker per CPU thread; the 60-degree probe gets a quarter of the threads (at least 2). - A failure setting rugnux up (start message, reader, thread start) marks the live status Failed and nothing else; the collection goes on as in Lite. MeasurementThread and ForwardThread also catch std::exception, so no exception skips the latch or the reader's Ended(). - The ParallelFor pool is lowered to nice 10 only where it can be raised back (CAP_SYS_NICE, or RLIMIT_NICE reaching 0); otherwise it is left alone and that is logged once. - The preview and metadata sockets get the END without the result blocks. - Receiver plots and scan result are fed only from the canonical pass (RugnuxObserver::OnImagePass): the pre-pass reports [0, E) again at the header geometry and the plots accumulate, so it would count those images twice. The canonical pass reads every image once. - A collection the image buffer cannot hold is still not analysed, and now says so in the broker status (Warning) as well as the rugnux status. With rugnux live off nothing changes: the extra END resets are of empty blocks and analysis_warning is empty. Co-Authored-By: Claude Opus 5.5 --- broker/JFJochStateMachine.cpp | 4 + receiver/JFJochReceiver.cpp | 5 + receiver/JFJochReceiverOutput.h | 3 + receiver/JFJochReceiverRugnux.cpp | 165 ++++++++++++++++++++---------- receiver/JFJochReceiverRugnux.h | 6 +- rugnux/Rugnux.h | 4 + rugnux/RugnuxImagePass.cpp | 4 +- rugnux/RugnuxLive.cpp | 2 + tests/RugnuxResultFilesTest.cpp | 46 +++++++-- writer/RugnuxResultBlocks.cpp | 63 +----------- writer/RugnuxResultBlocks.h | 15 +-- 11 files changed, 183 insertions(+), 134 deletions(-) diff --git a/broker/JFJochStateMachine.cpp b/broker/JFJochStateMachine.cpp index 97c880ded..29ee052d0 100644 --- a/broker/JFJochStateMachine.cpp +++ b/broker/JFJochStateMachine.cpp @@ -556,6 +556,10 @@ void JFJochStateMachine::MeasurementThread() { SetState(JFJochState::Idle, "Stream receiver (writer or downstream analysis) cannot cope with data; reduce frame rate", BrokerStatus::MessageSeverity::Warning); + else if (!tmp_output.receiver_output.analysis_warning.empty()) + SetState(JFJochState::Idle, + "Data collection without problems, but " + tmp_output.receiver_output.analysis_warning, + BrokerStatus::MessageSeverity::Warning); else SetState(JFJochState::Idle, "Data collection without problems", diff --git a/receiver/JFJochReceiver.cpp b/receiver/JFJochReceiver.cpp index 42910ecb1..b7d0d1cdc 100644 --- a/receiver/JFJochReceiver.cpp +++ b/receiver/JFJochReceiver.cpp @@ -206,6 +206,11 @@ void JFJochReceiver::SendEndMessage() { logger.Info("Disconnected from writers"); } + // The result blocks (AddToEndMessage) are megabytes only the writer needs. + message.merged.reset(); + message.map_coefficients.reset(); + message.placed_model.reset(); + if (zmq_metadata_socket != nullptr) zmq_metadata_socket->EndDataCollection(message); diff --git a/receiver/JFJochReceiverOutput.h b/receiver/JFJochReceiverOutput.h index edf086d85..f6ce317b2 100644 --- a/receiver/JFJochReceiverOutput.h +++ b/receiver/JFJochReceiverOutput.h @@ -26,6 +26,9 @@ struct JFJochReceiverOutput { std::string writer_err; std::optional images_written; MeanProcessingTime processing_time; + // A problem with the analysis alone (the images are all there), for the broker's status: why a live + // rugnux run was skipped. Empty otherwise. + std::string analysis_warning; }; diff --git a/receiver/JFJochReceiverRugnux.cpp b/receiver/JFJochReceiverRugnux.cpp index b4e9daf2a..d64e65528 100644 --- a/receiver/JFJochReceiverRugnux.cpp +++ b/receiver/JFJochReceiverRugnux.cpp @@ -4,7 +4,7 @@ #include "JFJochReceiverRugnux.h" #include -#include +#include #include #include #include @@ -26,6 +26,17 @@ namespace { std::chrono::steady_clock::now().time_since_epoch()).count(); } + // Whether this process may set a thread back to nice 0 once it has lowered it: CAP_SYS_NICE, or an + // RLIMIT_NICE that reaches 0 (the limit is 20 - nice). + bool CanRaiseNiceToZero() { + __user_cap_header_struct header{_LINUX_CAPABILITY_VERSION_3, 0}; + __user_cap_data_struct caps[_LINUX_CAPABILITY_U32S_3]{}; + if (syscall(SYS_capget, &header, caps) == 0 && (caps[0].effective & (1u << CAP_SYS_NICE))) + return true; + rlimit limit{}; + return getrlimit(RLIMIT_NICE, &limit) == 0 && (limit.rlim_cur == RLIM_INFINITY || limit.rlim_cur >= 20); + } + double SecondsSince(int64_t ns) { return static_cast(SteadyNowNs() - ns) / 1e9; } @@ -49,21 +60,25 @@ namespace { } }; - // What rugnux reports as it goes: its per-image results into the receiver's plots and scan - // result, which a later pass over the same image overwrites - so the canonical pass, the last to - // read every image, is what stays - and its steps into the log. + // What rugnux reports as it goes: the per-image results of its canonical pass into the receiver's + // plots and scan result - the plots accumulate, so an image reported by the pre-pass as well would be + // counted twice - and its steps into the log. class LiveObserver : public RugnuxObserver { JFJochReceiverPlots &plots; ScanResultGenerator &scan_result; const AzimuthalIntegrationMapping &mapping; RugnuxLiveStatus &status; Logger &logger; + std::atomic canonical{false}; public: LiveObserver(JFJochReceiverPlots &plots, ScanResultGenerator &scan_result, const AzimuthalIntegrationMapping &mapping, RugnuxLiveStatus &status, Logger &logger) : plots(plots), scan_result(scan_result), mapping(mapping), status(status), logger(logger) {} + void OnImagePass(bool c) override { canonical = c; } void OnImageProcessed(const DataMessage &msg) override { + if (!canonical) + return; plots.Add(msg, AzimuthalIntegrationProfile(mapping)); scan_result.Add(msg); } @@ -166,27 +181,23 @@ void JFJochReceiverRugnux::MeasurementThread() { const auto slots = image_buffer.GetStatus().total_slots; live_status.Start(experiment.GetImageNum()); if (slots < experiment.GetImageNum()) { - logger.Error("Image buffer holds {} images, the collection has {}: no rugnux analysis for it", - slots, experiment.GetImageNum()); + skipped_warning = fmt::format("no rugnux analysis: the image buffer holds {} images, the collection " + "has {}", slots, experiment.GetImageNum()); + logger.Error("{}", skipped_warning); live_status.Finish(RugnuxLiveState::Skipped, "the image buffer cannot hold the whole collection"); - } else { - // The start message as the writer gets it, so rugnux sees the dataset an HDF5 master states. - std::vector start_cbor; - image_buffer.GetStartMessage(start_cbor); - reader.Open(CBORStream2Deserialize(start_cbor)->start_message.value()); - char dir_template[] = "/tmp/jfjoch_rugnux_XXXXXX"; - if (mkdtemp(dir_template) == nullptr) - throw JFJochException(JFJochExceptionCategory::FileWriteError, "Cannot create rugnux scratch directory"); - scratch_dir = dir_template; - rugnux_done = std::async(std::launch::async, &JFJochReceiverRugnux::RugnuxThread, this); - probe_done = std::async(std::launch::async, &JFJochReceiverRugnux::ProbeThread, this); - } + } else + StartRugnux(rugnux_done, probe_done); measurement_started.count_down(); } catch (const JFJochException &e) { logger.ErrorException(e); Cancel(e); measurement_started.count_down(); throw; + } catch (const std::exception &e) { + logger.Error("Cancelling data collection due to exception: {}", e.what()); + cancelled = true; + measurement_started.count_down(); + throw; } logger.Info("Receiving started"); @@ -229,21 +240,43 @@ void JFJochReceiverRugnux::MeasurementThread() { written_s, end_sent_s); } - if (!scratch_dir.empty()) { - std::error_code ec; - std::filesystem::remove_all(scratch_dir, ec); - } end_time = std::chrono::system_clock::now(); current_status.SetProgress({}); current_status.SetStatus(GetStatus()); } +void JFJochReceiverRugnux::StartRugnux(std::future &rugnux_done, std::future &probe_done) { + // A failure here costs the analysis only: the images go on to the writer as they would without it. + try { + // The start message as the writer gets it, so rugnux sees the dataset an HDF5 master states. + std::vector start_cbor; + image_buffer.GetStartMessage(start_cbor); + reader.Open(CBORStream2Deserialize(start_cbor)->start_message.value()); + rugnux_done = std::async(std::launch::async, &JFJochReceiverRugnux::RugnuxThread, this); + probe_done = std::async(std::launch::async, &JFJochReceiverRugnux::ProbeThread, this); + } catch (const std::exception &e) { + logger.Error("rugnux live: cannot start - {}", e.what()); + live_status.Finish(RugnuxLiveState::Failed, e.what()); + } +} + void JFJochReceiverRugnux::RugnuxThread() { // Below the forward threads: every thread rugnux starts inherits this, so an image arriving finds a // core however busy rugnux keeps the machine. // The process-wide ParallelFor pool is not started by this thread, so it is lowered separately. setpriority(PRIO_PROCESS, static_cast(syscall(SYS_gettid)), 10); - parallel_detail::WorkerPool::Instance().SetNice(10); + // Only where it can be raised back afterwards: the pool outlives the collection, and left at nice 10 + // it would slow every analysis the broker runs after it. + const bool pool_niced = CanRaiseNiceToZero(); + if (pool_niced) + parallel_detail::WorkerPool::Instance().SetNice(10); + else { + static std::once_flag logged; + std::call_once(logged, [this] { + logger.Warning("rugnux live: the ParallelFor pool is left at its priority - lowering it needs " + "the right to raise it back (CAP_SYS_NICE or RLIMIT_NICE)"); + }); + } try { const auto dataset = reader.GetDataset(); const RugnuxLiveSetup setup = MakeRugnuxLiveSetup(*dataset, rugnux_nthreads, model); @@ -255,7 +288,7 @@ void JFJochReceiverRugnux::RugnuxThread() { result = process.Run(&observer); } // The verdict and summary of the report a `rugnux` run writes, without writing it. - const ReportDocument doc = BuildReportDocument(scratch_dir + "/p", experiment.GetFilePrefix(), + const ReportDocument doc = BuildReportDocument(experiment.GetFilePrefix(), experiment.GetFilePrefix(), setup.experiment, result); std::vector warnings; for (const auto &w: doc.warnings) @@ -269,8 +302,8 @@ void JFJochReceiverRugnux::RugnuxThread() { logger.Error("rugnux live: {}", e.what()); live_status.Finish(RugnuxLiveState::Failed, e.what()); } - if (!parallel_detail::WorkerPool::Instance().SetNice(0)) - logger.Warning("rugnux live: the ParallelFor pool stays at nice 10 - raising it back needs CAP_SYS_NICE"); + if (pool_niced && !parallel_detail::WorkerPool::Instance().SetNice(0)) + logger.Warning("rugnux live: the ParallelFor pool could not be raised back to nice 0"); } void JFJochReceiverRugnux::ProbeThread() { @@ -288,7 +321,8 @@ void JFJochReceiverRugnux::ProbeThread() { const int64_t frames = std::lround(PROBE_DEG / std::abs(gonio->GetIncrement_deg())); if (2 * frames > static_cast(reader.GetNumberOfImages())) return; // a sweep this short is decided on soon enough by the run itself - RugnuxLiveSetup setup = MakeRugnuxLiveSetup(*dataset, rugnux_nthreads, nullptr); + // A quarter of the threads: it runs beside the main run, and each of its workers holds GPU memory too. + RugnuxLiveSetup setup = MakeRugnuxLiveSetup(*dataset, std::max(2, rugnux_nthreads / 4), nullptr); setup.config.end_image = static_cast(frames); setup.config.rotation_postrefine_geometry = false; setup.config.run_scaling = false; @@ -317,32 +351,48 @@ void JFJochReceiverRugnux::ProbeThread() { } void JFJochReceiverRugnux::AddToEndMessage(EndMessage &message) { - std::unique_lock ul(rugnux_m); - if (!rugnux_result) - return; - const EndMessage &r = rugnux_result->end_message; - message.indexing_rate = r.indexing_rate; - message.bkg_estimate = r.bkg_estimate; - message.spindle_blind_fraction = r.spindle_blind_fraction; - message.ice_ring_score_mean = r.ice_ring_score_mean; - message.az_int_result = r.az_int_result; - message.rotation_lattice = r.rotation_lattice; - message.rotation_lattice_type = r.rotation_lattice_type; - message.rotation_extra_lattices = r.rotation_extra_lattices; - message.unit_cell = r.unit_cell; - message.space_group_name = r.space_group_name; - message.space_group_number = r.space_group_number; - message.reindex_matrix = r.reindex_matrix; - message.refined_beam_center_x = r.refined_beam_center_x; - message.refined_beam_center_y = r.refined_beam_center_y; - message.refined_poni_rot1 = r.refined_poni_rot1; - message.refined_poni_rot2 = r.refined_poni_rot2; - message.refined_poni_rot3 = r.refined_poni_rot3; - message.refined_rotation_axis = r.refined_rotation_axis; - // Merged data, map coefficients and placed model, for the writer to write beside the master. Only - // here, with rugnux_result set: every file in scratch_dir is then whole - rugnux writes synchronously - // and a cut never stops a write half-way - while a run that threw may have left one half-written. - FillRugnuxResultBlocks(scratch_dir + "/p", message); + // A failure here costs the end message the analysis only: it goes out as the receiver made it. + try { + std::unique_lock ul(rugnux_m); + if (!rugnux_result) + return; + const EndMessage &r = rugnux_result->end_message; + message.indexing_rate = r.indexing_rate; + message.bkg_estimate = r.bkg_estimate; + message.spindle_blind_fraction = r.spindle_blind_fraction; + message.ice_ring_score_mean = r.ice_ring_score_mean; + message.az_int_result = r.az_int_result; + message.rotation_lattice = r.rotation_lattice; + message.rotation_lattice_type = r.rotation_lattice_type; + message.rotation_extra_lattices = r.rotation_extra_lattices; + message.unit_cell = r.unit_cell; + message.space_group_name = r.space_group_name; + message.space_group_number = r.space_group_number; + message.reindex_matrix = r.reindex_matrix; + message.refined_beam_center_x = r.refined_beam_center_x; + message.refined_beam_center_y = r.refined_beam_center_y; + message.refined_poni_rot1 = r.refined_poni_rot1; + message.refined_poni_rot2 = r.refined_poni_rot2; + message.refined_poni_rot3 = r.refined_poni_rot3; + message.refined_rotation_axis = r.refined_rotation_axis; + // Merged data, map coefficients and placed model, for the writer to write beside the master. Moved: + // the result is not read again. + if (!rugnux_result->outputs) + return; + RugnuxOutputs &o = *rugnux_result->outputs; + if (o.merged) + message.merged = std::move(o.merged->mtz); + if (o.maps) + message.map_coefficients = std::move(o.maps->coefficients); + if (o.placed_model) + message.placed_model = PlacedModelFromStructure(*o.placed_model->model, o.placed_model->cell, + o.placed_model->space_group.xhm()); + } catch (const std::exception &e) { + logger.Error("rugnux live: the end message goes without the analysis results - {}", e.what()); + message.merged.reset(); + message.map_coefficients.reset(); + message.placed_model.reset(); + } } void JFJochReceiverRugnux::Configure(const StartMessage &msg) { @@ -433,6 +483,9 @@ void JFJochReceiverRugnux::ForwardThread(uint32_t id) { } catch (const JFJochException &e) { logger.ErrorException(e); Cancel(e); + } catch (const std::exception &e) { + logger.Error("Cancelling data collection due to exception: {}", e.what()); + cancelled = true; } } } @@ -477,6 +530,12 @@ float JFJochReceiverRugnux::GetEfficiency() const { return static_cast(images_collected) / static_cast(experiment.GetFrameNum()); } +JFJochReceiverOutput JFJochReceiverRugnux::GetFinalStatistics() const { + JFJochReceiverOutput ret = JFJochReceiver::GetFinalStatistics(); + ret.analysis_warning = skipped_warning; + return ret; +} + float JFJochReceiverRugnux::GetProgress() const { if (experiment.GetFrameNum() == 0) return 0.0; diff --git a/receiver/JFJochReceiverRugnux.h b/receiver/JFJochReceiverRugnux.h index 22a3e9742..445b7ed13 100644 --- a/receiver/JFJochReceiverRugnux.h +++ b/receiver/JFJochReceiverRugnux.h @@ -31,8 +31,8 @@ class JFJochReceiverRugnux : public JFJochReceiver { const int rugnux_nthreads; const std::shared_ptr model; const double deadline_s; // after the last image, the analysis is cut (0 = never) - std::string scratch_dir; // rugnux's own output, read back into the end message RugnuxLiveStatus &live_status; + std::string skipped_warning; // why the collection was not analysed, for the broker's status std::vector> forward_futures; std::future measurement; @@ -50,6 +50,9 @@ class JFJochReceiverRugnux : public JFJochReceiver { std::atomic last_image_ns{0}; void MeasurementThread(); + // Opens the reader on the collection and starts the rugnux and probe threads; a failure is the + // analysis's alone (live status Failed), never the collection's. + void StartRugnux(std::future &rugnux_done, std::future &probe_done); void ForwardThread(uint32_t id); void RugnuxThread(); void ProbeThread(); @@ -82,6 +85,7 @@ public: float GetEfficiency() const override; float GetProgress() const override; + JFJochReceiverOutput GetFinalStatistics() const override; void SetSpotFindingSettings(const SpotFindingSettings &spot_finding_settings) override {} }; diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index d135973e2..dbcda7a17 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -861,6 +861,10 @@ public: virtual void OnPhase(const std::string &phase) {} virtual void OnProgress(uint64_t done, uint64_t total) {} virtual void OnImageProcessed(const DataMessage &msg) {} + // An image pass starts: OnImageProcessed then reports its images. `canonical` on the pass whose + // per-image results the run keeps - every image once, at the final geometry; a pre-pass or a probe pass + // reports some of the images again, or at another geometry. + virtual void OnImagePass(bool canonical) {} virtual void OnStep(const std::string &title) {} virtual void OnNote(const std::string &text) {} }; diff --git a/rugnux/RugnuxImagePass.cpp b/rugnux/RugnuxImagePass.cpp index 9c0e0ef87..641a2883f 100644 --- a/rugnux/RugnuxImagePass.cpp +++ b/rugnux/RugnuxImagePass.cpp @@ -354,8 +354,10 @@ void Rugnux::ImagePass(PipelineLocals &p) { bragg_overlapped_major += bragg.overlapped_major; }; - if (observer) + if (observer) { observer->OnPhase("Processing images"); + observer->OnImagePass(p.write_output && !geometry_prepass); + } Step(fmt::format("{}{}, {} images", !per_image_analysis ? "Azimuthal integration" : calibration ? "Image pass" diff --git a/rugnux/RugnuxLive.cpp b/rugnux/RugnuxLive.cpp index df46c56e6..c5902a9e0 100644 --- a/rugnux/RugnuxLive.cpp +++ b/rugnux/RugnuxLive.cpp @@ -36,6 +36,8 @@ RugnuxLiveSetup MakeRugnuxLiveSetup(const JFJochReaderDataset &dataset, int nthr ProcessConfig &c = s.config; c.mode = ProcessMode::FullAnalysis; c.nthreads = nthreads; + // As a bare `rugnux` with no -N: the per-image loop keeps to what a card can take (ImagePass). + c.nthreads_auto = true; c.export_unmerged = false; c.spot_finding = RugnuxDefaultSpotFinding(true); c.rotation_indexing = true; diff --git a/tests/RugnuxResultFilesTest.cpp b/tests/RugnuxResultFilesTest.cpp index 7b83db30c..7f0b99792 100644 --- a/tests/RugnuxResultFilesTest.cpp +++ b/tests/RugnuxResultFilesTest.cpp @@ -84,7 +84,7 @@ namespace { } } -TEST_CASE("RugnuxResultFiles: rugnux's files -> END blocks -> CBOR -> writer, byte for byte", "[rugnux_result_files]") { +TEST_CASE("RugnuxResultFiles: rugnux's outputs -> END blocks -> CBOR -> writer, byte for byte", "[rugnux_result_files]") { Logger logger("RugnuxResultFiles"); const std::filesystem::path dir = std::filesystem::temp_directory_path() / "rugnux_result_files_test"; std::filesystem::remove_all(dir); @@ -115,7 +115,12 @@ TEST_CASE("RugnuxResultFiles: rugnux's files -> END blocks -> CBOR -> writer, by OutputProvenance provenance; provenance.command_line = "rugnux -N 8 input_master.h5"; provenance.input_file = "/some/where/input_master.h5"; - WriteMtzReflections(reflections, TEST_CELL, experiment, provenance, original + ".mtz"); + // The files as rugnux writes them from its outputs (RugnuxOutputFiles.cpp), and the same outputs as the + // broker puts them in the END message (JFJochReceiverRugnux::AddToEndMessage). + EndMessage end{}; + end.max_image_number = 100; + end.merged = MergedMtzTable(reflections, TEST_CELL, experiment, provenance); + WriteMtz(*end.merged, original + ".mtz"); // Map coefficients as model validation lays them out (ModelMaps::coefficients). MtzTable maps; @@ -137,15 +142,10 @@ TEST_CASE("RugnuxResultFiles: rugnux's files -> END blocks -> CBOR -> writer, by maps.columns[c].values.push_back(row[c]); } WriteMtz(maps, original + "_maps.mtz"); + end.map_coefficients = maps; WritePlacedModel(TestModel(), TEST_CELL, experiment.GetSpaceGroupOrP1(), original, logger); - - EndMessage end{}; - end.max_image_number = 100; - FillRugnuxResultBlocks(original, end); - REQUIRE(end.merged); - REQUIRE(end.map_coefficients); - REQUIRE(end.placed_model); + end.placed_model = PlacedModelFromStructure(TestModel(), TEST_CELL, experiment.GetSpaceGroupOrP1().xhm()); REQUIRE(end.merged->columns.size() == 16); // with the anomalous columns CHECK(end.merged->columns[0].values.size() == reflections.size()); CHECK(end.placed_model->atom_name.size() == 2 * 3 * 6 + 2); @@ -242,3 +242,31 @@ TEST_CASE("RugnuxResultFiles: an MTZ table that describes no file is refused", " std::filesystem::remove(path); } + +TEST_CASE("RugnuxResultFiles: a placed model block that describes no model is refused", "[rugnux_result_files]") { + Logger logger("RugnuxResultFiles"); + const PlacedModelMessage good = PlacedModelFromStructure(TestModel(), TEST_CELL, "P 43 21 2"); + REQUIRE_NOTHROW(PlacedModelStructure(good)); + + PlacedModelMessage short_field = good; + short_field.b_iso.pop_back(); + CHECK_THROWS(PlacedModelStructure(short_field)); + + PlacedModelMessage bad_aniso = good; + bad_aniso.aniso.resize(5); + CHECK_THROWS(PlacedModelStructure(bad_aniso)); + + // The writer reports it and goes on: no file, no crash. + const std::filesystem::path dir = std::filesystem::temp_directory_path() / "rugnux_result_files_bad_model"; + std::filesystem::remove_all(dir); + std::filesystem::create_directories(dir); + const std::string prefix = (dir / "run").string(); + EndMessage end{}; + end.placed_model = short_field; + REQUIRE_NOTHROW(WriteRugnuxResultFiles(end, prefix, false, logger)); + end.placed_model = good; + end.placed_model->space_group_name = "not a group"; + REQUIRE_NOTHROW(WriteRugnuxResultFiles(end, prefix, false, logger)); + CHECK(!std::filesystem::exists(prefix + "_model.cif")); + std::filesystem::remove_all(dir); +} diff --git a/writer/RugnuxResultBlocks.cpp b/writer/RugnuxResultBlocks.cpp index e3b882a72..de9ada77a 100644 --- a/writer/RugnuxResultBlocks.cpp +++ b/writer/RugnuxResultBlocks.cpp @@ -3,56 +3,11 @@ #include "RugnuxResultBlocks.h" -#include -#include - -#include -#include - -namespace { - -UnitCell ToUnitCell(const gemmi::UnitCell &c) { - return UnitCell{static_cast(c.a), static_cast(c.b), static_cast(c.c), - static_cast(c.alpha), static_cast(c.beta), static_cast(c.gamma)}; -} - -std::string SpaceGroupName(const gemmi::Mtz &mtz) { - return mtz.spacegroup != nullptr ? mtz.spacegroup->xhm() : std::string(); -} - -} // namespace - -MtzTable MtzTableFromFile(const std::string &path) { - const gemmi::Mtz mtz = gemmi::read_mtz_file(path); - MtzTable t; - t.title = mtz.title; - t.unit_cell = ToUnitCell(mtz.cell); - t.space_group_name = SpaceGroupName(mtz); - t.history = mtz.history; - t.sort_order = mtz.sort_order; - // A dataset id is its position in the table. - const auto position = [&mtz, &path](int id) { - for (size_t i = 0; i < mtz.datasets.size(); i++) - if (mtz.datasets[i].id == id) - return static_cast(i); - throw std::runtime_error(path + ": no dataset " + std::to_string(id)); - }; - for (const auto &d : mtz.datasets) - t.datasets.push_back({d.project_name, d.crystal_name, d.dataset_name, static_cast(d.wavelength)}); - for (const auto &c : mtz.columns) - t.columns.push_back({c.label, c.type, position(c.dataset_id), {c.begin(), c.end()}}); - for (const auto &b : mtz.batches) { - MtzTable::Batch batch{b.number, b.title, {b.ints.begin(), b.ints.end()}, b.floats, b.axes}; - batch.ints[20] = position(b.dataset_id()); - t.batches.push_back(std::move(batch)); - } - return t; -} - -PlacedModelMessage PlacedModelFromStructure(const gemmi::Structure &st, const std::string &data_space_group_name) { +PlacedModelMessage PlacedModelFromStructure(const gemmi::Structure &st, const UnitCell &cell, + const std::string &data_space_group_name) { PlacedModelMessage p; p.name = st.name; - p.unit_cell = ToUnitCell(st.cell); + p.unit_cell = cell; p.space_group_name = data_space_group_name; p.model_space_group_name = st.spacegroup_hm; bool has_aniso = false; @@ -83,15 +38,3 @@ PlacedModelMessage PlacedModelFromStructure(const gemmi::Structure &st, const st p.aniso.clear(); return p; } - -void FillRugnuxResultBlocks(const std::string &prefix, EndMessage &end) { - if (std::filesystem::exists(prefix + ".mtz")) - end.merged = MtzTableFromFile(prefix + ".mtz"); - if (std::filesystem::exists(prefix + "_maps.mtz")) - end.map_coefficients = MtzTableFromFile(prefix + "_maps.mtz"); - if (std::filesystem::exists(prefix + "_model.cif")) { - const gemmi::Structure st = gemmi::read_structure_gz(prefix + "_model.cif"); - end.placed_model = PlacedModelFromStructure(st, end.merged ? end.merged->space_group_name - : st.spacegroup_hm); - } -} diff --git a/writer/RugnuxResultBlocks.h b/writer/RugnuxResultBlocks.h index 8441cbb55..5205c8144 100644 --- a/writer/RugnuxResultBlocks.h +++ b/writer/RugnuxResultBlocks.h @@ -9,13 +9,8 @@ #include "../common/JFJochMessages.h" -// The END-message blocks (common/JFJochMessages.h) read back from the files rugnux wrote at -// `prefix`: .mtz into `merged`, _maps.mtz into `map_coefficients` and -// _model.cif into `placed_model`, each only where its file exists. The writer makes the same -// files again from them (WriteRugnuxResultFiles, writer/WriteRugnuxFiles.h). -void FillRugnuxResultBlocks(const std::string &prefix, EndMessage &end); - -MtzTable MtzTableFromFile(const std::string &path); -// data_space_group_name: the group of the reflection files beside the model, which is what -// WritePlacedModel is to be given; the model's own group is taken from `st`. -PlacedModelMessage PlacedModelFromStructure(const gemmi::Structure &st, const std::string &data_space_group_name); +// The placed model of a rugnux run as the END message carries it (PlacedModelMessage). cell and +// data_space_group_name are the reflection files' beside it, which is what WritePlacedModel is to be given; +// the model's own group is taken from `st`. +PlacedModelMessage PlacedModelFromStructure(const gemmi::Structure &st, const UnitCell &cell, + const std::string &data_space_group_name);