Files
Jungfraujoch/common/DetectorOrientation.h
T
leonarski_fandClaude Opus 5 a27c4cf26f reader: take a miniCBF's mounting from the imgCIF axis table its header states
A miniCBF header states three things about how the instrument is put together
that the reader was assuming instead: which laboratory direction the image's
columns run along, which its rows run along, and which the spindle turns about.
Some beamlines append a CBF template block holding the full imgCIF axis table,
which says all three outright.

Two instruments in the corpus are not what was assumed, in two different ways.
One mounts its detector a quarter turn round, so the image's columns run
vertically. Another turns its spindle about the VERTICAL, with the image mounted
the usual way; its table says so, and its "# Oscillation_axis" line says so a
second way, by naming the image direction the spindle runs along rather than a
vector. Either error leaves the spindle 90 degrees from the image. That is not a
sign, so the run's axis-sign rescue cannot reach it, and no refinement recovers
it: all three affected sweeps indexed nothing usable.

So the table is read. The element axes give the image orientation, matched against
the eight discrete mountings exactly as the NXmx module directions already are -
the match itself moves to DetectorOrientation, so both readers share one
definition rather than two copies. The goniometer axis with no parent gives the
spindle DIRECTION; its sign stays the rescue's business, which is the part a
convention can legitimately differ on. The detector axis with no parent gives the
2theta arm, replacing the assumption that the arm shares the spindle's axis - the
one header stating both states them with the same vector, so this changes no
answer, only what it rests on. imgCIF's frame differs from the internal one by a
half turn about x, a rotation and not a mirror, as writer/HDF5NXmx.cpp already
records from the other side.

Where a header carries no table, a "+SLOW" on the Oscillation_axis line still
says the spindle runs along the image's slow direction. That is the only thing one
of the three affected sets says about it. The axis NAME on that line stays
unusable - the header that carries both says "X.CW" where its own table says Y -
but the direction token is not: where both are present they agree, which is what
makes reading it evidence rather than a guess.

Also: naming a frame with no directory at all now finds its sweep. parent_path()
of a bare filename is empty and iterating an empty path finds nothing, so running
from inside the data directory reported that no images were found.

Measured, with nothing on the command line. The vertical-spindle protein set goes
from no usable lattice to 100% indexed, P 6(3) 2 2 with a cell 0.43% from
deposited, 87846 reflections at 86.3% completeness and CC(1/2) 0.995. Its
companion from the same detector, which has no table and only the +SLOW token,
goes from a spurious monoclinic cell at 2.3% completeness and I/sigma 0.21 to the
right orthorhombic lattice, 97.7% indexed, 59.7% complete, CC(1/2) 0.996. The
quarter-turned set's three sweeps, at three arm positions, now all index without
the hand-passed quarter turn they needed and agree on one cell to 0.03 A. Six
miniCBF sets that state no table and no +SLOW - including one whose
Oscillation_axis line names an axis in a third dialect - are byte-identical in
.hkl, .mtz, .cif and the image statistics, as are two NXmx sets, which is the
shared orientation matcher moving nothing on that path either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3yNBXk4wKdMZy1ak2NY7f
2026-08-30 14:03:32 +02:00

51 lines
2.4 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <optional>
#include "Coord.h"
// How the stored image is laid out in the detector plane: mirrored in Y, and/or turned by a multiple
// of 90 degrees about the beam. Both are exact pixel remappings, which an arbitrary in-plane rotation
// (PONI rot3) is not - a viewer can show the image the right way up from these two without resampling
// anything.
//
// It is applied to the offset from the PONI, in stored-image millimetres, BEFORE the continuous PONI
// tilt takes that offset to the laboratory:
//
// lab = R(rot1, rot2, rot3) * Matrix() * ( (x-beam_x)*pixel, (y-beam_y)*pixel, distance )
//
// Mirror first, then the quarter turns. Every element of the group the two generate can be written
// that way, so the order is a convention rather than a derivation, and this is the one.
//
// This is NOT the same thing as DetectorSetup::mirror_y, which describes the raw readout -> assembled
// image module layout and is spent before the geometry sees anything. This one describes the assembled
// image -> detector canonical frame, changes no pixel, and defaults to the identity.
class DetectorOrientation {
bool mirror_y = false;
int64_t quarter_turns = 0; // 0..3, right-handed about the beam = clockwise on the displayed image
public:
DetectorOrientation() = default;
DetectorOrientation(bool mirror_y, int64_t quarter_turns);
DetectorOrientation& MirrorY(bool input);
DetectorOrientation& QuarterTurns(int64_t input);
[[nodiscard]] bool IsMirrorY() const;
[[nodiscard]] int64_t GetQuarterTurns() const;
[[nodiscard]] bool IsIdentity() const;
// Rz(quarter_turns * 90 deg) * diag(1,-1,1)^mirror_y. Entries are exactly 0 and +-1.
[[nodiscard]] RotMatrix Matrix() const;
// The orientation whose fast and slow axes are the two directions given, or nothing when they are
// not one of the eight. Nothing means the image is turned in its own plane by something that is
// not a multiple of 90 degrees, which is a continuous rotation of the detector and belongs in the
// PONI angles - it cannot be told apart from the tilt by looking at the two directions alone.
[[nodiscard]] static std::optional<DetectorOrientation> Match(const Coord &fast, const Coord &slow);
bool operator==(const DetectorOrientation &other) const;
};