Files
Jungfraujoch/image_analysis/WriteReflections.h
T
leonarski_f d8c14046b2 rugnux: unit-cell e.s.d.s - ZERR in a new SHELX .ins beside the .hkl, _cell.*_esd in the mmCIF, CELL_ESD_* in the report
Rotation runs now estimate the standard uncertainties of the cell. Every strong integrated
observation (I > 3 sigma, unclipped, unsaturated, off ice rings) gives |q| = 1/d at its observed
centroid, which depends on the detector geometry only; q^2 = h^T G* h is linear in the reciprocal
metric, so the components of G* the point group leaves free (projected from the group's rotation
parts, any setting) are fitted by weighted linear least squares with 4-sigma rejection, and the
GoF-scaled covariance is carried analytically onto a, b, c, alpha, beta, gamma (ties give equal
e.s.d.s, fixed angles zero). Least-squares cell e.s.d.s are known to be too small (Herbstein 2000),
so the same fit is made on 8 blocks of consecutive frames and their scatter / sqrt(8) is the second
estimate; the written e.s.d. is the larger of the two per parameter. Stills get none.

Outputs: <prefix>.ins next to every .hkl (TITL, CELL at the wavelength, ZERR with Z = order of the
group, LATT/SYMM from the space group - positive LATT only with -1 at the origin - a placeholder
organic SFAC/UNIT at 18 A^3 per non-H atom, HKLF 4, END); the mmCIF gains _cell.*_esd ("." where
fixed) and _cell_measurement.reflns_used/theta_min/theta_max, and writes the cell to 4/3 decimals;
report keys CELL_ESD, CELL_ESD_LSQ, CELL_ESD_BLOCKS, CELL_ESD_BLOCK_COUNT, CELL_ESD_REFLECTIONS,
CELL_ESD_THETA_RANGE_DEG (+ dev CELL_ESD_OBSERVATIONS, CELL_DSPACING_FIT).

On 13 small-molecule sets the block scatter is 10-40x the least-squares e.s.d. and is what gets
written; it lands within ~0.3-5x of XDS's CORRECT.LP e.s.d.s. SHELXT solves from the generated
.ins and SHELXL carries ZERR into its CIF su. Costs 12 ms on a small molecule, 0.16 s on 690k
lysozyme observations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
(cherry picked from commit e74a3be76261fe1b6c0ff7b80c21afc4768c46cd)
2026-10-26 14:33:38 +01:00

133 lines
7.5 KiB
C++

