Files
Jungfraujoch/common/DetectorModules.cpp
leonarski_fandClaude Opus 5.5 ebce906025 Serial XFEL stills inputs: readers from the files themselves, module geometry and its refinement
Combines the stills reader and module work (stills-readers, stills-modgeom, stills-euxfel) onto
rc178 with the owner's decisions for this release:

- Inputs are files, never a directory; several files of one format are one dataset (the multi-input
  CLI of the previous commit), so every reader takes an explicit list: JFJochXFELReader::ReadFiles,
  ReadCXI, ReadPanelTable and JFJochXTCReader::ReadFiles take std::vector<std::string>; ListFiles and
  xtc::CollectStreams are gone, CanRead* accept a regular file only.
- One HDF5 frame reader (JFJochCrystFELReader -> JFJochXFELReader) parameterised by what the file holds:
  per-shot groups or a 3D stack, a /metadata panel table (posx/posy/angle, pixel size, nominal camera
  length, pixel mask: as SACLA writes), a Cheetah CXI single panel (square frame; a tiled slab is
  refused with a message). At most 8 files open; every file checked against the first. Signed 32-bit
  decode, per-frame 3D masks, panels at their own coffset (from the EuXFEL work).
- Frames stored one chunk each are read the way the NXmx reader reads its images: only the chunk's
  place is asked of HDF5 under its lock (H5Dget_chunk_info_by_coord), the bytes are read through
  HDF5ImageSource::RawFile (positional, no lock) and decoded outside it - deflate(+shuffle) by zlib,
  bitshuffle/LZ4 (and zstd, plain LZ4) by CompressedImage's decoders. Bitshuffle/LZ4 used to go through
  HDF5's filter under the global lock, one frame at a time.
- LCLS-I XTC (Rayonix): wavelength from the beam record, pixel size from the Rayonix model and
  binning, only --detector-distance needed; damaged/compressed frames skipped, a truncated last event
  dropped; std::ifstream, no POSIX calls: builds with MSVC. No DIALS .expt support at all.
- CrystFEL .geom: module geometry only (panel ranges, fs/ss, corners, pixel size, coffset, data path)
  and optionally the camera length; everything else is ignored with a note in the log.
