The adopted space group travelled the pipeline as a bare int and was rebuilt downstream with find_spacegroup_by_number, which returns the reference setting. So every setting a number cannot name was destroyed one line after it was determined: P 1 1 2 came back as P 1 2 1, I 1 1 2 as C 1 2 1, R 3:R as R 3:H. DatasetSettings now holds the gemmi::SpaceGroup itself, DiffractionExperiment exposes it as GetGemmiSpaceGroup() / GetSpaceGroupOrP1(), and everything that used to take an int - HKLKeyGenerator (its int constructor is gone, so the compiler finds the callers), the merge, the R-free flags, French-Wilson, the reindexing ambiguity, the completeness enumeration, the MTZ and mmCIF exports, the model validation - takes the group. -S keeps the setting the symbol names rather than reducing it to a number. The end message carries both spellings and a reader prefers the name, since only the name keeps the setting while the number is what a reader written before the name understands. It carries them over CBOR too: the determined group was never serialised at all, so a group rugnux chose reached the master file only when the same process wrote it, and an online writer fell back to whatever the user had supplied at the start. Both keys are optional additions, so an older reader skips them and a newer one reads an older sender. On disk the master's /entry/sample/space_group carries the extended Hermann-Mauguin name and is what the reader takes the group from, so a setting survives a _process.h5 and the --mode scale that re-reads it; the number stays beside it and is the fallback for files written before. Every one of the 230 reference settings the old writer could produce reads back as itself, so older files are unaffected. Stage A and Stage B of the search still enumerate reference settings only, so this determines no group differently today - it is what the enumeration needs before it can be widened. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N
109 lines
6.4 KiB
C++
109 lines
6.4 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 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,
|
|
const gemmi::SpaceGroup *data_space_group = nullptr,
|
|
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` otherwise
|
|
// (a change of indexing is metric- and group-preserving). A no-op where the model needed neither.
|
|
const gemmi::SpaceGroup *AdoptModelFrame(const ModelValidationResult &validation,
|
|
std::vector<MergedReflection> &merged,
|
|
const gemmi::SpaceGroup &data_space_group,
|
|
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` 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,
|
|
const gemmi::SpaceGroup *space_group,
|
|
double d_min,
|
|
Logger &logger);
|