Merge branch 'mv-ncs' into rc175: models expanded by their strict NCS operators; the written cell follows the model's indexing

Exact: p.hkl md5 and MTZ reflection data identical on 18 sets. 4nwv R-free 0.545 -> 0.297 and cell in the depositor's
frame; written cells corrected on 9rcs, 9b22, 9qw8, myob_x06da_powder_1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EizBKhTqrAYago9KJG3dA3
This commit is contained in:
2026-10-10 13:36:31 +02:00
co-authored by Claude Opus 5.5
10 changed files with 584 additions and 13 deletions
@@ -22,6 +22,7 @@
#include <unordered_map>
#include <gemmi/mmread_gz.hpp> // read_structure_gz
#include <gemmi/assembly.hpp> // expand_ncs
#include <gemmi/gz.hpp> // MaybeGzipped
#include <gemmi/it92.hpp> // IT92 x-ray form factors
#include <gemmi/dencalc.hpp> // DensityCalculator
@@ -213,6 +214,15 @@ void reorient_about_centroid(gemmi::Model &model, const gemmi::Mat33 &rot) {
SetModelPositions(model, pos);
}
// A deposition can hold only part of the asymmetric unit, with the strict-NCS operators that generate
// the rest (mmCIF _struct_ncs_oper code 'generate', PDB MTRIX without the given flag): an icosahedral
// virus deposits a few chains of the dozens in the crystal's ASU. Read as it is, the model is that
// fraction of the crystal, and every R and map made from it describes the fraction.
void expand_strict_ncs(gemmi::Structure &st) {
if (std::any_of(st.ncs.begin(), st.ncs.end(), [](const gemmi::NcsOp &op) { return !op.given; }))
gemmi::expand_ncs(st, gemmi::HowToNameCopiedChain::Short);
}
// Move a model into another cell by keeping its fractional coordinates. Correct only when the two
// cells describe the same axes in the same order, which is what the probe below is there to arrange.
void refractionalize_into(gemmi::Structure &st, const gemmi::UnitCell &target) {
@@ -437,6 +447,7 @@ ModelValidationResult Validate(const std::vector<MergedReflection> &merged,
// falls back to the content when it does not recognise one. A model arrives named however
// whoever produced it named it, so the file itself is the better authority.
st = gemmi::read_structure_gz(model_path, gemmi::CoorFormat::Detect);
expand_strict_ncs(st);
} catch (const std::exception &e) {
result.failure_reason = fmt::format("cannot read model {}: {}", model_path, e.what());
logger.Error("Model validation: {}", result.failure_reason);
@@ -1819,6 +1830,7 @@ std::vector<MergedReflection> ModelReferenceIntensities(const std::string &model
// falls back to the content when it does not recognise one. A model arrives named however
// whoever produced it named it, so the file itself is the better authority.
st = gemmi::read_structure_gz(model_path, gemmi::CoorFormat::Detect);
expand_strict_ncs(st);
} catch (const std::exception &e) {
logger.Error("Model reference: cannot read model {}: {}", model_path, e.what());
return out;