diff --git a/common/DetectorOrientation.cpp b/common/DetectorOrientation.cpp index 4e7095c4c..e11ec56e7 100644 --- a/common/DetectorOrientation.cpp +++ b/common/DetectorOrientation.cpp @@ -46,6 +46,18 @@ RotMatrix DetectorOrientation::Matrix() const { return {rz_x, mirror_y ? -rz_y : rz_y, {0, 0, 1}}; } +std::optional DetectorOrientation::Match(const Coord &fast, const Coord &slow) { + for (int64_t quarter_turns = 0; quarter_turns < 4; quarter_turns++) { + for (const bool mirror_y: {false, true}) { + const DetectorOrientation orientation(mirror_y, quarter_turns); + const RotMatrix m = orientation.Matrix(); + if (((m.Column(0) - fast).Length() < 1e-3f) && ((m.Column(1) - slow).Length() < 1e-3f)) + return orientation; + } + } + return {}; +} + bool DetectorOrientation::operator==(const DetectorOrientation &other) const { return (mirror_y == other.mirror_y) && (quarter_turns == other.quarter_turns); } diff --git a/common/DetectorOrientation.h b/common/DetectorOrientation.h index e9a48a2ce..d6a25d43a 100644 --- a/common/DetectorOrientation.h +++ b/common/DetectorOrientation.h @@ -3,6 +3,8 @@ #pragma once +#include + #include "Coord.h" // How the stored image is laid out in the detector plane: mirrored in Y, and/or turned by a multiple @@ -38,5 +40,11 @@ public: // 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 Match(const Coord &fast, const Coord &slow); + bool operator==(const DetectorOrientation &other) const; }; diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 862358d7a..4e4d39a7e 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## 1.0.0 ### 1.0.0-rc.166 +* A miniCBF sweep takes the mounting from the imgCIF axis table its header carries: which laboratory direction the image's columns and rows run along, and which the spindle turns about. Where there is no table, a `+SLOW` on the `Oscillation_axis` line still says the spindle runs along the image's slow direction. All three were previously assumed, and two instruments are not what was assumed. +* Naming a miniCBF frame with no directory - a frame in the working directory - finds its sweep instead of reporting that no images were found. * A detector swung out on a 2theta arm is placed where it stands, from the depends_on transformation chain of an NXmx master or the `Detector_2theta` line of a miniCBF header; both were previously read and then ignored. * `jfjoch_viewer` opens PILATUS miniCBF sweeps - naming any frame opens the whole sweep - and can run a processing job on one. * A detector whose stored image is mirrored in Y or mounted at a multiple of 90 degrees can be described as such, in the detector configuration or with `--detector-mirror-y` / `--detector-quarter-turns`, rather than having to be expressed as a detector rotation. diff --git a/docs/DETECTOR_GEOMETRY.md b/docs/DETECTOR_GEOMETRY.md index 3cc216692..2b62eeafe 100644 --- a/docs/DETECTOR_GEOMETRY.md +++ b/docs/DETECTOR_GEOMETRY.md @@ -59,6 +59,30 @@ rot2 = asin(-slow.z) rot1 = atan2(-fast.z, normal.z) rot3 = atan with `rot2` in [-90°, 90°]. The angles are what is stored and what is written out, so a geometry given as angles comes back exactly as it was given. +## What a miniCBF header states about the mounting + +A PILATUS miniCBF gives the geometry twice. The `# ` lines every writer produces carry the distance, the +beam centre and the angles; some beamlines then append a CBF template block holding a full **imgCIF axis +table**, which states the laboratory direction of the image's fast and slow pixel directions, of the base +goniometer axis, and of a 2theta arm where there is one. Where that table is present it is read, in +preference to any assumption - it is the same information NXmx puts in `fast_pixel_direction` / +`slow_pixel_direction` and the goniometer `vector`, in the form this format states it. + +imgCIF's laboratory frame has Z from the sample towards the source and Y opposite gravity, so it differs +from the internal frame by a half turn about x - a rotation, not a mirror, so an axis carried through it +turns the same way by the same angle. + +Two things a header can state that an assumption gets wrong by 90 degrees, which no refinement recovers +and which the run's axis-sign rescue cannot reach either, a quarter turn not being a sign: + +* the image mounted a quarter turn round, so its columns run vertically; +* a spindle that turns about the **vertical** rather than the horizontal. + +Where a header carries no axis table, a `+SLOW` on its `# Oscillation_axis` line still says the spindle +runs along the image's slow direction rather than its fast one. The axis *name* on that line is not +usable - one header says `X.CW +SLOW` where its own table says the axis is Y - but the direction token is, +and on the header that states both they agree. + ## A detector swung out on a 2theta arm Chemical crystallography reaches high angle by swinging the detector out on a 2theta arm rather than by diff --git a/reader/HDF5MetadataSource.cpp b/reader/HDF5MetadataSource.cpp index ac68aaa60..20bef0df3 100644 --- a/reader/HDF5MetadataSource.cpp +++ b/reader/HDF5MetadataSource.cpp @@ -42,18 +42,7 @@ static std::optional ReadModuleOrientation(HDF5Object *file if ((f.size() != 3) || (s.size() != 3)) return {}; - const Coord fast = McStasToInternal(f); - const Coord slow = McStasToInternal(s); - - for (int64_t quarter_turns = 0; quarter_turns < 4; quarter_turns++) { - for (bool mirror_y: {false, true}) { - const DetectorOrientation orientation(mirror_y, quarter_turns); - const RotMatrix m = orientation.Matrix(); - if (((m.Column(0) - fast).Length() < 1e-3f) && ((m.Column(1) - slow).Length() < 1e-3f)) - return orientation; - } - } - return {}; + return DetectorOrientation::Match(McStasToInternal(f), McStasToInternal(s)); } // Where the detector stands, from the chain of transformations the file says it depends on. diff --git a/reader/JFJochCBFReader.cpp b/reader/JFJochCBFReader.cpp index a08639663..c60033e11 100644 --- a/reader/JFJochCBFReader.cpp +++ b/reader/JFJochCBFReader.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -57,7 +58,12 @@ std::optional