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)
85 lines
4.1 KiB
C++
85 lines
4.1 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <array>
|
|
#include <cstdint>
|
|
#include <optional>
|
|
#include <span>
|
|
#include <vector>
|
|
|
|
#include <Eigen/Dense>
|
|
|
|
#include "../../common/UnitCell.h"
|
|
#include "../IntegrationOutcome.h"
|
|
#include "gemmi/symmetry.hpp"
|
|
|
|
// The e.s.d.s of the unit cell, as SHELXL's ZERR and a CIF's _cell.*_esd ask for them.
|
|
//
|
|
// The cell is fitted to d-spacings: each strong integrated observation gives |q| = 1/d at its
|
|
// observed centroid, which depends on the detector geometry alone (not on the orientation, nor on the
|
|
// rotation angle), and q^2 = h^T G* h is LINEAR in the reciprocal metric tensor G*. So the fit is a
|
|
// plain linear least squares in the components of G* the point group leaves free, and its covariance,
|
|
// scaled by the goodness of fit, maps onto a, b, c, alpha, beta, gamma through the derivatives of the
|
|
// cell with respect to G*. A length the group ties to another gets the same e.s.d., an angle it fixes
|
|
// gets zero.
|
|
//
|
|
// Least-squares cell e.s.d.s are known to be too small (Herbstein (2000) Acta Cryst. B56, 547-557):
|
|
// they see only the scatter of the centroids, not what changes over the sweep. The same fit is
|
|
// therefore also made on blocks of consecutive frames, and the scatter of the block cells, over the
|
|
// square root of their number, is the second estimate. The e.s.d. written is the larger of the two.
|
|
|
|
// One observation: the Miller index in the setting of the cell, and |q| = 1/d at the observed
|
|
// centroid, in 1/A.
|
|
struct CellObservation {
|
|
int32_t h = 0, k = 0, l = 0;
|
|
float q = 0.0f;
|
|
int32_t frame = 0;
|
|
};
|
|
|
|
struct CellEsd {
|
|
std::array<double, 6> esd{}; // written: per parameter the larger of the two below
|
|
std::array<double, 6> esd_lsq{}; // least squares, scaled by the goodness of fit
|
|
std::optional<std::array<double, 6>> esd_blocks; // block scatter / sqrt(blocks)
|
|
int blocks = 0;
|
|
int64_t observations = 0; // observations in the fit, after rejection
|
|
int64_t reflections = 0; // distinct h k l among them (_cell_measurement.reflns_used)
|
|
double theta_min_deg = 0.0;
|
|
double theta_max_deg = 0.0;
|
|
UnitCell fitted{}; // the d-spacing fit's own cell, for comparison with the reported one
|
|
};
|
|
|
|
// Symmetric 3x3 matrices spanning the reciprocal metrics G* the point group of `sg` allows
|
|
// (G* = R G* R^T for every rotation part R): 6 for triclinic down to 1 for cubic. Orthonormal as
|
|
// 6-vectors (g11, g22, g33, g12, g13, g23).
|
|
std::vector<Eigen::Matrix3d> MetricBasis(const gemmi::SpaceGroup &sg);
|
|
|
|
// a, b, c (A) and alpha, beta, gamma (deg) of the reciprocal metric G*.
|
|
std::array<double, 6> CellFromReciprocalMetric(const Eigen::Matrix3d &gstar);
|
|
|
|
// The e.s.d.s of a, b, c, alpha, beta, gamma for G* = sum_k p_k basis_k with covariance cov(p).
|
|
std::array<double, 6> CellEsdFromCovariance(const std::vector<Eigen::Matrix3d> &basis, const Eigen::VectorXd &p,
|
|
const Eigen::MatrixXd &cov);
|
|
|
|
struct MetricFit {
|
|
bool ok = false;
|
|
Eigen::VectorXd p;
|
|
Eigen::MatrixXd cov; // scaled by the goodness of fit
|
|
std::vector<bool> used;
|
|
};
|
|
|
|
// Weighted linear least squares of q^2 = h^T G* h, |q| taken at one constant e.s.d. (weight 1/q^2 on
|
|
// q^2), with the observations further than 4 sigma off the fit rejected and the fit repeated.
|
|
MetricFit FitReciprocalMetric(std::span<const CellObservation> obs, const std::vector<Eigen::Matrix3d> &basis);
|
|
|
|
// Both estimates for observations in the setting of `sg`; blocks are cut by frame. Empty with too
|
|
// few observations to fit.
|
|
std::optional<CellEsd> EstimateCellEsd(std::span<const CellObservation> obs, const gemmi::SpaceGroup &sg,
|
|
double wavelength_A, int nblocks = 8);
|
|
|
|
// The observations of a rotation run's integrated frames: observed, unclipped, unsaturated, off the
|
|
// ice rings and I > 3 sigma.
|
|
std::vector<CellObservation> GatherCellObservations(const std::vector<IntegrationOutcome> &outcomes,
|
|
size_t nthreads);
|