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
465 lines
17 KiB
C++
465 lines
17 KiB
C++
// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include <filesystem>
|
|
#include <utility>
|
|
|
|
#include "DetectorSetup.h"
|
|
#include "JFJochException.h"
|
|
#include "NetworkAddressConvert.h"
|
|
#define check_max(param, val, max) if ((val) > (max)) throw JFJochException(JFJochExceptionCategory::InputParameterAboveMax, param)
|
|
#define check_min(param, val, min) if ((val) < (min)) throw JFJochException(JFJochExceptionCategory::InputParameterBelowMin, param)
|
|
|
|
DetectorSetup::DetectorSetup(const DetectorGeometryFixed &geom, DetectorType detector_type,
|
|
const std::string &description, const std::vector<std::string> &det_modules_hostname)
|
|
: DetectorSetup(std::make_shared<DetectorGeometryFixed>(geom),
|
|
detector_type, description, det_modules_hostname) {
|
|
switch (detector_type) {
|
|
case DetectorType::DECTRIS:
|
|
break;
|
|
default:
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Detector not compatible with fixed geometry");
|
|
}
|
|
}
|
|
|
|
|
|
DetectorSetup::DetectorSetup(const DetectorGeometryModular &geom, DetectorType detector_type,
|
|
const std::string &description, const std::vector<std::string> &det_modules_hostname)
|
|
: DetectorSetup(std::make_shared<DetectorGeometryModular>(geom),
|
|
detector_type, description, det_modules_hostname) {
|
|
switch (detector_type) {
|
|
case DetectorType::EIGER:
|
|
case DetectorType::JUNGFRAU:
|
|
break;
|
|
default:
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Detector not compatible with modular geometry");
|
|
}
|
|
}
|
|
|
|
DetectorSetup::DetectorSetup(std::shared_ptr<DetectorGeometry> in_geometry,
|
|
DetectorType in_detector_type,
|
|
const std::string &in_description,
|
|
const std::vector<std::string> &
|
|
in_det_modules_hostname)
|
|
: description(in_description),
|
|
geometry(std::move(in_geometry)),
|
|
det_modules_hostname(in_det_modules_hostname),
|
|
gain_calibration(std::make_shared<JFGainCalibration>()),
|
|
detector_type(in_detector_type),
|
|
read_out_time(0),
|
|
min_count_time(MIN_COUNT_TIME),
|
|
min_frame_time(std::chrono::milliseconds(1)) {
|
|
|
|
if (description.empty())
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Detector description cannot be empty");
|
|
|
|
switch (detector_type) {
|
|
case DetectorType::EIGER:
|
|
high_voltage = 150;
|
|
read_out_time = PSI_EIGER_READOUT_TIME;
|
|
if (!det_modules_hostname.empty() && (2 * geometry->GetModulesNum() != det_modules_hostname.size()))
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Mismatch between number of modules in detector geometry and hostname (For EIGER - one module = 2 hostnames)");
|
|
break;
|
|
case DetectorType::JUNGFRAU:
|
|
high_voltage = 120;
|
|
bit_depth_readout = 16;
|
|
read_out_time = PSI_JUNGFRAU_READOUT_TIME;
|
|
if (!det_modules_hostname.empty() && (geometry->GetModulesNum() != det_modules_hostname.size()))
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Mismatch between number of modules in detector geometry and hostname");
|
|
break;
|
|
case DetectorType::DECTRIS:
|
|
high_voltage = 0;
|
|
// bit_depth_readout stays unset: a DECTRIS detector's electronic readout depth is a
|
|
// constant we never need, and what is reported downstream is the image depth (see
|
|
// DiffractionExperiment::FillMessage).
|
|
bit_depth_image = 16; // placeholder, replaced from the stream2 image_dtype when armed
|
|
read_out_time = std::chrono::microseconds(0);
|
|
if (!det_modules_hostname.empty() && ( det_modules_hostname.size() != 1))
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Just one address need to be provided for DECTRIS detector");
|
|
break;
|
|
default:
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Detector not supported");
|
|
}
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::Description(const std::string &input) {
|
|
if (input.empty())
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Detector description cannot be empty");
|
|
description = input;
|
|
return *this;
|
|
}
|
|
|
|
const DetectorGeometry &DetectorSetup::GetGeometry() const {
|
|
return *geometry;
|
|
}
|
|
|
|
const std::vector<std::string> &DetectorSetup::GetDetectorModuleHostname() const {
|
|
return det_modules_hostname;
|
|
}
|
|
|
|
uint64_t DetectorSetup::GetModulesNum() const {
|
|
return geometry->GetModulesNum();
|
|
}
|
|
|
|
std::string DetectorSetup::GetDescription() const {
|
|
return description;
|
|
}
|
|
|
|
float DetectorSetup::GetPixelSize_mm() const {
|
|
return pixel_size_um / 1000.0f;
|
|
}
|
|
|
|
std::string DetectorSetup::GetSensorMaterial() const {
|
|
return sensor_material;
|
|
}
|
|
|
|
float DetectorSetup::GetSensorThickness_um() const {
|
|
return sensor_thickness_um;
|
|
}
|
|
|
|
void DetectorSetup::LoadGain(const std::vector<std::string> &filenames) {
|
|
if (filenames.size() != GetModulesNum())
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Mismatch in number of gain calibration files");
|
|
gain_file_names = filenames;
|
|
gain_calibration->LoadGain(filenames);
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::UDPInterfaceCount(int64_t input) {
|
|
if ((input != 1) && (input != 2))
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Only 1 and 2 are supported as UDP interface count");
|
|
udp_interface_count = input;
|
|
return *this;
|
|
}
|
|
|
|
const std::vector<JFModuleGainCalibration> &DetectorSetup::GetGainCalibration() const {
|
|
return gain_calibration->GetCalibration();
|
|
}
|
|
|
|
int64_t DetectorSetup::GetUDPInterfaceCount() const {
|
|
if (detector_type == DetectorType::EIGER)
|
|
return 2;
|
|
return udp_interface_count;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::SensorMaterial(const std::string &input) {
|
|
sensor_material = input;
|
|
return *this;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::SensorThickness_um(float input) {
|
|
sensor_thickness_um = input;
|
|
return *this;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::PixelSize_um(float input) {
|
|
pixel_size_um = input;
|
|
return *this;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::Geometry(const DetectorGeometryFixed &input) {
|
|
if (detector_type != DetectorType::DECTRIS)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"PSI detector geometry cannot be updated during operation");
|
|
geometry = std::make_shared<DetectorGeometryFixed>(input);
|
|
return *this;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::TxDelay(const std::vector<int64_t> &v) {
|
|
if (!v.empty() && (v.size() != GetModulesNum()))
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Mismatch between size of TX delay vector and modules number");
|
|
for (const auto &i: v) {
|
|
if ((i < 0) || (i > 31))
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "TX delay must be in range 0-31");
|
|
}
|
|
tx_delay = v;
|
|
return *this;
|
|
}
|
|
|
|
const std::vector<int64_t> &DetectorSetup::GetTxDelay() const {
|
|
return tx_delay;
|
|
}
|
|
|
|
const std::vector<std::string> &DetectorSetup::GetGainFileNames() const {
|
|
return gain_file_names;
|
|
}
|
|
|
|
DetectorType DetectorSetup::GetDetectorType() const {
|
|
return detector_type;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::HighVoltage(int32_t input) {
|
|
high_voltage = input;
|
|
return *this;
|
|
}
|
|
|
|
int32_t DetectorSetup::GetHighVoltage() const {
|
|
return high_voltage;
|
|
}
|
|
|
|
void DetectorSetup::SetTrimFiles(const std::vector<std::string> &filenames) {
|
|
if (detector_type != DetectorType::EIGER)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Trim bits make sense only for EIGER");
|
|
|
|
if ((filenames.size() == 1)
|
|
&& std::filesystem::is_directory(filenames[0])) {
|
|
trim_file_directory = filenames[0];
|
|
trim_file_names.clear();
|
|
} else {
|
|
if (filenames.size() != 2 * GetModulesNum())
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Mismatch in number of trim bit calibration files");
|
|
trim_file_directory = "";
|
|
trim_file_names = filenames;
|
|
}
|
|
}
|
|
|
|
const std::vector<std::string> &DetectorSetup::GetTrimFileNames() const {
|
|
return trim_file_names;
|
|
}
|
|
|
|
std::string DetectorSetup::GetTrimFileDirectory() const {
|
|
return trim_file_directory;
|
|
}
|
|
|
|
std::string DetectorSetup::GetSerialNumber() const {
|
|
return serial_number;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::SerialNumber(const std::string &input) {
|
|
serial_number = input;
|
|
return *this;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::BaseIPv4Addr(const std::string &input) {
|
|
ipv4_base_addr = IPv4AddressFromStr(input);
|
|
return *this;
|
|
}
|
|
|
|
uint32_t DetectorSetup::GetSrcIPv4Addr(uint32_t half_module) const {
|
|
if (half_module >= GetUDPInterfaceCount() * GetModulesNum())
|
|
throw JFJochException(JFJochExceptionCategory::ArrayOutOfBounds, "Non existing module");
|
|
return ipv4_base_addr + (half_module << 24);
|
|
}
|
|
|
|
std::string DetectorSetup::GetBaseIPv4Addr() const {
|
|
return IPv4AddressToStr(ipv4_base_addr);
|
|
}
|
|
|
|
bool DetectorSetup::IsModuleSync() const {
|
|
if (GetModulesNum() == 1)
|
|
return false;
|
|
else
|
|
return module_sync;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::ModuleSync(bool input) {
|
|
module_sync = input;
|
|
return *this;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::MirrorY(bool input) {
|
|
mirror_y = input;
|
|
return *this;
|
|
}
|
|
|
|
bool DetectorSetup::IsMirrorY() const {
|
|
return mirror_y;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::ImageOrientation(const DetectorOrientation &input) {
|
|
image_orientation = input;
|
|
return *this;
|
|
}
|
|
|
|
DetectorOrientation DetectorSetup::GetImageOrientation() const {
|
|
return image_orientation;
|
|
}
|
|
|
|
DetectorSetup & DetectorSetup::ReadOutTime(std::chrono::nanoseconds input) {
|
|
if (input.count() < 0)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Read out time has to be non-negative");
|
|
read_out_time = input;
|
|
return *this;
|
|
}
|
|
|
|
std::chrono::nanoseconds DetectorSetup::GetReadOutTime() const {
|
|
return read_out_time;
|
|
}
|
|
|
|
std::chrono::nanoseconds DetectorSetup::GetMinFrameTime() const {
|
|
switch (GetDetectorType()) {
|
|
case DetectorType::EIGER:
|
|
return MIN_FRAME_TIME_EIGER;
|
|
case DetectorType::JUNGFRAU:
|
|
if (GetUDPInterfaceCount() == 1)
|
|
return MIN_FRAME_TIME_JUNGFRAU_HALF_SPEED;
|
|
return MIN_FRAME_TIME_JUNGFRAU_FULL_SPEED;
|
|
case DetectorType::DECTRIS:
|
|
return min_frame_time;
|
|
default:
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Detector not supported");
|
|
}
|
|
}
|
|
|
|
std::chrono::nanoseconds DetectorSetup::GetMinCountTime() const {
|
|
return min_count_time;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::MinCountTime(std::chrono::nanoseconds input) {
|
|
min_count_time = input;
|
|
return *this;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::MinFrameTime(std::chrono::nanoseconds input) {
|
|
min_frame_time = input;
|
|
return *this;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::BitDepthImage(int64_t input) {
|
|
if (GetDetectorType() != DetectorType::DECTRIS)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Bit depth image can be only changed for DECTRIS detector");
|
|
switch (input) {
|
|
case 8:
|
|
case 16:
|
|
case 32:
|
|
bit_depth_image = input;
|
|
return *this;
|
|
default:
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Bit depth image can be only 8, 16 or 32");
|
|
}
|
|
}
|
|
|
|
std::optional<int64_t> DetectorSetup::GetBitDepthReadout() const {
|
|
return bit_depth_readout;
|
|
}
|
|
|
|
std::optional<int64_t> DetectorSetup::GetBitDepthImage() const {
|
|
return bit_depth_image;
|
|
}
|
|
|
|
std::string DetectorSetup::GetDECTRISStream2Addr() const {
|
|
if (GetDetectorType() != DetectorType::DECTRIS)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Stream2 only possible for DECTRIS systems");
|
|
if (det_modules_hostname.size() != 1)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Stream2 address not defined");
|
|
return "tcp://" + det_modules_hostname[0] + ":" + std::to_string(SimplonStream2Port);
|
|
}
|
|
|
|
float DetectorSetup::GetMinThreshold_keV() const {
|
|
return min_energy_threshold_keV;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::MinThreshold_keV(float input) {
|
|
if (input <= 0.0f)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Min threshold must be positive number");
|
|
min_energy_threshold_keV = input;
|
|
return *this;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::SaturationLimit(std::optional<int64_t> input) {
|
|
if (input && input <= 0)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Saturation limit must be positive number");
|
|
saturation_limit = input;
|
|
return *this;
|
|
}
|
|
|
|
std::optional<int64_t> DetectorSetup::GetSaturationLimit() const {
|
|
return saturation_limit;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::DECTRISROI(const std::string &input) {
|
|
dectris_roi = input;
|
|
return *this;
|
|
}
|
|
|
|
std::string DetectorSetup::GetDECTRISROI() const {
|
|
if (dectris_roi.empty())
|
|
return "disabled";
|
|
return dectris_roi;
|
|
}
|
|
|
|
std::optional<DetectorSettings> DetectorSetup::GetDefaultSettings() const {
|
|
return settings;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::DefaultSettings(const std::optional<DetectorSettings> &input) {
|
|
settings = input;
|
|
return *this;
|
|
}
|
|
|
|
DetectorSetup DetJF4M(const std::string &description, const std::vector<std::string> &det_modules_hostname) {
|
|
return DetJF(8, 2, 8, 36, true, description, det_modules_hostname);
|
|
}
|
|
|
|
DetectorSetup DetJF9M(const std::string &description, const std::vector<std::string> &det_modules_hostname) {
|
|
return DetJF(18, 3, 8, 36, true, description, det_modules_hostname);
|
|
}
|
|
|
|
DetectorSetup DetJF(int32_t nmodules, int32_t horizontal_stacking, int32_t gap_x, int32_t gap_y, bool mirror_y,
|
|
const std::string &description, const std::vector<std::string> &det_modules_hostname) {
|
|
return DetJF(DetectorGeometryModular(nmodules, horizontal_stacking, gap_x, gap_y, mirror_y),
|
|
description, det_modules_hostname);
|
|
}
|
|
|
|
DetectorSetup DetJF(const DetectorGeometryModular &geom, const std::string &description,
|
|
const std::vector<std::string> &det_modules_hostname) {
|
|
return {geom, DetectorType::JUNGFRAU, description, det_modules_hostname};
|
|
}
|
|
|
|
DetectorSetup DetEIGER(int32_t nmodules, int32_t horizontal_stacking, int32_t gap_x, int32_t gap_y, bool mirror_y,
|
|
const std::string &description, const std::vector<std::string> &det_modules_hostname) {
|
|
return DetEIGER(DetectorGeometryModular(nmodules, horizontal_stacking, gap_x, gap_y, mirror_y),
|
|
description, det_modules_hostname);
|
|
}
|
|
|
|
DetectorSetup DetEIGER(const DetectorGeometryModular &geom, const std::string &description,
|
|
const std::vector<std::string> &det_modules_hostname) {
|
|
return {geom, DetectorType::EIGER, description, det_modules_hostname};
|
|
}
|
|
|
|
DetectorSetup DetDECTRIS(int64_t width, int64_t height, const std::string &description, const std::string &addr) {
|
|
if (addr.empty())
|
|
return {DetectorGeometryFixed(width, height), DetectorType::DECTRIS, description, {}};
|
|
else
|
|
return {DetectorGeometryFixed(width, height), DetectorType::DECTRIS, description, {addr}};
|
|
}
|
|
|
|
int32_t DetectorSetup::GetTempThreshold_degC() const {
|
|
return temperature_thresold_degC;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::TempThreshold_degC(int64_t input) {
|
|
check_min("Temperature threshold (degC)", input, 40);
|
|
check_max("Temperature threshold (degC)", input, 70);
|
|
temperature_thresold_degC = static_cast<int32_t>(input);
|
|
return *this;
|
|
}
|
|
|
|
DetectorSetup &DetectorSetup::TrimEnergies_eV(std::vector<int> input) {
|
|
trim_energy_eV_values = std::move(input);
|
|
return *this;
|
|
}
|
|
|
|
std::vector<int> DetectorSetup::GetTrimEnergies_eV() const {
|
|
return trim_energy_eV_values;
|
|
}
|