Files
Jungfraujoch/image_analysis/rotation_indexer/RotationIndexer.cpp
T
leonarski_fandClaude Opus 4.8 c86beeb393 rotation indexer: fix sub-range crash, explicit angle, settings + guards
Running rotation indexing on a sub-range (e.g. images 60-120) segfaulted: the
first pass passed the *global* image number to RotationIndexer::ProcessImage,
which indexes v_ (sized to the run's image count) -> out-of-bounds write.

- ProcessImage now takes an explicit mid-exposure angle (optional; falls back to
  the goniometer at the image index), so the indexer no longer assumes its slot
  index equals the goniometer image index. IndexAndRefine supplies it via
  RotationAngle(), matching the angle used for prediction. Added a bounds guard in
  ProcessImage so a bad index can never corrupt memory.
- JFJochProcess feeds the rotation indexer the local ordinal (not the global
  index), and shifts the goniometer (start += start_image*incr, incr *= stride,
  per-image wedge preserved) so local index i maps to the angle of original image
  start+i*stride - fixing rotation angles for the whole sub-range pipeline
  (prediction, refinement, output), not just the indexer.
- Expose "Rotation images" (number used for the first pass) in the job dialog,
  enabled when rotation indexing is on. (Count > available is already clamped by
  select_equally_spaced_image_ordinals.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 15:59:21 +02:00

195 lines
7.4 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "../../common/JFJochMath.h"
#include "RotationIndexer.h"
#include "../geom_refinement/XtalOptimizer.h"
#include "../indexing/FFTIndexer.h"
#include "../lattice_search/LatticeSearch.h"
#include "../indexing/MultiLatticeSearch.h"
RotationIndexer::RotationIndexer(const DiffractionExperiment &x, IndexerThreadPool &indexer)
: experiment(x),
index_ice_rings(x.GetIndexingSettings().GetIndexIceRings()),
v_(experiment.GetImageNum()),
angle_deg_(experiment.GetImageNum()),
axis_(x.GetGoniometer()),
geom_(x.GetDiffractionGeometry()),
updated_geom_(geom_),
indexer_(indexer) {
}
void RotationIndexer::RunIndexing() {
std::unique_lock ul(m);
if (!axis_)
return;
std::vector<Coord> coords;
coords.reserve(max_spots_per_image * v_.size());
for (int i = 0; i < v_.size(); i++) {
const float angle_deg = angle_deg_[i].value_or(axis_->GetAngle_deg(i) + axis_->GetWedge_deg() / 2.0f);
const auto rot = axis_->GetTransformationAngle(angle_deg);
for (const auto &s: v_[i])
coords.emplace_back(rot * s.ReciprocalCoord(geom_));
}
const auto indexer_result = indexer_.Run(experiment, coords);
if (!indexer_result.lattice.empty() && indexer_result.lattice[0].CalcVolume() > 1.0) {
auto sg = experiment.GetGemmiSpaceGroup();
if (sg) {
search_result_ = LatticeSearchResult{
.niggli_class = 0, // Since Niggli class was not searched for, we don't know which one
.conventional = indexer_result.lattice[0], // If lattice provided, it is for now primitive == conventional
.system = sg->crystal_system(),
.centering = sg->centring_type(),
.reindex = gemmi::Mat33(1, 0, 0, 0, 1, 0, 0, 0, 1),
};
} else {
// Find lattice type based on cell
search_result_ = LatticeSearch(indexer_result.lattice[0]);
}
// Run refinement
DiffractionExperiment experiment_copy(experiment);
XtalOptimizerData data{
.geom = experiment_copy.GetDiffractionGeometry(),
.latt = search_result_.conventional,
.crystal_system = search_result_.system,
.min_spots = experiment.GetIndexingSettings().GetViableCellMinSpots(),
.refine_beam_center = true,
.refine_distance_mm = false,
.refine_detector_angles = true,
.refine_rotation_axis = true,
.index_ice_rings = experiment.GetIndexingSettings().GetIndexIceRings(),
.axis = axis_
};
if (data.crystal_system == gemmi::CrystalSystem::Trigonal)
data.crystal_system = gemmi::CrystalSystem::Hexagonal;
if (data.crystal_system == gemmi::CrystalSystem::Monoclinic)
data.latt.ReorderMonoclinic();
if (XtalOptimizer(data, v_)) {
indexed_lattice = data.latt;
updated_geom_ = data.geom;
axis_ = data.axis;
}
if (indexer_result.lattice.size() > 1) {
auto ml_latt = MultiLatticeSearch(indexer_result.lattice);
for (auto &l : ml_latt) {
if (extra_lattices_.size() >= experiment.GetIndexingSettings().GetMaxExtraLattices())
break;
// Ignore lattices oriented by less than 3.0 degree
if (l.rotation_vector.Length() < 3.0 * PI / 180.0)
continue;
RotMatrix rot(l.rotation_vector.Length(), l.rotation_vector.Normalize());
XtalOptimizerData data_multi{
.geom = experiment_copy.GetDiffractionGeometry(),
.latt = data.latt.Multiply(rot),
.crystal_system = search_result_.system,
.min_spots = experiment.GetIndexingSettings().GetViableCellMinSpots(),
.refine_beam_center = false,
.refine_distance_mm = false,
.refine_detector_angles = false,
.refine_unit_cell = false,
.refine_rotation_axis = false,
.index_ice_rings = experiment.GetIndexingSettings().GetIndexIceRings(),
.axis = axis_
};
// Quick refinement: orientation only. Cell size/angles, beam center,
// detector angles and rotation axis are all kept from the first lattice.
// XtalOptimizer always refines orientation; everything else is frozen above.
XtalOptimizer(data_multi, v_);
extra_lattices_.push_back(data_multi.latt);
}
}
}
}
void RotationIndexer::ProcessImage(int64_t image, const std::vector<SpotToSave> &spots,
std::optional<float> angle_deg) {
std::unique_lock ul(m);
// For non-rotation just ignore the whole procedure
if (!axis_)
return;
// Guard: `image` is a slot in [0, image count); a bad index (e.g. a global number for a subset
// run) must not corrupt memory.
if (image < 0 || image >= static_cast<int64_t>(v_.size()))
return;
if (accumulated_spots >= max_spots)
return;
if (indexed_lattice)
return;
angle_deg_[image] = angle_deg;
v_[image].reserve(spots.size());
for (const auto &s: spots) {
if (index_ice_rings || !s.ice_ring)
v_[image].emplace_back(s);
}
// truncate spots, so we don't get above max_spots (total) and max_spots_per_image (for this image)
size_t max_spots_limit = std::min(max_spots_per_image, max_spots - accumulated_spots);
if (v_[image].size() > max_spots_limit) {
std::ranges::nth_element(v_[image], v_[image].begin() + max_spots_limit,
[](const SpotToSave &a, const SpotToSave &b) {
return a.intensity > b.intensity;
}
);
v_[image].resize(max_spots_limit);
}
accumulated_spots += v_[image].size();
}
std::optional<RotationIndexerResult> RotationIndexer::GetLattice() const {
std::unique_lock ul(m);
if (!indexed_lattice)
return {};
return RotationIndexerResult{
.lattice = indexed_lattice.value(),
.extra_lattices = extra_lattices_,
.search_result = search_result_,
.geom = updated_geom_,
.axis = axis_,
};
}
void RotationIndexer::ForceLattice(const CrystalLattice &lattice) {
indexed_lattice = lattice;
auto sg_num = experiment.GetSpaceGroupNumber().value_or(1);
auto sg = gemmi::find_spacegroup_by_number(sg_num);
if (sg != nullptr) {
search_result_ = LatticeSearchResult{
.niggli_class = 0, // Since Niggli class was not searched for, we don't know which one
.conventional = lattice, // If lattice provided, it is for now primitive == conventional
.system = sg->crystal_system(),
.centering = sg->centring_type(),
};
} else
search_result_ = LatticeSearchResult{
.niggli_class = 0, // Since Niggli class was not searched for, we don't know which one
.conventional = lattice, // If lattice provided, it is for now primitive == conventional
.system = gemmi::CrystalSystem::Triclinic,
.centering = 'P',
};
}