Files
Jungfraujoch/rugnux/ModelValidation.h
T
leonarski_f 511be0c366
Build Packages / build:rpm (rocky8) (push) Successful in 24m0s
Build Packages / Unit tests (push) Skipped
Build Packages / build:windows:nocuda (push) Successful in 16m54s
Build Packages / build:windows:cuda (push) Successful in 19m25s
Build Packages / build:viewer-tgz:cpu (push) Successful in 14m44s
Build Packages / build:viewer-tgz:cuda (push) Successful in 16m3s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 13m15s
Build Packages / build:rugnux:windows (push) Successful in 10m45s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 9m34s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 19m7s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 18m9s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 24m48s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 18m13s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 24m51s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 22m58s
Build Packages / build:rpm (rocky9) (push) Successful in 21m23s
Build Packages / Generate python client (push) Successful in 1m2s
Build Packages / Build documentation (push) Successful in 1m23s
Build Packages / Create release (push) Skipped
Build Packages / XDS test (durin plugin) (push) Successful in 9m45s
Build Packages / XDS test (neggia plugin) (push) Successful in 10m19s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 11m10s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 22m15s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 17m37s
Build Packages / DIALS test (push) Successful in 17m16s
v1.0.0-rc.165 (#75)
* `rugnux --model` adopts the model's space group as a label where the data were merged in its enantiomorph, instead of reindexing the reflections - which swapped I(+) with I(-).
* `rugnux --model` warns, naming the atom, when the anomalous density at the model's atoms comes out inverted, which means the data and the model are in opposite hands.
* `rugnux --model` writes an anomalous difference map (`<prefix>_anom.ccp4`) when the merge kept the Bijvoet split, and names the ten model atoms it peaks highest on as `ANOMALOUS_SITE_01`..`_10`.
* `MEAN_ATOM_DENSITY_SIGMA` is read from the map by cubic rather than linear interpolation and comes out around a tenth higher; it is no longer comparable with the figure earlier versions printed.
* `rugnux --model` reads an mmCIF coordinate file as well as a PDB one, gzipped or not, taking the format from the file's content rather than its name.
* A model `rugnux --model` cannot use is reported as a `WARNING:` line in the results report instead of only in the log.
* The rugnux results report has a `10. MODEL VALIDATION` section when `--model` was given; `REPORT_VERSION` is 4, `WARNINGS` moves to section 11 and no existing key changed.
* The rugnux results report records how the run was invoked, what it cost and what it ran on: `COMMAND_LINE=`, `WALL_TIME=` and `GPU_COUNT=` / `GPU=`.
* rugnux says which GPUs it can see before it starts processing.
* `rugnux --export-unmerged` also writes `<prefix>_unmerged.mtz` on a `--no-merge` run, and is ignored on a run with no output prefix instead of writing a file called `_unmerged.mtz`.
* `/start` asks the writer whether the run can be written before the detector is armed, so a run whose master file already exists, or whose output directory cannot be created, is refused up front with the writer's own message. This needs the TCP image stream or the built-in HDF5 writer; the ZeroMQ stream is unchanged.
* A calibration that fails goes to `Error` carrying the reason instead of `Inactive`, so `/wait_till_done` and `/wait_until_running` report it; a cancelled calibration still goes to `Inactive`.
* `/wait_till_done` answers 500 with the message when a collection ended in an error. A cancelled collection and a collection that only triggered a warning still answer 200.
* A pending start failure is discarded by `/cancel` and `/deactivate`, as it already was by `/start` and `/initialize`.
* `/scan_result` no longer reports the previous run's images after a collection that failed to start, or after `/deactivate`.
* The TCP image stream protocol version is 4. `jfjoch_writer` and `jfjoch_broker` have to be of the same release, as before.

Reviewed-on: #75
2026-08-27 22:16:54 +02:00

109 lines
6.3 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <optional>
#include <string>
#include <vector>
#include "gemmi/symmetry.hpp"
#include "../common/Reflection.h" // MergedReflection
#include "../common/UnitCell.h"
class Logger;
// Result of validating merged intensities against an atomic model.
struct ModelValidationResult {
bool ok = false;
std::string model_path; // the file that was asked for, so the report can name it
// Why it did not run, when ok is false: the file could not be read, had no atoms, no cell or no
// usable space group. A caller that asked for a model must be able to say so rather than finish
// quietly with no R-free and no maps, which reads exactly like a run that was never asked.
std::string failure_reason;
double r_work = 0.0;
double r_free = 0.0;
double k_sol = 0.0;
double b_sol = 0.0;
double k_overall = 0.0;
int n_work = 0;
int n_free = 0;
double mean_atom_density_sigma = 0.0; // mean 2Fo-Fc value at atom centres, in sigma
std::string maps_prefix; // where the .ccp4 / _maps.mtz were written
// The anomalous difference map read at the model's own atoms: the strongest sites, highest
// first. Empty when the merge kept no Bijvoet split, and so had nothing to make the map from.
struct AnomalousSite {
std::string label; // atom, residue and chain, e.g. "SG CYS A7"
double sigma = 0.0; // height of the anomalous map at that atom, in map sigma
};
std::vector<AnomalousSite> anomalous_sites;
int anomalous_pairs = 0; // Bijvoet pairs the anomalous map was computed from
// Set when the anomalous map is inverted at the model's atoms - deeper troughs than peaks - which
// says the data and the model are in opposite hands. Named so the report can quote the evidence.
bool anomalous_hands_disagree = false;
std::string anomalous_deepest_site;
double anomalous_deepest_sigma = 0.0;
// The alternative-indexing operator picked by R-free, identity where none was needed.
// AdoptModelFrame below applies it to the reflections that are written out, so the file, the
// R-factors and the maps all describe one indexing.
gemmi::Op indexing_op = gemmi::Op::identity();
// Set when the data were merged in the enantiomorph of the model's space group and the model's
// group is adopted for the written reflections. This is a change of LABEL only: the two groups
// have the same rotation operations, so they index and split into Bijvoet hands identically, and
// the reflections themselves must not move. See the note in ValidateAgainstModel.
bool adopted_model_enantiomorph = false;
int model_space_group_number = 0;
};
// Given merged intensities and an atomic model (PDB or mmCIF, gzipped or not - the format is taken
// from the file's content), scale the model structure factors (with an
// optimized flat bulk-solvent contribution and an overall anisotropic B) to the observed
// amplitudes, then report R-work / R-free and write 2Fo-Fc and Fo-Fc electron-density maps
// (CCP4) plus an MTZ of map coefficients next to output_prefix.
//
// No refinement of the structure itself is done. The model is only re-fractionalized into the
// data unit cell (a cheap rigid cell adjustment) so a deposited model with a slightly different
// cell still lines up with the processed data. Returns ok=false (and logs) on any failure.
// data_space_group_number is the space group the data was merged in. If it is the enantiomorph of
// the model's space group (e.g. data P4(1)2(1)2, model P4(3)2(1)2 - indistinguishable from merged
// intensities), the model's group is adopted as a label, without touching the reflections.
//
// probe_indexing_ambiguity controls how a merohedral (alternative-indexing) ambiguity is resolved.
// Pass false when a reference (MTZ) already fixed the indexing - the data are then kept in that
// authoritative indexing. Pass true (model only, no reference) to resolve it here as a fallback, by
// fitting each candidate reindexing to the model and keeping the lowest R-free. Holohedral crystals
// (no twin laws) are unaffected either way.
ModelValidationResult ValidateAgainstModel(const std::vector<MergedReflection> &merged,
const UnitCell &cell,
const std::string &model_path,
const std::string &output_prefix,
Logger &logger,
std::optional<int> data_space_group_number = std::nullopt,
bool probe_indexing_ambiguity = true);
// Reindex `merged` into the frame ValidateAgainstModel reported, so the reflection files that are
// written describe the same indexing as the R-factors and the maps. Returns the space group they are
// now in - the model's, where its enantiomorph was adopted, and `data_space_group_number` otherwise
// (a change of indexing is metric- and group-preserving). A no-op where the model needed neither.
int AdoptModelFrame(const ModelValidationResult &validation,
std::vector<MergedReflection> &merged,
int data_space_group_number,
bool merge_friedel,
Logger &logger);
// Reference intensities computed from an atomic model: |F_model|^2, with a flat bulk-solvent
// contribution at the standard constants, for every reflection to `d_min`. This is the same
// information a reference MTZ carries, and it is what lets a model break the indexing ambiguity of a
// serial-stills run: there the ambiguity has to be broken per image, at integration time, long before
// there is a merge to fit a model to. `cell` and `space_group_number` override the model's own, where
// the run already knows them. Empty on any failure (which is logged).
std::vector<MergedReflection> ModelReferenceIntensities(const std::string &model_path,
const std::optional<UnitCell> &cell,
std::optional<int> space_group_number,
double d_min,
Logger &logger);