Four changes to the same least-squares fit, which the rotation first pass runs on every candidate lattice and the per-image path runs on every frame. The linear solver was DENSE_QR on a problem that is very tall and thin - thousands of spots against at most seventeen parameters. That is the shape QR handles worst: it copies the Jacobian out of Ceres' row-major storage into a column-major buffer on every solve, and Eigen's blocked Householder then degenerates to the unblocked path because its block size is the column count. Accumulating J^T J reads the Jacobian once instead. Both solve the same damped system, so the step is the same to round-off. Ceres sizes its dual numbers from the declared parameter blocks, not from which of them the caller then holds constant. Nothing outside a test set refine_distance_mm - the positional residual leaves the distance degenerate with the cell scale, which is why the rotation post-refinement fits it in a step of its own with the cell held fixed - so the block was declared only to be frozen, and every residual differentiated seventeen parameters to use sixteen. It is gone, along with the test that exercised distance recovery; that test seeded the distance off truth, which the cell would now absorb, so its seed moves to the true value. The post-refinement's own detector step held five of its seven blocks constant and now bakes them into the residual, leaving beam and distance. The predicted reciprocal vector was built by rotating all three direct columns and then crossing them. A rotation commutes with the cross product and leaves the triple product alone, so the same vector comes out of crossing the unrotated columns and turning the result once - three rotations become one, for every crystal system. The documentation described the arrangement before all this, and had drifted in a second way: the first-pass rotation indexing has been refining the detector tilt and the rotation axis by default, which the text said were held fixed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
63 lines
2.5 KiB
C++
63 lines
2.5 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 "../common/GoniometerAxis.h"
|
|
#include "../common/CrystalLattice.h"
|
|
#include "../common/DiffractionGeometry.h"
|
|
#include "../common/SpotToSave.h"
|
|
#include "gemmi/symmetry.hpp"
|
|
|
|
struct XtalOptimizerData {
|
|
DiffractionGeometry geom;
|
|
CrystalLattice latt;
|
|
gemmi::CrystalSystem crystal_system = gemmi::CrystalSystem::Triclinic;
|
|
int64_t min_spots = 8;
|
|
|
|
float min_length_A = 5.0;
|
|
float max_length_A = 500.0;
|
|
float min_angle_deg = 60.0f;
|
|
float max_angle_deg = 120.0f;
|
|
|
|
bool refine_beam_center = true;
|
|
bool refine_detector_angles = false;
|
|
bool refine_unit_cell = true; // This refines unit cell size + angles - orientation is always refined
|
|
bool refine_rotation_axis = false;
|
|
|
|
bool index_ice_rings = true;
|
|
|
|
// Weight each spot by how strong it is for its resolution, so that low-confidence spots contribute
|
|
// without driving the fit (see SpotConfidenceWeights). Off by default: the indexers call this with a
|
|
// spot list they have already selected, it is the per-image refinement that gets the raw list.
|
|
bool weight_spots_by_confidence = false;
|
|
|
|
// Stopping rule. max_iterations > 0 bounds the solver by ITERATIONS, which is reproducible;
|
|
// otherwise it is bounded by max_time, wall-clock seconds, which is not - the same image refines
|
|
// to a different answer on a busier machine. Online acquisition needs the wall-clock bound because
|
|
// its budget is real; offline reprocessing wants the reproducible one.
|
|
float max_time = 1.0;
|
|
int max_iterations = 0;
|
|
|
|
std::optional<GoniometerAxis> axis;
|
|
|
|
// output
|
|
std::optional<double> beam_corr_x;
|
|
std::optional<double> beam_corr_y;
|
|
|
|
// For rotation only optimizer
|
|
std::optional<double> angle_corr;
|
|
std::optional<Coord> angle_axis;
|
|
};
|
|
|
|
// num_threads sets the Ceres solver thread count for the internal least-squares refine. It defaults
|
|
// to 1 because XtalOptimizer is usually called from many threads at once; raise it only when a caller
|
|
// runs a small number of refinements concurrently and wants each to use several cores.
|
|
bool XtalOptimizer(XtalOptimizerData &data, const std::vector<std::vector<SpotToSave>> &spots,
|
|
int num_threads = 1);
|
|
bool XtalOptimizerRotationOnly(XtalOptimizerData &data, const std::vector<SpotToSave> &spots, float tolerance);
|
|
|
|
|