Files
Jungfraujoch/common/DetectorSetup.cpp
T
leonarski_fandClaude Opus 5 77c6d0f4ec Report the stored image depth in bit_depth_readout, and write underload_value
NXmx has no field for the depth of the stored image - only bit_depth_readout,
"how many bits the electronics record per pixel". The two diverge exactly when
summation is used: the readout keeps the detector's native width while the
summed image must be wider to hold the sum.

Every NXmx reader nonetheless takes bit_depth_readout as the width of the stored
pixel. dxtbx ignores the non-standard bit_depth_image entirely for a generic
NXmx file, derives its masking markers from bit_depth_readout, and raises
"Unsupported integer dtype uint32" for a 32-bit image when the field is absent.
Reporting the electronic value there would mislead precisely where it differs.

So report the image depth in both fields, and drop the machinery that existed to
carry the electronic one for a DECTRIS detector: the SIMPLON read, the
DetectorSetup setter, and the receiver-side propagation of a key that the DECTRIS
stream2 protocol does not even define. JUNGFRAU and PSI EIGER keep their readout
depth, which the FPGA acquisition genuinely needs.

Also write NXmx underload_value, the lowest valid value. Without it a reader
takes the trusted minimum to be -0x7FFFFFFF, so the error-pixel marker sits
inside the trusted range and is consumed as an intensity. Measured with DIALS
3.27 on a written file: trusted_range goes from (-2147483647, 32766) to
(-32767, 32766), so the INT16_MIN gap pixels are now masked.

Third fix in the same area: JFJochReceiverLite::Configure took the image width
from the incoming stream but not the sign, while the image itself is forwarded
byte-for-byte. A detector sending int32 was re-declared uint32, and the VDS
master was typed unsigned over signed data files. Take pixel_signed from the
stream too - it and the width are both carried by the one image_dtype key.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-22 18:15:44 +02:00

447 lines
16 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::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;
}