// SPDX-FileCopyrightText: 2025 Paul Scherrer Institute
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <string>
#include <vector>
#include "../common/Reflection.h"
#include "../common/UnitCell.h"
#include "../common/DiffractionExperiment.h"
#include "../common/MtzTable.h"
#include "IntegrationOutcome.h"
struct MergeStatistics;
struct TwinningAnalysisResult;
struct CellEsd; // geom_refinement/CellEsd.h
// The error model as it is reported, already formatted. `isa` is the whole-range 1/sqrt(a*b), the
// same quantity XDS's ISa denotes, so a file written here is directly comparable with a CORRECT.LP;
// `isa_asymptotic` is the strong-reflection tier, which only the rotation path has. `a` and `b` are
// in XDS's convention, sigma^2 = a*(sigma0^2 + b*I^2). Empty strings are written as unknown.
struct ErrorModelReport {
std::string isa;
std::string isa_asymptotic;
std::string a;
std::string b;
};
// What the reflection files record about where they came from, beyond the experiment itself and the
// Rugnux version and commit: the MTZ HISTORY records and the mmCIF's _software and
// _jfjoch_provenance. Empty strings are left out.
struct OutputProvenance {
std::string command_line;
std::string input_file; // the input as found on disk, absolute
std::string input_identity; // a cheap identifier of it: size and modification time
std::string collection_date; // as the input states it (NXmx /entry/start_time), ISO-8601
};
// An e.s.d. below this is a parameter the symmetry fixes: written "." in the mmCIF.
constexpr double CELL_ESD_FIXED = 1e-9;
// The merged reflections, their statistics and the experiment, as one PDBx/mmCIF data block.
// nthreads: workers for the per-reflection row formatting, which is the bulk of the file.
// cell_esd: the cell's e.s.d.s (_cell.*_esd, _cell_measurement), where there are any.
void WriteMmcifReflections(const std::vector<MergedReflection> &reflections,
const UnitCell &unitCell,
const DiffractionExperiment &experiment,
const MergeStatistics &statistics,
const ErrorModelReport &error_model,
const TwinningAnalysisResult &twinning,
const OutputProvenance &provenance,
const std::string &filename,
size_t nthreads,
const CellEsd *cell_esd = nullptr);
// The merged MTZ: H K L IMEAN SIGIMEAN [I(+) SIGI(+) I(-) SIGI(-)] F SIGF [F(+) SIGF(+) F(-) SIGF(-)]
// FreeR_flag, one row per unique reflection in the CCP4 anomalous layout, the bracketed columns only where
// the merge kept a Bijvoet split; with the HISTORY records saying where it came from. WriteMtzReflections
// writes it (WriteMtz, writer/WriteRugnuxFiles.h).
MtzTable MergedMtzTable(const std::vector<MergedReflection> &reflections,
const UnitCell &unitCell,
const DiffractionExperiment &experiment,
const OutputProvenance &provenance);
void WriteMtzReflections(const std::vector<MergedReflection> &reflections,
const UnitCell &unitCell,
const DiffractionExperiment &experiment,
const OutputProvenance &provenance,
const std::string &filename);
// SHELX HKLF-4 text file, FORMAT(3I4,2F8.2) h k l I sigma(I), scaled so the largest value fits the
// field, ended by a 0 0 0 record. nthreads: workers for the row formatting, as for the mmCIF.
// Merged reflections, Bijvoet mates as separate records (the stills output).
void WriteShelxHklReflections(const std::vector<MergedReflection> &reflections,
const DiffractionExperiment &experiment,
const std::string &filename,
size_t nthreads);
// Unmerged scaled fulls, one record each at its measured index (the rotation output).
void WriteShelxHklReflections(const std::vector<ScaledFull> &fulls,
const std::string &filename,
size_t nthreads);
// The SHELX instruction file to go with the .hkl: TITL, CELL (at the wavelength), ZERR where the
// e.s.d.s are known, LATT, SYMM, a placeholder SFAC/UNIT, HKLF 4, END - what SHELXT reads, and what
// SHELXL's CIF takes the cell and its e.s.d.s from.
void WriteShelxIns(const UnitCell &cell, const CellEsd *esd, const DiffractionExperiment &experiment,
const std::string &title, const std::string &filename);
// Unmerged observations in the column and batch-header layout POINTLESS writes: aimless, pointless,
// careless and iotbx.merging_statistics all read that layout. H K L are the ASU indices and M/ISYM
// recovers the index the reflection was measured at (which is what careless needs to see the crystal
// frame) and says whether the observation is a partial.
// sum_partials: add the partials of each rocking event into one full, written at the batch of the
// event's centroid with the summed rocking-curve fraction in FRACTIONCALC - the plain sum every
// rotation program writes, over the events the combine would assemble (--min-partiality). False
// writes one row per integrated box instead, flagged as partials for the reading program to sum.
// Stills have no rocking events and are unaffected either way.
// The intensities carry the deterministic per-reflection corrections and nothing else - Lorentz
// and polarization in LP, the sensor's angle-dependent efficiency in QE, and the attenuation of the
// flight-path medium in FLIGHT - so raw counts are I / LP * QE * FLIGHT. The partiality and the
// per-image scale are left for the reading program to fit, since every program this file is for
// fits a scale model of its own.
void WriteUnmergedMtzReflections(const std::vector<IntegrationOutcome> &outcomes,
const UnitCell &unitCell,
const DiffractionExperiment &experiment,
bool sum_partials,
const OutputProvenance &provenance,
const std::string &filename,
size_t nthreads = 1);
// The same file as its table, in two steps, for a caller that builds it while the outcomes' per-image
// scale fields are still being written: UnmergedMtzTable reads everything but mosaicity_deg, which
// SetUnmergedMtzMosaicity then copies into the batch headers.
MtzTable UnmergedMtzTable(const std::vector<IntegrationOutcome> &outcomes,
const UnitCell &unitCell,
const DiffractionExperiment &experiment,
bool sum_partials,
const OutputProvenance &provenance,
size_t nthreads = 1);
void SetUnmergedMtzMosaicity(MtzTable &mtz, const std::vector<IntegrationOutcome> &outcomes);
void WriteReflections(const std::vector<MergedReflection> &reflections,
const UnitCell &unitCell,
const DiffractionExperiment &experiment,
const MergeStatistics &statistics,
const ErrorModelReport &error_model,
const TwinningAnalysisResult &twinning,
const OutputProvenance &provenance,
const std::string &filename,
size_t nthreads,
const std::vector<ScaledFull> *scaled_fulls = nullptr);