The rc.160 sample-identity cleanup left protein names, internal dataset codes, and a measured cell angle in several source comments and one test. Most sensitive: a measured monoclinic beta tied to an internal code in Rugnux.cpp and IndexAndRefine.cpp. Rewrite each to describe the crystallographic situation only (space group / metric relation), keeping the technical reasoning intact. Comment- and string-only; no behaviour change. (rugnux_cli.cpp's remaining name is scrubbed in the following commit, alongside its other change to that file.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
152 lines
9.1 KiB
C++
152 lines
9.1 KiB
C++
// SPDX-FileCopyrightText: 2025 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 "../../common/Reflection.h"
|
|
#include "gemmi/symmetry.hpp"
|
|
|
|
// Determine the likely space group of a dataset from its P1-merged intensities, in the spirit
|
|
// of POINTLESS (Evans 2006):
|
|
//
|
|
// Stage A - point group (Laue) symmetry. Every candidate rotation operator is scored once by
|
|
// the correlation of I(h) with I(Rh). The chosen point group is the largest one all
|
|
// of whose operators are confirmed (high CC). A wrong operator scores ~0, so this is
|
|
// self-pruning - the unit cell is not needed.
|
|
//
|
|
// Stage B - space group within that point group. Each Sohncke space group of the point group
|
|
// predicts a set of systematically absent reflections (centering + screw axes). The
|
|
// chosen space group is the one that explains the MOST absences while every reflection
|
|
// it predicts absent is in fact weak. The symmorphic group (no absences) is the
|
|
// fallback when no screw/centering is supported. Enantiomorphic pairs (e.g. P4_1 vs
|
|
// P4_3) are indistinguishable from intensities and are reported as a pair.
|
|
|
|
struct SpaceGroupOperatorScore {
|
|
std::string op_triplet_hkl; // reciprocal-space triplet of the rotation, e.g. "-h,-k,l"
|
|
double cc = 0.0; // correlation of I(h) with I(Rh)
|
|
int n_pairs = 0; // independent reflection pairs the CC was computed from
|
|
bool present = false; // operator confirmed as a real symmetry of the intensities
|
|
};
|
|
|
|
struct SpaceGroupCandidateScore {
|
|
gemmi::SpaceGroup space_group;
|
|
int absent_observed = 0; // observed reflections this SG predicts systematically absent
|
|
int absent_violations = 0; // of those, how many are nonetheless strongly present
|
|
double absent_mean_i_over_sigma = 0.0;
|
|
double present_mean_i_over_sigma = 0.0;
|
|
bool consistent = false; // absent class confirmed weak (few violations)
|
|
bool selected = false; // chosen result (or its enantiomorph)
|
|
};
|
|
|
|
struct SearchSpaceGroupOptions {
|
|
// Lattice (metric) symmetry from LatticeSearch. When set, the point-group search is limited to
|
|
// the Sohncke subgroups of this system's holohedry - the metric is an upper bound on the
|
|
// intensity symmetry, so e.g. a tetragonal metric tests {1,2,222,4,422} and never 3/6/23. All
|
|
// subgroups are still tested (down to P1), so a pseudo-symmetric metric never forces a higher
|
|
// symmetry than the intensities support. Unset = search every Sohncke system.
|
|
std::optional<gemmi::CrystalSystem> lattice_system;
|
|
|
|
// Centering is NOT taken from the lattice metric: an indexer that returns the conventional cell
|
|
// (e.g. cubic 'P'-looking axes for a body-centered lattice) hides the centering, which lives only
|
|
// in the systematic absences. Stage B therefore tests every centering allowed by the point group
|
|
// and confirms it from the data (h+k+l etc. absent), rather than trusting a geometric hint.
|
|
|
|
// Friedel mates are treated as equivalent when matching HKLs (i.e. anomalous signal ignored).
|
|
bool merge_friedel = true;
|
|
|
|
// Ignore reflections beyond this resolution (smaller d = higher resolution). 0 disables.
|
|
double d_min_limit_A = 0.0;
|
|
|
|
// Drop reflections weaker than this from the correlation stage only (the absence stage must
|
|
// keep weak reflections - that is where the screw-axis signal lives).
|
|
double min_i_over_sigma = 0.0;
|
|
|
|
// Drop reflections STRONGER than this resolution-normalised E^2 = I/<I>(shell) from the correlation
|
|
// stage only. A second lattice deposits intensity on one reciprocal position but not its symmetry
|
|
// mate, so an overlap-contaminated reflection is a one-sided E^2 outlier that poisons an operator's
|
|
// I(h)/I(Rh) correlation (it flipped a pseudo-merohedral P2_1 case to P1 under -A: excluding the E^2>9 tail, ~0.4%
|
|
// of reflections, lifted the 2-fold CC 0.33->0.53 back over the gate). Clean Wilson-distributed data
|
|
// almost never reaches E^2=9 (P(E^2>9) ~ 0.01-0.3%), so this removes essentially nothing there and
|
|
// only trims the overlap tail. 0 disables. Absences are unaffected (they need the weak tail).
|
|
double max_e_squared_for_cc = 9.0;
|
|
|
|
// --- Stage A: point group ---
|
|
// A rotation is accepted as a real symmetry when its I(h)/I(Rh) correlation reaches this over
|
|
// at least min_pairs_per_operator independent pairs.
|
|
double min_operator_cc = 0.5;
|
|
int min_pairs_per_operator = 20;
|
|
|
|
// Per-operator CC alone cannot tell a real weak operator from a false strong one (a noisy crystal's
|
|
// genuine 2-fold can score below a pseudo-symmetric crystal's near-perfect false one). So a point
|
|
// group is also required to be SELF-CONSISTENT: merging the intensities under it must not inflate the
|
|
// reduced chi^2 (within-orbit scatter / sigma^2) beyond this factor times the most-consistent
|
|
// candidate. A false operator forces non-equivalent reflections together so they disagree by many
|
|
// sigma and chi^2 blows up; a real one leaves it ~flat even when the operator CC is only moderate.
|
|
// Calibrated on the rotation-test battery: every correct point group stays within ~1.7x the best
|
|
// subgroup even on weak / badly-integrated data (worst real case Thau P41212 at 1.71), while a twin
|
|
// law or pseudo-symmetry lands clearly higher (Ins_H_2's R3->R32 twin 2-fold at 2.01). 1.85 sits
|
|
// between the two, so a partial merohedral twin is kept in its true lower symmetry (R3), not
|
|
// over-promoted to the holohedral R32.
|
|
double max_merge_chi2_ratio = 1.85;
|
|
|
|
// A genuine high-symmetry merge can drift just past max_merge_chi2_ratio when its data are only
|
|
// imperfectly scaled: each real symmetry step then adds a little systematic scatter, so a weakly-
|
|
// scaled cubic like Ins_I_3 lands at ratio ~2.0 - right where a merohedral twin (Ins_H_2's R3->R32
|
|
// at ~1.95) also lands, so the chi^2 ratio alone cannot separate them. A candidate whose chi^2 is
|
|
// only this far past the best subgroup is therefore rescued if the SYSTEMATIC part of the extra
|
|
// scatter stayed small (max_systematic_b_ratio): merging under a genuine operator gains multiplicity
|
|
// without intensity-proportional disagreement, so the merge error model's b barely moves, whereas a
|
|
// twin forces non-equivalent reflections together and b balloons (Ins_I_3 b x1.6 vs Ins_H_2 x2.2,
|
|
// measured against the largest confirmed subgroup). Both bounds sit in the gap; the rescue only ever
|
|
// promotes, and only in this narrow chi^2 band.
|
|
double max_merge_chi2_rescue = 2.30;
|
|
double max_systematic_b_ratio = 1.90;
|
|
|
|
// --- Stage B: space group (screw axes / centering) ---
|
|
bool determine_space_group = true; // false: stop at the symmorphic representative
|
|
|
|
// Signed I/sigma above which a reflection counts as genuinely present. Signed, so negative
|
|
// noise is never mistaken for a real reflection. Used both to spot reflections that violate a
|
|
// wrongly-assumed absence, and to keep the correlation stage from pairing near-zero reflections.
|
|
double present_i_over_sigma = 3.0;
|
|
|
|
// Extra intensity gate for the systematic-absence test: a reflection also has to reach this
|
|
// resolution-normalised intensity E^2 = I / <I>(shell) to count as violating a predicted absence.
|
|
// The error model can under-estimate sigma on weak axial reflections and fake a high I/sigma, so a
|
|
// reflection at a few percent of the shell-mean intensity is judged absent regardless of its sigma
|
|
// (e.g. the monoclinic 2_1: 0k0-odd at ~1% of 0k0-even). 0 disables the gate (I/sigma only).
|
|
double present_e_squared = 0.3;
|
|
|
|
// A candidate's absence conditions are accepted when at most this fraction of the reflections
|
|
// it predicts absent are in fact strongly present.
|
|
double max_absent_violation_fraction = 0.10;
|
|
|
|
// Need at least this many observed reflections in the predicted-absent class before a
|
|
// screw/centering is claimed (guards against deciding from a handful of reflections).
|
|
int min_absent_observed = 8;
|
|
};
|
|
|
|
struct SearchSpaceGroupResult {
|
|
std::optional<gemmi::SpaceGroup> best_space_group;
|
|
// Other space groups that fit the data equally well (same systematic absences): enantiomorphic
|
|
// partners (P4_1 vs P4_3), origin-ambiguous pairs (I222 vs I2_12_12_1), or groups left
|
|
// undetermined by incomplete data. The data cannot choose between best_space_group and these.
|
|
std::vector<gemmi::SpaceGroup> alternatives;
|
|
|
|
std::string point_group_hm; // chosen point group, e.g. "422"
|
|
std::vector<SpaceGroupOperatorScore> operator_scores; // Stage A, all distinct operators tested
|
|
std::vector<SpaceGroupCandidateScore> candidates; // Stage B, ranked
|
|
};
|
|
|
|
SearchSpaceGroupResult SearchSpaceGroup(
|
|
const std::vector<MergedReflection>& merged,
|
|
const SearchSpaceGroupOptions& opt = {});
|
|
|
|
std::string SearchSpaceGroupResultToText(
|
|
const SearchSpaceGroupResult& result,
|
|
size_t max_candidates_to_print = 20);
|