- --mask-good / --mask-bad <file>:<dataset>: a mask array in another HDF5 file, in the frames' stored
  layout (what a .geom's mask_file used to give).
- Gain measured on the background for panel-table and CXI input that states none (--gain overrides).
- Detector modules from explicit geometry only (DetectorModules: the reader's panels, an NXmx file with
  one NXdetector_module per module, or --module-geometry); never from the image. Spots and predictions
  are mapped through the modules, the image is not re-assembled.
- Module refinement in the stills bundle adjustment (Ceres; per-module shift and in-plane turn, gauge
  on mean shift/turn/scale and on the two tilt-like patterns when the tilt is refined; adopted only
  where it predicts held-out frames better). Applied for a foreign multi-panel detector (panel table,
  .geom panels); for a JUNGFRAU measured and reported only. Written to <prefix>_modules.h5 (NXmx
  NXdetector_module groups), read back with --module-geometry.
- A panel-table file's camera length is nominal: with a reference cell it is refined from the cell.
- Broker and writer output unchanged: masters keep the single NXdetector_module group, the start
  message carries no module list.

Tests: [CrystFELGeometry], JFJochXFELReader_*, JFJochXTCReader, [JFJochMultiReader], [CellPopulation],
SaturationCeiling_*, [DetectorModules], RefineGlobalGeometry_ModulePlacement, [reindex], Sublattice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
2026-10-20 23:30:48 +02:00

155 lines
5.5 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "DetectorModules.h"
#include "DetectorGeometry.h"
#include "Definitions.h"
#include <algorithm>
#include <cmath>
#include <tuple>
DetectorModule DetectorModule::AsImaged(int64_t x0, int64_t y0, int64_t width, int64_t height) {
DetectorModule m;
m.x0 = x0;
m.y0 = y0;
m.width = width;
m.height = height;
m.ox = static_cast<double>(x0);
m.oy = static_cast<double>(y0);
return m;
}
bool DetectorModule::ContainsImagePixel(double x, double y) const {
// A pixel's centre is at its index, so the module covers [x0 - 0.5, x0 + width - 0.5).
return x >= static_cast<double>(x0) - 0.5 && x < static_cast<double>(x0 + width) - 0.5
&& y >= static_cast<double>(y0) - 0.5 && y < static_cast<double>(y0 + height) - 0.5;
}
std::pair<double, double> DetectorModule::ImageToPlane(double x, double y) const {
const double u = x - static_cast<double>(x0), v = y - static_cast<double>(y0);
return {ox + u * fx + v * sx, oy + u * fy + v * sy};
}
std::pair<double, double> DetectorModule::PlaneToImage(double px, double py) const {
// Inverse of the 2x2 [f s]
const double det = fx * sy - sx * fy;
const double dx = px - ox, dy = py - oy;
const double u = (sy * dx - sx * dy) / det;
const double v = (-fy * dx + fx * dy) / det;
return {static_cast<double>(x0) + u, static_cast<double>(y0) + v};
}
std::pair<double, double> DetectorModule::PlaneCentre() const {
return ImageToPlane(static_cast<double>(x0) + 0.5 * static_cast<double>(width - 1),
static_cast<double>(y0) + 0.5 * static_cast<double>(height - 1));
}
void DetectorModule::Apply(const ModuleShift &shift) {
const double c = std::cos(shift.rot_rad), s = std::sin(shift.rot_rad);
const double ux = ox - shift.x0, uy = oy - shift.y0;
ox = shift.x0 + c * ux - s * uy + shift.dx;
oy = shift.y0 + s * ux + c * uy + shift.dy;
const double nfx = c * fx - s * fy, nfy = s * fx + c * fy;
const double nsx = c * sx - s * sy, nsy = s * sx + c * sy;
fx = nfx; fy = nfy;
sx = nsx; sy = nsy;
}
DetectorModules::DetectorModules(std::vector<DetectorModule> in) : modules(std::move(in)) {}
bool DetectorModules::empty() const {
return modules.empty();
}
size_t DetectorModules::size() const {
return modules.size();
}
const DetectorModule &DetectorModules::operator[](size_t i) const {
return modules.at(i);
}
const std::vector<DetectorModule> &DetectorModules::Get() const {
return modules;
}
bool DetectorModules::Moved() const {
for (const auto &m: modules)
if (m.ox != static_cast<double>(m.x0) || m.oy != static_cast<double>(m.y0)
|| m.fx != 1.0 || m.fy != 0.0 || m.sx != 0.0 || m.sy != 1.0)
return true;
return false;
}
int64_t DetectorModules::ModuleOfImagePixel(double x, double y) const {
for (size_t i = 0; i < modules.size(); i++)
if (modules[i].ContainsImagePixel(x, y))
return static_cast<int64_t>(i);
return -1;
}
int64_t DetectorModules::ModuleOfPlanePoint(double px, double py) const {
for (size_t i = 0; i < modules.size(); i++) {
const auto [x, y] = modules[i].PlaneToImage(px, py);
if (modules[i].ContainsImagePixel(x, y))
return static_cast<int64_t>(i);
}
return -1;
}
std::pair<double, double> DetectorModules::ImageToPlane(double x, double y) const {
const int64_t m = ModuleOfImagePixel(x, y);
if (m < 0)
return {x, y};
return modules[m].ImageToPlane(x, y);
}
std::pair<double, double> DetectorModules::PlaneToImage(double px, double py) const {
const int64_t m = ModuleOfPlanePoint(px, py);
if (m < 0)
return {px, py};
return modules[m].PlaneToImage(px, py);
}
void DetectorModules::Apply(const std::vector<ModuleShift> &shifts) {
for (auto &m: modules) {
const auto [cx, cy] = m.PlaneCentre();
const double reach = 0.5 * static_cast<double>(std::min(m.width, m.height));
const ModuleShift *best = nullptr;
for (const auto &s: shifts)
if (std::hypot(s.x0 - cx, s.y0 - cy) < reach
&& (!best || std::hypot(s.x0 - cx, s.y0 - cy) < std::hypot(best->x0 - cx, best->y0 - cy)))
best = &s;
if (best)
m.Apply(*best);
}
}
std::vector<ModuleShift> DetectorModules::Centres() const {
std::vector<ModuleShift> out;
for (const auto &m: modules) {
ModuleShift s;
std::tie(s.x0, s.y0) = m.PlaneCentre();
out.push_back(s);
}
return out;
}
DetectorModules ModulesAsImaged(const DetectorGeometry &geom) {
if (geom.GetModulesNum() < 2)
return {};
std::vector<DetectorModule> out;
for (int64_t m = 0; m < geom.GetModulesNum(); m++) {
const Coord f = geom.GetFastDirection(m), s = geom.GetSlowDirection(m);
const double cols = CONVERTED_MODULE_COLS - 1, lines = CONVERTED_MODULE_LINES - 1;
const double xa = static_cast<double>(geom.GetX0(m)), ya = static_cast<double>(geom.GetY0(m));
const double xb = xa + cols * f.x + lines * s.x, yb = ya + cols * f.y + lines * s.y;
const auto x0 = static_cast<int64_t>(std::lround(std::min(xa, xb)));
const auto y0 = static_cast<int64_t>(std::lround(std::min(ya, yb)));
out.push_back(DetectorModule::AsImaged(x0, y0, std::lround(std::abs(xb - xa)) + 1,
std::lround(std::abs(yb - ya)) + 1));
}
return DetectorModules(out);
}