Build Packages / build:rpm (rocky9_sls9) (push) Successful in 18m57s
Build Packages / Unit tests (push) Skipped
Build Packages / build:windows:nocuda (push) Successful in 16m55s
Build Packages / build:windows:cuda (push) Successful in 18m48s
Build Packages / build:viewer-tgz:cpu (push) Successful in 13m10s
Build Packages / build:viewer-tgz:cuda (push) Successful in 14m45s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 22m23s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 20m12s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 23m7s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 20m43s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 23m9s
Build Packages / XDS test (durin plugin) (push) Successful in 12m26s
Build Packages / build:rpm (rocky9) (push) Successful in 24m58s
Build Packages / Generate python client (push) Successful in 50s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 23m20s
Build Packages / Create release (push) Skipped
Build Packages / XDS test (JFJoch plugin) (push) Successful in 12m37s
Build Packages / build:rpm (rocky8) (push) Successful in 27m58s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 25m38s
Build Packages / Build documentation (push) Successful in 59s
Build Packages / DIALS test (push) Successful in 23m16s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m38s
**Files written by Jungfraujoch now import correctly in DIALS, XDS and pyFAI.** A tilted detector, a grid scan, a still recorded at a goniometer position, and saturated or unreadable pixels were each described in a way that a third-party program acted on wrongly. If you process Jungfraujoch data outside Jungfraujoch, prefer this release to any earlier one. * HDF5: the detector tilt (`rot1`/`rot2`/`rot3`) is exported correctly in the NXmx transformation chain; untilted geometries are unaffected. * HDF5: a still recorded at a goniometer position is no longer read back as a single image, and a grid scan records a stationary spindle so a program that requires a rotation axis can open it. * HDF5: the sample transformation chain is written in mounting order, with a Smargon head position told apart from the spindle, one entry per image, `module_offset` as a float unit vector, and `offset_units` on every offset. * HDF5: saturated, underloaded and unreadable pixels are described so a downstream program masks them - `saturation_value`, `underload_value`, `error_value` and `bit_depth_readout` are written correctly, and a data file missing next to a VDS master reads as the error marker rather than as zero counts. * HDF5: the rotation axis is read back under whatever name it carries, and `mirror_y` records whether the assembled image is mirrored in Y relative to the detector's raw readout. * A grid scan and a goniometer axis can both be set; they are no longer alternatives. * `images_per_file` is chosen from the acquisition when it is not given: a rotation sweep of at most 20000 images goes into a single data file, a grid scan splits on whole fast-axis rows, and stills and serial keep 1000. * The writer refuses a stream whose start message declares a different pixel format than its images carry, and a DECTRIS detector sending signed images is no longer declared unsigned. * The image stream can carry the sample transformation chain (`transformations`, in the END message); a producer that does not send it gets the same chain built by the writer. * rugnux: fixing the space group with `-S` no longer prevents the lattice from being found - a lattice indexed in a different setting is reindexed into that group's own setting, and a run whose crystal does not have that group's lattice stops and names the cell it indexed as, rather than reporting statistics that cannot describe it. * rugnux: the per-image resolution estimate now predicts the resolution the merged data reach rather than the highest-resolution spot found, and is reported as `SPOT_RESOLUTION_ESTIMATE`. * rugnux: two runs of the same command on the same images produce the same merged intensities; the azimuthal profile written alongside them is not yet reproducible in the same way. * rugnux: the offline lattice refinement is bounded by iterations rather than by a wall clock, so a loaded machine can no longer refine to a different lattice; a live acquisition keeps its real-time bound. * rugnux: the detector-frame modulation correction is fitted on a grid spanning the detector, so whether it is applied no longer depends on how far integration reached. * rugnux: the geometry pre-pass no longer writes `<prefix>_01.mtz`, `_01.cif`, `_01.hkl` and `_01_image.dat`; the refined second pass writes those files under `<prefix>`, and that is the result to use. * rugnux: `_process.h5` describes the pixel format of the images it links to, and is written on a thread of its own. * rugnux: the detector geometry is also logged in XDS's convention (`ORGX`/`ORGY`, detector axis vectors, rotation axis), so it can be compared with an XDS refinement. * rugnux: an image integrated in pyFAI through the `.poni` file written by `--mode calibration` comes out with the correct azimuth, and the file declares pyFAI's `orientation`, which needs pyFAI 2024.01 or newer. Radial integration is unchanged. * rugnux: a rotation run is substantially faster throughout - beam-stop detection, first-pass indexing, geometry refinement, integration, scaling and merging - and observations outside the scaling resolution range are dropped as they are ingested. The refined geometry, the space group chosen and the merged statistics are unchanged. * Faster spot finding and indexing, on the broker as well as in rugnux; the spots found and the lattices indexed are unchanged. * A run reserves substantially less GPU memory: nothing is allocated for buffers that are never read, and a worker builds only the engines it uses. * rugnux: with `-N` left at its default the per-image loop of `--mode mx` uses at most 16 workers per GPU, rather than one per hardware thread; an explicit `-N` is obeyed as given. * CUDA 12 builds now contain device code for Volta, so the RHEL 8 packages and the portable Linux `.tgz` run on a V100; the CUDA 13 artefacts (RHEL 9, Ubuntu, Windows) remain Turing and newer. * The build resolves a single Eigen for the whole project, and refuses to configure if Ceres picks up a different one; a build that mixed two Eigen versions was undefined behaviour and crashed at -O2. * Documentation: a security page, and the supported GPU generations and minimum NVIDIA driver version of every released artefact. **Breaking change to OpenAPI** - regenerate the client (`jfjoch-client` 1.0.0-rc.162, `frontend/src/client`): * `dataset_settings.images_per_file` is no longer `default: 1000` and no longer accepts `0`; it is optional, and its minimum is 1. A client sending `0` (previously "one file for the whole run") is now rejected - omit the field instead, which for a rotation sweep gives the same single file. * `file_writer_format` now defaults to `NXmxVDS`, matching the server's own default and the layout recommended for DIALS, XDS and CrystFEL. A generated client that fills in schema defaults and does not set the format explicitly will write VDS masters where it previously wrote legacy ones; set `NXmxLegacy` explicitly to keep them. --------- Co-authored-by: jungfrau <jungfrau@mx-aare-test.psi.ch> Reviewed-on: #72 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
283 lines
12 KiB
C++
283 lines
12 KiB
C++
// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "JFJochBrokerParser.h"
|
|
#include "../common/NetworkAddressConvert.h"
|
|
#include "../image_pusher/ZMQStream2Pusher.h"
|
|
#include "../image_pusher/CBORFilePusher.h"
|
|
#include "../image_pusher/HDF5FilePusher.h"
|
|
#include "OpenAPIConvert.h"
|
|
#include "Detector_type.h"
|
|
#include "../image_pusher/NonePusher.h"
|
|
#include "../image_pusher/TCPStreamPusher.h"
|
|
|
|
DetectorGeometryModular ParseStandardDetectorGeometry(const org::openapitools::server::model::Detector &j) {
|
|
auto s = j.getStandardGeometry();
|
|
return DetectorGeometryModular(s.getNmodules(), s.getModulesInRow(), s.getGapX(), s.getGapY(), j.isMirrorY());
|
|
}
|
|
|
|
DetectorModuleGeometry::Direction Convert(const org::openapitools::server::model::Detector_module_direction& d) {
|
|
switch (d.getValue()) {
|
|
case org::openapitools::server::model::Detector_module_direction::eDetector_module_direction::XP:
|
|
return DetectorModuleGeometry::Direction::Xpos;
|
|
case org::openapitools::server::model::Detector_module_direction::eDetector_module_direction::XN:
|
|
return DetectorModuleGeometry::Direction::Xneg;
|
|
case org::openapitools::server::model::Detector_module_direction::eDetector_module_direction::YP:
|
|
return DetectorModuleGeometry::Direction::Ypos;
|
|
case org::openapitools::server::model::Detector_module_direction::eDetector_module_direction::YN:
|
|
return DetectorModuleGeometry::Direction::Yneg;
|
|
default:
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "invalid detector direction");
|
|
}
|
|
}
|
|
|
|
DetectorType Convert(const org::openapitools::server::model::Detector_type &d) {
|
|
switch (d.getValue()) {
|
|
case org::openapitools::server::model::Detector_type::eDetector_type::EIGER:
|
|
return DetectorType::EIGER;
|
|
case org::openapitools::server::model::Detector_type::eDetector_type::JUNGFRAU:
|
|
return DetectorType::JUNGFRAU;
|
|
case org::openapitools::server::model::Detector_type::eDetector_type::DECTRIS:
|
|
return DetectorType::DECTRIS;
|
|
default:
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "invalid detector type");
|
|
}
|
|
}
|
|
|
|
DetectorGeometryModular ParseCustomDetectorGeometry(const org::openapitools::server::model::Detector &j) {
|
|
std::vector<DetectorModuleGeometry> modules;
|
|
for (const auto &iter: j.getCustomGeometry()) {
|
|
auto fast = Convert(iter.getFastAxis());
|
|
auto slow = Convert(iter.getSlowAxis());
|
|
modules.emplace_back(iter.getX0(), iter.getY0(), fast, slow);
|
|
}
|
|
return DetectorGeometryModular(modules, j.isMirrorY());
|
|
}
|
|
|
|
|
|
DetectorGeometryModular ParseDetectorGeometry(const org::openapitools::server::model::Detector &d) {
|
|
if (d.standardGeometryIsSet() && d.customGeometryIsSet())
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Need to set EITHER standard or custom geometry");
|
|
|
|
if (d.standardGeometryIsSet())
|
|
return ParseStandardDetectorGeometry(d);
|
|
else if (d.customGeometryIsSet())
|
|
return ParseCustomDetectorGeometry(d);
|
|
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Need to set EITHER standard or custom geometry");
|
|
}
|
|
|
|
DetectorSetup ParseDetectorSetup(const org::openapitools::server::model::Detector &d) {
|
|
DetectorType detector_type = Convert(d.getType());
|
|
|
|
if (detector_type == DetectorType::DECTRIS) {
|
|
std::string hostname;
|
|
if (d.getHostname().size() > 1)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"DECTRIS detector requires single hostname (or none)");
|
|
else if (d.getHostname().size() == 1)
|
|
hostname = d.getHostname()[0];
|
|
|
|
DetectorSetup setup = DetDECTRIS(1,1, d.getDescription(), hostname);
|
|
|
|
if (d.roiModeIsSet())
|
|
setup.DECTRISROI(d.getRoiMode());
|
|
|
|
return setup;
|
|
}
|
|
|
|
DetectorGeometryModular geom = ParseDetectorGeometry(d);
|
|
|
|
DetectorSetup setup(geom, detector_type, d.getDescription(), d.getHostname());
|
|
|
|
auto calib = d.getCalibrationFile();
|
|
auto trim_energies = d.getTrimEnergiesEV();
|
|
if (!calib.empty()) {
|
|
switch (detector_type) {
|
|
case DetectorType::EIGER:
|
|
setup.SetTrimFiles(calib);
|
|
if (trim_energies.empty())
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Trimming energies not provided");
|
|
setup.TrimEnergies_eV(trim_energies);
|
|
break;
|
|
case DetectorType::JUNGFRAU:
|
|
setup.LoadGain(calib);
|
|
break;
|
|
default:
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Detector type not supported");
|
|
}
|
|
}
|
|
|
|
switch (detector_type) {
|
|
case DetectorType::EIGER:
|
|
case DetectorType::JUNGFRAU:
|
|
setup.PixelSize_um(75.0f);
|
|
break;
|
|
default:
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Detector type not supported");
|
|
}
|
|
if (d.highVoltageVIsSet())
|
|
setup.HighVoltage(d.getHighVoltageV());
|
|
|
|
setup.UDPInterfaceCount(d.getUdpInterfaceCount())
|
|
.SerialNumber(d.getSerialNumber())
|
|
.ModuleSync(d.isModuleSync())
|
|
.MirrorY(d.isMirrorY());
|
|
|
|
// Only override the sensor from the request when it explicitly sets these. The API model defaults
|
|
// them (320 um / Si) with IsSet=false, so an unconditional set would clobber the detector-reported
|
|
// value (DECTRIS SIMPLON read) or a detector-specific default with 320 um (highVoltageV above is
|
|
// guarded the same way).
|
|
if (d.sensorThicknessUmIsSet())
|
|
setup.SensorThickness_um(d.getSensorThicknessUm());
|
|
if (d.sensorMaterialIsSet())
|
|
setup.SensorMaterial(d.getSensorMaterial());
|
|
|
|
if (d.readoutTimeNsIsSet())
|
|
setup.ReadOutTime(std::chrono::nanoseconds(d.getReadoutTimeNs()));
|
|
|
|
if (d.baseDataIpv4AddressIsSet())
|
|
setup.BaseIPv4Addr(d.getBaseDataIpv4Address());
|
|
if (d.txDelayIsSet())
|
|
setup.TxDelay(d.getTxDelay());
|
|
|
|
if (d.minCountTimeNsIsSet())
|
|
setup.MinCountTime(std::chrono::nanoseconds(d.getMinCountTimeNs()));
|
|
|
|
if (d.minFrameTimeNsIsSet())
|
|
setup.MinFrameTime(std::chrono::nanoseconds(d.getMinFrameTimeNs()));
|
|
|
|
if (d.defaultSettingsIsSet())
|
|
setup.DefaultSettings(Convert(d.getDefaultSettings()));
|
|
|
|
if (d.tempThresoldDegCIsSet())
|
|
setup.TempThreshold_degC(d.getTempThresoldDegC());
|
|
|
|
return setup;
|
|
}
|
|
|
|
void ParseFacilityConfiguration(const org::openapitools::server::model::Jfjoch_settings &j, DiffractionExperiment &experiment) {
|
|
if (j.instrumentIsSet())
|
|
experiment.ImportInstrumentMetadata(Convert(j.getInstrument()));
|
|
|
|
if (j.fileWriterIsSet())
|
|
experiment.ImportFileWriterSettings(Convert(j.getFileWriter()));
|
|
|
|
if (j.detectorSettingsIsSet())
|
|
experiment.ImportDetectorSettings(Convert(j.getDetectorSettings()));
|
|
|
|
if (j.azimIntIsSet())
|
|
experiment.ImportAzimuthalIntegrationSettings(Convert(j.getAzimInt()));
|
|
|
|
if (j.imageFormatIsSet())
|
|
experiment.ImportImageFormatSettings(Convert(j.getImageFormat()));
|
|
|
|
if (j.indexingIsSet())
|
|
experiment.ImportIndexingSettings(Convert(j.getIndexing()));
|
|
|
|
if (j.braggIntegrationIsSet())
|
|
experiment.ImportBraggIntegrationSettings(Convert(j.getBraggIntegration()));
|
|
else
|
|
// A config with no bragg_integration block still has to bootstrap a concrete max_hkl, or the
|
|
// predictor falls back to deriving the hkl cube from the refined cell and a live acquisition's
|
|
// per-image cost is decided by whichever crystal is mounted. None of the shipped configs sets
|
|
// the block, so this is the normal path online.
|
|
experiment.ImportBraggIntegrationSettings(
|
|
BraggIntegrationSettings().MaxHKL(BRAGG_ONLINE_DEFAULT_MAX_HKL));
|
|
|
|
if (j.darkMaskIsSet())
|
|
experiment.ImportDarkMaskSettings(Convert(j.getDarkMask()));
|
|
}
|
|
|
|
std::unique_ptr<ImagePusher> ParseZMQImagePusher(const org::openapitools::server::model::Jfjoch_settings &j) {
|
|
if (!j.zeromqIsSet())
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "ZeroMQ settings must be provided");
|
|
|
|
std::optional<int32_t> send_buffer_size;
|
|
if (j.getZeromq().sendBufferSizeIsSet())
|
|
send_buffer_size = j.getZeromq().getSendBufferSize();
|
|
|
|
std::optional<int32_t> send_watermark;
|
|
if (j.getZeromq().sendWatermarkIsSet())
|
|
send_watermark = j.getZeromq().getSendWatermark();
|
|
|
|
auto tmp = std::make_unique<ZMQStream2Pusher>(j.getZeromq().getImageSocket(),
|
|
send_watermark,
|
|
send_buffer_size);
|
|
|
|
if (j.getZeromq().writerNotificationSocketIsSet())
|
|
tmp->WriterNotificationSocket(j.getZeromq().getWriterNotificationSocket());
|
|
return std::move(tmp);
|
|
}
|
|
|
|
std::unique_ptr<ImagePusher> ParseTCPImagePusher(const org::openapitools::server::model::Jfjoch_settings &j) {
|
|
if (!j.tcpIsSet())
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "TCP/IP Socket settings must be provided");
|
|
|
|
std::optional<int32_t> send_buffer_size;
|
|
if (j.getTcp().sendBufferSizeIsSet())
|
|
send_buffer_size = j.getTcp().getSendBufferSize();
|
|
|
|
auto tmp = std::make_unique<TCPStreamPusher>(j.getTcp().getImageSocket(), j.getTcp().getNwriters(), send_buffer_size);
|
|
|
|
// Optional liveness/backpressure tuning; unset -> keep the pusher's built-in defaults.
|
|
if (j.getTcp().peerLivenessTimeoutMsIsSet() && j.getTcp().getPeerLivenessTimeoutMs() > 0)
|
|
tmp->SetPeerLivenessTimeout(std::chrono::milliseconds(j.getTcp().getPeerLivenessTimeoutMs()));
|
|
if (j.getTcp().maxBackpressureTimeoutMsIsSet() && j.getTcp().getMaxBackpressureTimeoutMs() > 0)
|
|
tmp->SetMaxBackpressureTimeout(std::chrono::milliseconds(j.getTcp().getMaxBackpressureTimeoutMs()));
|
|
|
|
return std::move(tmp);
|
|
}
|
|
|
|
std::unique_ptr<ImagePusher> ParseImagePusher(const org::openapitools::server::model::Jfjoch_settings &j) {
|
|
switch (j.getImagePusher().getValue()) {
|
|
case org::openapitools::server::model::Image_pusher_type::eImage_pusher_type::ZEROMQ:
|
|
return ParseZMQImagePusher(j);
|
|
case org::openapitools::server::model::Image_pusher_type::eImage_pusher_type::TCP:
|
|
return ParseTCPImagePusher(j);
|
|
case org::openapitools::server::model::Image_pusher_type::eImage_pusher_type::HDF5:
|
|
return std::make_unique<HDF5FilePusher>();
|
|
case org::openapitools::server::model::Image_pusher_type::eImage_pusher_type::NONE:
|
|
return std::make_unique<NonePusher>();
|
|
case org::openapitools::server::model::Image_pusher_type::eImage_pusher_type::CBOR:
|
|
return std::make_unique<CBORFilePusher>();
|
|
default:
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Invalid value");
|
|
}
|
|
}
|
|
|
|
void ParseAcquisitionDeviceGroup(const org::openapitools::server::model::Jfjoch_settings &input,
|
|
AcquisitionDeviceGroup &aq_devices) {
|
|
if (!input.pcieIsSet())
|
|
aq_devices.AddHLSDevice(256);
|
|
else for (auto &p: input.getPcie()) {
|
|
std::optional<uint32_t> ipv4_addr = {};
|
|
if (p.ipv4IsSet())
|
|
ipv4_addr = IPv4AddressFromStr(p.getIpv4());
|
|
aq_devices.AddPCIeDevice(p.getBlk(), ipv4_addr);
|
|
}
|
|
}
|
|
|
|
void ParseReceiverSettings(const org::openapitools::server::model::Jfjoch_settings &input, JFJochReceiverService &service) {
|
|
// Using default in case
|
|
service.NumThreads(input.getReceiverThreads());
|
|
|
|
if (input.zeromqPreviewIsSet()) {
|
|
service.PreviewSocket(input.getZeromqPreview().getSocketAddress());
|
|
service.PreviewSocketSettings(Convert(input.getZeromqPreview()));
|
|
}
|
|
|
|
if (input.zeromqMetadataIsSet()) {
|
|
service.MetadataSocket(input.getZeromqMetadata().getSocketAddress());
|
|
service.MetadataSocketSettings(Convert(input.getZeromqMetadata()));
|
|
}
|
|
}
|
|
|
|
SpotFindingSettings ParseSpotFindingSettings(const org::openapitools::server::model::Jfjoch_settings &input) {
|
|
if (input.spotFindingIsSet())
|
|
return Convert(input.getSpotFinding());
|
|
return SpotFindingSettings();
|
|
} |