Files
Jungfraujoch/common/DiffractionGeometry.h
T
leonarski_fandClaude Opus 5 fed077e683 geometry: hold the detector plane as axis vectors, and give the discrete part its own home
The detector plane was three PONI angles and nothing else, so the two things it
cannot express - an image mirrored in Y, and one mounted at a multiple of 90
degrees - had no home at all. They are now the DetectorOrientation carried by the
detector setup, composed with the PONI rotation into one orthogonal matrix whose
columns ARE the fast axis, the slow axis and the sample->PONI normal:

    lab = R(rot1, rot2, rot3) * Delta * ( (x-bx)*p , (y-by)*p , distance )

GetFastAxis/GetSlowAxis/GetNormalAxis read those columns and DetectorAxes() sets
the plane from them, decomposing back to the angles; PoniRotMatrix and
PoniAnglesFromMatrix are the conversion in both directions, exact on the canonical
branch (rot2 in [-pi/2, pi/2]) and with a stated convention at gimbal lock. The
angles stay stored rather than re-derived, so a geometry given as angles is
written back as the same angles, to the bit.

Delta is never inferred. In particular an arbitrary rot3 is NOT decomposed into a
quarter turn plus a residual: rot3 is a fitted quantity, and a least-squares step
must not be able to turn the stored image. It is set only where something states
it - the detector setup, --detector-mirror-y / --detector-quarter-turns, or the
value a file this system wrote records - and defaults to the identity, which makes
the whole change a no-op for every existing detector and every existing file.

It is a different setting from DetectorSetup::mirror_y, which flips the MODULE
LAYOUT while an image is assembled and so decides what the stored pixels are.
Merging the two would apply the mirror twice for every modular detector, or change
the pixel content of every file written; both are ruled out. The new one earns its
keep exactly where the old one is a no-op: a detector whose image arrives already
assembled has no layout to flip.

Both generators are signed permutations of the in-plane offset, so they preserve
the distance from the PONI. That is why almost nothing downstream changes:
everything needing an azimuth already goes through LabCoord, and everything that
does not needs only a radius. The two hand-written copies of the rotation -
XtalResidual and RingOptimizer - take the discrete part as four constants next to
cos_rot3/sin_rot3, since it acts in the detector frame where rot3 acts in the
laboratory and cannot be folded into it. RingOptimizer needs it despite being a
radial fit: it fits the tilt, and the discrete part changes which way the tilt
tips a ring.

Carried as two optional CBOR keys and two detectorSpecific datasets, both
back-compatible; the NXmx module axis vectors and the translation direction stop
being hardcoded and are computed from it, reproducing today's values exactly at
the identity. GetPoniRotMatrix is renamed GetDetectorMatrix, because it is no
longer only the PONI rotation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lc5JG6kJqZoCWaoZ43JGTW
2026-08-29 23:00:35 +02:00

92 lines
4.6 KiB
C++

// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "JFJochException.h"
#include "Coord.h"
#include "DetectorOrientation.h"
#include "GoniometerAxis.h"
// The two directions of the PONI convention, as pure functions, so the conversion can be exercised
// on its own. rot_matrix = Rz(-rot3) * Rx(-rot2) * Ry(+rot1) in the internal frame (x = column,
// y = row downward, z = beam); its columns are the lab directions of a +1 column step, a +1 row step
// and the sample->PONI vector.
RotMatrix PoniRotMatrix(float rot1, float rot2, float rot3);
// The inverse. rot2 comes back in [-pi/2, pi/2] and rot1, rot3 in (-pi, pi], which is the canonical
// branch: on it the round trip is the identity. At rot2 = +-pi/2 only rot1 +- rot3 is determined, and
// the convention is to put it all into rot1 and leave rot3 at zero.
void PoniAnglesFromMatrix(const RotMatrix &rot_matrix, float &rot1, float &rot2, float &rot3);
class DiffractionGeometry {
float beam_x_pxl = 0.0;
float beam_y_pxl = 0.0;
float det_distance_mm = 100.0;
float pixel_size_mm = 0.075;
float wavelength_A = 1.0;
float poni_rot_1 = 0.0f;
float poni_rot_2 = 0.0f;
float poni_rot_3 = 0.0f;
DetectorOrientation orientation;
// The full detector orientation: the PONI rotation composed with the discrete image orientation.
// Its columns are the fast, slow and normal axes. Orthogonal, but improper when the image is
// mirrored, so transpose() is still its inverse.
RotMatrix det_matrix;
std::optional<GoniometerAxis> axis;
void UpdateDetectorMatrix();
public:
DiffractionGeometry &BeamX_pxl(float input);
DiffractionGeometry &BeamY_pxl(float input);
DiffractionGeometry &DetectorDistance_mm(float input);
DiffractionGeometry &PixelSize_mm(float input);
DiffractionGeometry &Wavelength_A(float input);
DiffractionGeometry &PoniRot1_rad(float input);
DiffractionGeometry &PoniRot2_rad(float input);
DiffractionGeometry &PoniRot3_rad(float input);
DiffractionGeometry &Orientation(const DetectorOrientation &input);
// Sets the detector plane from its two axis vectors (unit, orthogonal). The discrete orientation
// is left as it is - it says how the image is stored, which two vectors cannot - and the PONI
// angles are re-derived so that the two views stay in step.
DiffractionGeometry &DetectorAxes(const Coord &fast, const Coord &slow);
DiffractionGeometry &Rotation(const std::optional<GoniometerAxis> &input);
[[nodiscard]] float GetBeamX_pxl() const;
[[nodiscard]] float GetBeamY_pxl() const;
[[nodiscard]] float GetDetectorDistance_mm() const;
[[nodiscard]] float GetPixelSize_mm() const;
[[nodiscard]] float GetWavelength_A() const;
[[nodiscard]] Coord GetScatteringVector() const;
[[nodiscard]] float GetPoniRot1_rad() const;
[[nodiscard]] float GetPoniRot2_rad() const;
[[nodiscard]] float GetPoniRot3_rad() const;
[[nodiscard]] DetectorOrientation GetOrientation() const;
[[nodiscard]] Coord GetFastAxis() const; // lab direction of a +1 column step
[[nodiscard]] Coord GetSlowAxis() const; // lab direction of a +1 row step
[[nodiscard]] Coord GetNormalAxis() const; // sample -> PONI direction
[[nodiscard]] std::pair<float, float> GetDirectBeam_pxl() const;
[[nodiscard]] std::optional<GoniometerAxis> GetRotation() const;
[[nodiscard]] Coord LabCoord(float x, float y) const;
[[nodiscard]] Coord DetectorToRecip(float x, float y) const;
[[nodiscard]] std::pair<float, float> RecipToDetector(const Coord &recip) const;
[[nodiscard]] float TwoTheta_rad(float x, float y) const;
[[nodiscard]] float Phi_rad(float x, float y) const;
[[nodiscard]] float PxlToRes(float x, float y) const;
[[nodiscard]] float PxlToQ(float x, float y) const;
[[nodiscard]] float PxlToRes(float dist_pxl) const;
[[nodiscard]] float ResToPxl(float d_A) const;
[[nodiscard]] Coord ResToPxl(float d_A, float phi) const;
[[nodiscard]] float DistFromEwaldSphere(const Coord& recip) const;
[[nodiscard]] float CalcAzIntSolidAngleCorr(float x, float y) const;
[[nodiscard]] float CalcAzIntPolarizationCorr(float x, float y, float coeff) const;
[[nodiscard]] std::pair<float, float> ResPhiToPxl(float d_A, float phi_rad) const;
[[nodiscard]] Coord ProjectToEwaldSphere(const Coord &p0) const;
// eq. 18 in https://journals.iucr.org/d/issues/2014/08/00/dz5332/index.html
[[nodiscard]] float AngleFromEwaldSphere_deg(const Coord &p0) const;
[[nodiscard]] const RotMatrix& GetDetectorMatrix() const;
};