Fix the defects found reviewing the branch before merge
Build Packages / build:viewer-tgz:cpu (push) Successful in 19m32s
Build Packages / build:windows:nocuda (push) Successful in 19m57s
Build Packages / build:viewer-tgz:cuda (push) Successful in 22m45s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 22m38s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 23m24s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 28m8s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 28m9s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 28m18s
Build Packages / XDS test (durin plugin) (push) Successful in 11m10s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 20m21s
Build Packages / build:windows:cuda (push) Successful in 22m5s
Build Packages / build:rpm (rocky9) (push) Successful in 20m57s
Build Packages / Generate python client (push) Successful in 34s
Build Packages / Build documentation (push) Successful in 1m29s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (ubuntu2204) (push) Successful in 25m41s
Build Packages / DIALS test (push) Successful in 21m19s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 21m34s
Build Packages / build:rpm (rocky8) (push) Successful in 27m4s
Build Packages / XDS test (neggia plugin) (push) Successful in 10m19s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 10m58s
Build Packages / Unit tests (push) Successful in 1h17m36s
Build Packages / build:viewer-tgz:cpu (push) Successful in 19m32s
Build Packages / build:windows:nocuda (push) Successful in 19m57s
Build Packages / build:viewer-tgz:cuda (push) Successful in 22m45s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 22m38s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 23m24s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 28m8s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 28m9s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 28m18s
Build Packages / XDS test (durin plugin) (push) Successful in 11m10s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 20m21s
Build Packages / build:windows:cuda (push) Successful in 22m5s
Build Packages / build:rpm (rocky9) (push) Successful in 20m57s
Build Packages / Generate python client (push) Successful in 34s
Build Packages / Build documentation (push) Successful in 1m29s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (ubuntu2204) (push) Successful in 25m41s
Build Packages / DIALS test (push) Successful in 21m19s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 21m34s
Build Packages / build:rpm (rocky8) (push) Successful in 27m4s
Build Packages / XDS test (neggia plugin) (push) Successful in 10m19s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 10m58s
Build Packages / Unit tests (push) Successful in 1h17m36s
Image buffer: the per-image CBOR metadata headroom had been re-derived from the online reflection cap alone, which cut it from 4 MiB to 2.55 MB while the measured worst case - reflections plus the capped spot list plus the three azimuthal arrays - is 2.9 MB, so the receiver dropped the frames with the most to say. Restore it and give it a name that both the code and its guard test read: written down twice, the two had drifted and the test kept passing against the value the code had left. Spot finding: an unset low_resolution_limit means no limit at that end, as an unset high_resolution_limit already did. An optional rather than a zero sentinel, because zero is not a natural "no limit" here - every pixel lies above it, so the plain comparison masked the whole image instead of none of it, and nothing validated the zero. The API field is no longer required; a zero is folded into the unset case at the boundary, where older clients still send it, so one spelling reaches the analysis code. The FPGA takes its fixed-point ceiling instead, since ap_ufixed<16,9> wraps above 512 A and would have masked everything. image_preprocessing: check the CUDA calls on the fused decode path - the one new GPU file with none, and the path fed by bytes we did not produce. An unchecked synchronise returned the host-written sentinel as if it were a measurement, so the decode looked successful and the fallback to the host decoder never fired. rugnux: --stride no longer writes one past the end of the per-image arrays, whose count floored where the worker loop ceils, and the written process file links the images actually processed rather than the first N - each frame's picture now sits next to its own analysis. Powder calibration: the face-centred calibrants no longer list their systematically absent rings, so the distance fit starts from a reflection that exists rather than an extinct one; the triclinic calibrant covers both signs of h and k instead of a single octant, which is only valid for a diagonal metric. The test asserted the old behaviour - one ring formula for every cubic standard - and is rewritten. CBOR: skip an unknown tagged value in the end block, as the other four blocks already do. One advance lands on the tagged item rather than past it, so an older reader fed a newer end message threw and never finalized its file. Viewer: a settings value the setter rejects no longer escapes as an uncaught throw from a worker slot, and the field offers only what the setter accepts. Space-group search: judge stage B on the same "present" cut stage A already computes. Merged sigma is floored so no reflection reads above ISa, so on a low-ISa merge the fixed cut left both stage B tests unsatisfiable - every screw axis passed unchallenged and the centering rescue switched itself off on exactly the weak data it exists for. Where the fixed cut is the smaller of the two they are equal and this is inert: over the 37-crystal rotation battery every crystal reports the identical space group and identical merge statistics, so it is a no-op there and the low-ISa case it targets remains unmeasured. rugnux: --polarization reaches --mode azint, which parsed the flag and then dropped it; that mode also applies the same polarization default as every other mode. Acknowledge the ACTS/traccc project, whose sparse connected-component labelling both spot extractors take their algorithm from, with its citation and its license. The rc.161 change list is brought back to one line per entry, and the user-visible changes that were missing from it added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -320,9 +320,14 @@ void AcquisitionDevice::RunInternalGenerator(const DiffractionExperiment &experi
|
||||
void AcquisitionDevice::SetSpotFinderParameters(const SpotFindingSettings &settings) {
|
||||
SpotFinderParameters fpga_parameters{};
|
||||
|
||||
// The FPGA compares d against these as xray_d_t = ap_ufixed<16,9> (hls_jfjoch.h), which tops out just
|
||||
// below 512 A and WRAPS above it - so "no low-resolution limit" is that ceiling, not infinity. Sending
|
||||
// a larger number would wrap to a small d and mask the whole image. The high end needs no such care:
|
||||
// no pixel has d < 0.
|
||||
constexpr float FPGA_MAX_D_A = 511.0f;
|
||||
fpga_parameters.snr_threshold = settings.signal_to_noise_threshold;
|
||||
fpga_parameters.count_threshold = settings.photon_count_threshold;
|
||||
fpga_parameters.max_d = settings.low_resolution_limit;
|
||||
fpga_parameters.max_d = settings.low_resolution_limit.value_or(FPGA_MAX_D_A);
|
||||
fpga_parameters.min_d = settings.high_resolution_limit.value_or(0.0f);
|
||||
fpga_parameters.min_pix_per_spot = settings.min_pix_per_spot.value_or(2);
|
||||
HW_SetSpotFinderParameters(fpga_parameters);
|
||||
|
||||
@@ -16,9 +16,13 @@ SpotFindingSettings Convert(const org::openapitools::server::model::Spot_finding
|
||||
ret.photon_count_threshold = input.getPhotonCountThreshold();
|
||||
ret.min_pix_per_spot = input.getMinPixPerSpot();
|
||||
ret.max_pix_per_spot = input.getMaxPixPerSpot();
|
||||
if (input.highResolutionLimitIsSet())
|
||||
// Both limits are optional and unset means "no limit at that end". A value of 0 has meant the same
|
||||
// thing since rc.161 and clients still send it that way, so fold it into the unset case here - the
|
||||
// analysis code then has exactly one spelling for "no limit" and no zero to special-case.
|
||||
if (input.highResolutionLimitIsSet() && input.getHighResolutionLimit() > 0)
|
||||
ret.high_resolution_limit = input.getHighResolutionLimit();
|
||||
ret.low_resolution_limit = input.getLowResolutionLimit();
|
||||
if (input.lowResolutionLimitIsSet() && input.getLowResolutionLimit() > 0)
|
||||
ret.low_resolution_limit = input.getLowResolutionLimit();
|
||||
ret.enable = input.isEnable();
|
||||
ret.indexing = input.isIndexing();
|
||||
ret.quick_integration = input.isQuickIntegration();
|
||||
@@ -41,7 +45,8 @@ org::openapitools::server::model::Spot_finding_settings Convert(const SpotFindin
|
||||
ret.setMaxPixPerSpot(input.max_pix_per_spot);
|
||||
if (input.high_resolution_limit.has_value())
|
||||
ret.setHighResolutionLimit(input.high_resolution_limit.value());
|
||||
ret.setLowResolutionLimit(input.low_resolution_limit);
|
||||
if (input.low_resolution_limit.has_value())
|
||||
ret.setLowResolutionLimit(input.low_resolution_limit.value());
|
||||
ret.setEnable(input.enable);
|
||||
ret.setIndexing(input.indexing);
|
||||
ret.setHighResolutionLimitForSpotCountLowRes(input.cutoff_spot_count_low_res);
|
||||
|
||||
@@ -30,6 +30,7 @@ Spot_finding_settings::Spot_finding_settings()
|
||||
m_High_resolution_limit = 0.0f;
|
||||
m_High_resolution_limitIsSet = false;
|
||||
m_Low_resolution_limit = 0.0f;
|
||||
m_Low_resolution_limitIsSet = false;
|
||||
m_High_resolution_limit_for_spot_count_low_res = 0.0f;
|
||||
m_Quick_integration = false;
|
||||
m_Ice_ring_width_q_recipA = 0.03f;
|
||||
@@ -223,8 +224,8 @@ bool Spot_finding_settings::operator==(const Spot_finding_settings& rhs) const
|
||||
|
||||
((!highResolutionLimitIsSet() && !rhs.highResolutionLimitIsSet()) || (highResolutionLimitIsSet() && rhs.highResolutionLimitIsSet() && getHighResolutionLimit() == rhs.getHighResolutionLimit())) &&
|
||||
|
||||
(getLowResolutionLimit() == rhs.getLowResolutionLimit())
|
||||
&&
|
||||
|
||||
((!lowResolutionLimitIsSet() && !rhs.lowResolutionLimitIsSet()) || (lowResolutionLimitIsSet() && rhs.lowResolutionLimitIsSet() && getLowResolutionLimit() == rhs.getLowResolutionLimit())) &&
|
||||
|
||||
(getHighResolutionLimitForSpotCountLowRes() == rhs.getHighResolutionLimitForSpotCountLowRes())
|
||||
&&
|
||||
@@ -263,7 +264,8 @@ void to_json(nlohmann::json& j, const Spot_finding_settings& o)
|
||||
j["max_pix_per_spot"] = o.m_Max_pix_per_spot;
|
||||
if(o.highResolutionLimitIsSet())
|
||||
j["high_resolution_limit"] = o.m_High_resolution_limit;
|
||||
j["low_resolution_limit"] = o.m_Low_resolution_limit;
|
||||
if(o.lowResolutionLimitIsSet())
|
||||
j["low_resolution_limit"] = o.m_Low_resolution_limit;
|
||||
j["high_resolution_limit_for_spot_count_low_res"] = o.m_High_resolution_limit_for_spot_count_low_res;
|
||||
j["quick_integration"] = o.m_Quick_integration;
|
||||
j["ice_ring_width_q_recipA"] = o.m_Ice_ring_width_q_recipA;
|
||||
@@ -289,7 +291,11 @@ void from_json(const nlohmann::json& j, Spot_finding_settings& o)
|
||||
j.at("high_resolution_limit").get_to(o.m_High_resolution_limit);
|
||||
o.m_High_resolution_limitIsSet = true;
|
||||
}
|
||||
j.at("low_resolution_limit").get_to(o.m_Low_resolution_limit);
|
||||
if(j.find("low_resolution_limit") != j.end())
|
||||
{
|
||||
j.at("low_resolution_limit").get_to(o.m_Low_resolution_limit);
|
||||
o.m_Low_resolution_limitIsSet = true;
|
||||
}
|
||||
j.at("high_resolution_limit_for_spot_count_low_res").get_to(o.m_High_resolution_limit_for_spot_count_low_res);
|
||||
j.at("quick_integration").get_to(o.m_Quick_integration);
|
||||
j.at("ice_ring_width_q_recipA").get_to(o.m_Ice_ring_width_q_recipA);
|
||||
@@ -383,6 +389,15 @@ float Spot_finding_settings::getLowResolutionLimit() const
|
||||
void Spot_finding_settings::setLowResolutionLimit(float const value)
|
||||
{
|
||||
m_Low_resolution_limit = value;
|
||||
m_Low_resolution_limitIsSet = true;
|
||||
}
|
||||
bool Spot_finding_settings::lowResolutionLimitIsSet() const
|
||||
{
|
||||
return m_Low_resolution_limitIsSet;
|
||||
}
|
||||
void Spot_finding_settings::unsetLow_resolution_limit()
|
||||
{
|
||||
m_Low_resolution_limitIsSet = false;
|
||||
}
|
||||
float Spot_finding_settings::getHighResolutionLimitForSpotCountLowRes() const
|
||||
{
|
||||
|
||||
@@ -95,10 +95,12 @@ public:
|
||||
bool highResolutionLimitIsSet() const;
|
||||
void unsetHigh_resolution_limit();
|
||||
/// <summary>
|
||||
/// Low resolution limit for spot finding [Angstrom]
|
||||
/// Low resolution limit for spot finding [Angstrom]. Optional: if omitted, spot finding is not clipped at the low-resolution end. A value of 0 is accepted and means the same thing.
|
||||
/// </summary>
|
||||
float getLowResolutionLimit() const;
|
||||
void setLowResolutionLimit(float const value);
|
||||
bool lowResolutionLimitIsSet() const;
|
||||
void unsetLow_resolution_limit();
|
||||
/// <summary>
|
||||
/// High resolution threshold to consider spot \"low resolution\" [Angstrom]
|
||||
/// </summary>
|
||||
@@ -154,7 +156,7 @@ protected:
|
||||
float m_High_resolution_limit;
|
||||
bool m_High_resolution_limitIsSet;
|
||||
float m_Low_resolution_limit;
|
||||
|
||||
bool m_Low_resolution_limitIsSet;
|
||||
float m_High_resolution_limit_for_spot_count_low_res;
|
||||
|
||||
bool m_Quick_integration;
|
||||
|
||||
@@ -1038,7 +1038,6 @@ components:
|
||||
- photon_count_threshold
|
||||
- max_pix_per_spot
|
||||
- min_pix_per_spot
|
||||
- low_resolution_limit
|
||||
- quick_integration
|
||||
- high_resolution_limit_for_spot_count_low_res
|
||||
- ice_ring_width_q_recipA
|
||||
@@ -1079,7 +1078,9 @@ components:
|
||||
low_resolution_limit:
|
||||
type: number
|
||||
format: float
|
||||
description: Low resolution limit for spot finding [Angstrom]
|
||||
description: |
|
||||
Low resolution limit for spot finding [Angstrom]. Optional: if omitted, spot finding is not
|
||||
clipped at the low-resolution end. A value of 0 is accepted and means the same thing.
|
||||
high_resolution_limit_for_spot_count_low_res:
|
||||
type: number
|
||||
format: float
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -8,7 +8,6 @@
|
||||
#include "NetworkAddressConvert.h"
|
||||
#include "JFJochCompressor.h" // For ZSTD_USE_JFJOCH_RLE
|
||||
#include "DiffractionExperiment.h"
|
||||
#include "../image_analysis/bragg_prediction/BraggPrediction.h"
|
||||
|
||||
#include "CUDAWrapper.h"
|
||||
#include "JFJochException.h"
|
||||
@@ -612,23 +611,23 @@ void DiffractionExperiment::CheckDataProcessingSettings(const SpotFindingSetting
|
||||
check_min("Photon count threshold", settings.photon_count_threshold, 0);
|
||||
check_min("Minimum pixels per spot", settings.min_pix_per_spot.value_or(2), 1);
|
||||
check_min("Maximum pixels per spot", settings.max_pix_per_spot, settings.min_pix_per_spot.value_or(2) + 1);
|
||||
check_finite("Spot finding low resolution limit", settings.low_resolution_limit);
|
||||
|
||||
// An unset high-resolution limit means "as far as the detector reaches", so there is nothing to check.
|
||||
// A value that is present still has to be finite - NaN fails every comparison below, so testing it
|
||||
// inside the branch would let it through unchecked.
|
||||
// An unset limit means "no limit at that end", so there is nothing to check. A value that is present
|
||||
// still has to be finite - NaN fails every comparison below, so testing it inside the branch would
|
||||
// let it through unchecked.
|
||||
if (settings.high_resolution_limit.has_value())
|
||||
check_finite("Spot finding high resolution limit", *settings.high_resolution_limit);
|
||||
if (settings.high_resolution_limit.value_or(0.0f) > 0) {
|
||||
if (settings.low_resolution_limit.has_value())
|
||||
check_finite("Spot finding low resolution limit", *settings.low_resolution_limit);
|
||||
|
||||
if (settings.high_resolution_limit.has_value()) {
|
||||
check_min("Spot finding high resolution limit", *settings.high_resolution_limit, 0.5);
|
||||
check_max("Spot finding high resolution limit", *settings.high_resolution_limit, 50.0);
|
||||
if (settings.low_resolution_limit > 0) {
|
||||
check_min("Spot finding low resolution limit", settings.low_resolution_limit,
|
||||
if (settings.low_resolution_limit.has_value())
|
||||
check_min("Spot finding low resolution limit", *settings.low_resolution_limit,
|
||||
*settings.high_resolution_limit);
|
||||
}
|
||||
} else if (settings.low_resolution_limit > 0) {
|
||||
check_min("Spot finding low resolution limit", settings.low_resolution_limit, 1.0);
|
||||
check_max("Spot finding low resolution limit", settings.low_resolution_limit, 50.0);
|
||||
} else if (settings.low_resolution_limit.has_value()) {
|
||||
check_min("Spot finding low resolution limit", *settings.low_resolution_limit, 1.0);
|
||||
check_max("Spot finding low resolution limit", *settings.low_resolution_limit, 50.0);
|
||||
}
|
||||
|
||||
check_min("Ice ring width in Q-space (A^-1)", settings.ice_ring_width_Q_recipA, 0.0);
|
||||
@@ -1121,18 +1120,12 @@ int64_t DiffractionExperiment::GetImagesPerFile() const {
|
||||
|
||||
int64_t DiffractionExperiment::GetImageBufferLocationSize() const {
|
||||
// A buffer slot holds the compressed image plus the per-image CBOR metadata (spot list, reflection
|
||||
// list, azimuthal profile, ...). The reflection list dominates and is bounded by
|
||||
// BraggPrediction::kOnlineMaxReflections at ~170 B each - the ONLINE limit, not the offline one:
|
||||
// the slot size divides a fixed total buffer, so sizing every slot for the offline limit would cut
|
||||
// the slot count, and the receiver's ability to absorb a burst, by about three. Derived from that
|
||||
// constant
|
||||
// rather than restate it - the two used to be written down separately and a raised cap would have
|
||||
// silently outgrown the slot. Half again on top covers the capped spot list and the profile. A
|
||||
// frame whose metadata still does not fit is dropped individually by the receiver rather than
|
||||
// aborting the collection.
|
||||
constexpr int64_t kReflectionBytes = 170;
|
||||
const int64_t metadata = BraggPrediction::kOnlineMaxReflections * kReflectionBytes * 3 / 2;
|
||||
return GetMaxCompressedSize() + metadata;
|
||||
// list, azimuthal profile, ...). The reflection list is bounded by the ONLINE cap
|
||||
// BraggPrediction::kOnlineMaxReflections at ~170 B each, but it is not the whole of it: the capped
|
||||
// spot list and the three azimuthal arrays at up to 65534 bins add another megabyte, and the
|
||||
// measured worst case is ~2.9 MB. A frame whose metadata still does not fit is dropped
|
||||
// individually by the receiver rather than aborting the collection.
|
||||
return GetMaxCompressedSize() + kImageMetadataHeadroom;
|
||||
}
|
||||
|
||||
float DiffractionExperiment::GetLossyCompressionSerialMX() const {
|
||||
|
||||
@@ -234,6 +234,11 @@ public:
|
||||
int64_t GetMaxCompressedSize() const;
|
||||
int64_t GetImageBufferLocationSize() const;
|
||||
|
||||
// Room reserved in every image-buffer slot for the per-image CBOR metadata, on top of the
|
||||
// compressed image. CBORSerialize_Image_MetadataHeadroom holds it against the largest metadata
|
||||
// the serializer can produce, so change the two together.
|
||||
static constexpr int64_t kImageMetadataHeadroom = 4 * 1024 * 1024;
|
||||
|
||||
int64_t GetDataStreamsNum() const;
|
||||
int64_t GetModulesNum(uint16_t data_stream) const;
|
||||
int64_t GetModulesNum() const;
|
||||
|
||||
@@ -12,4 +12,13 @@ the result, follows Jon Wright (ESRF): "Experiences with GPU decompression for b
|
||||
data", HDF5 User Group meeting (2021), and [bslz4decoders](https://github.com/jonwright/bslz4decoders).
|
||||
The CUDA kernels in Jungfraujoch are its own, but the approach is his.
|
||||
|
||||
Spot extraction groups strong pixels into spots with the sparse connected-component labelling of the
|
||||
ACTS traccc project: P. Gessinger, H. M. Gray, A. Krasznahorkay, C. Leggett, J. Niermann,
|
||||
A. Salzburger, S. N. Swatman and B. Yeo, "traccc: GPU track reconstruction library for HEP
|
||||
experiments" (2025), [arXiv:2505.22822](https://arxiv.org/abs/2505.22822);
|
||||
[traccc](https://github.com/acts-project/traccc). The CPU spot extractor adapts its SparseCCL source,
|
||||
and the CUDA spot extractor follows the design of its GPU counterpart - a backward-neighbour graph
|
||||
over a sorted hit list, resolved by a parallel union-find. traccc is MPL-2.0; see
|
||||
[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md).
|
||||
|
||||
This software uses Viridis, Magma and Inferno colormaps from Matplotlib under its BSD-compatible license
|
||||
|
||||
+37
-31
@@ -3,57 +3,63 @@
|
||||
### 1.0.0-rc.161
|
||||
This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use.
|
||||
|
||||
* Spot finding: Self-calibrating **adaptive detection** added, now the **default** for rotation as well as stills - no per-dataset threshold tuning; a fused GPU engine runs it together with azimuthal integration in one image pass.
|
||||
* Spot finding: Serial stills pick `--min-pix-per-spot` per image; connected components run on the GPU; detection is configurable over the API, where a `high_resolution_limit` of 0 means "no limit" instead of throwing.
|
||||
* Resolution limits: Bragg integration, azimuthal integration and spot finding all default to **as far as the detector reaches**, replacing a fixed 1.0 Å integration limit and a 1.5 Å rotation spot-finding limit that silently discarded everything beyond them; `--integration-high-resolution` and `--spot-high-resolution` still set one by hand.
|
||||
* Bragg prediction: How far the predictor walks the lattice is a setting (`bragg_integration_settings.max_hkl`) instead of a fixed 100, derived per crystal offline from the refined cell (`--max-hkl` overrides); the broker keeps a fixed bootstrap so a live acquisition has a predictable per-image cost.
|
||||
* Bragg integration: The local background ring is now made robust with a **high-side sigma clip** (`--background-clip <n>`, default 4) instead of the symmetric trimmed mean, which is biased low on Poisson data and added ~5 counts to every partial. The trim stays reachable with `--background-trim <f>`. Expect `<I/sigma>` to fall and edge `R_meas` to rise - that is the removed bias, not a regression; per-shell agreement with independent processing improves.
|
||||
* Bragg integration: The **uncertainty of the background estimate** is now propagated into `sigma`; both engines omitted it, which understated every reflection's `sigma` by `sqrt(1 + n_signal/n_bkg)` = 1.109 with the shipped stencil. Expect `<I/sigma>` to fall by that factor on every dataset.
|
||||
* Bragg integration: New **radial background correction** (`--background-radial[=on|off|auto]`, default off) for the bias a curved radial background leaves in a flat ring mean - tens of counts on a single reflection sitting on an ice ring. `auto` applies it per image where that image's ice score shows a *smooth* powder ring, which targets correctly (it fires on ten battery crystals, all ice-positive) but costs 1.35x the wall clock and is sign-mixed on the merge statistics, so it stays opt-in: measured against a fixed atomic model it removes 43 % of the ice bands' excess amplitude on smooth ice, but on ice made of discrete crystallite spots - where a radius-only background model has nothing to fit - it makes the bias worse, and the gate separates the two.
|
||||
* rugnux: **Ice-ring handling now runs only when the crystal is measured to have ice** - the eleven fixed bands cost 16-26 % of the unique reflections whether it does or not. Detection uses two channels, the spot finder's peak-excluded radial profile (`--ice-min-score`, default 1.5) and found spots on the rings against ice-free flanks (`--ice-min-spot-ratio`, default 2.0), which catch smooth and textured ice respectively; the previous score was read off the plain azimuthal profile, where strong reflections in a ring's bin score as ice.
|
||||
* rugnux: The merge-time **ice-ring mask has been removed**. Measured against anomalous peak height it returned -0.001 +- 0.018 sigma while deleting reflections whose mean `I/sigma` was *above* the dataset average, leaving `R_meas`, CC1/2 and ISa unchanged and the affected shell 17 completeness points short. Ice reflections are still kept out of the scale fit and the space-group search, as every established scaling program does, and are kept in the final merge.
|
||||
* rugnux: **Multiplicity was over-reported** by the rotation merge: the per-shell observation counter rode on the R_meas re-walk, which deliberately applies a wider filter than the merge, so observations the merge had excluded were counted against a unique count that excluded them - and a group the merge dropped entirely has a NaN resolution, which `ResolutionShells::GetShell` silently binned as the lowest-resolution shell rather than rejecting. Only counts were affected; intensities, sigmas, R_meas, CC1/2, completeness and ISa were always right.
|
||||
* rugnux: **The first-pass rotation spots are now found rather than taken from the file**. `--redo-rotation-spots` (now the behaviour) and the reuse path are removed. The stored spots were the acquisition's, found at its threshold with its ice-band entries already dropped by its spot budget, so the lattice search never saw the spot-finding settings. Costs ~3 % of the run; over the rotation battery `R_meas` improves on 9 of the 10 crystals that move materially.
|
||||
* rugnux: Rotation indexing **no longer keeps a metric symmetry that indexes almost nothing**. The Bravais class is decided from the unrefined FFT candidate against a fixed 3° tolerance, so a lattice pseudo-symmetric to a few tenths of a degree is promoted a class too far and the constraint then snaps a real angle to the ideal one - measured, one crystal's promoted cell indexed 2 of 60 validation frames where its own primitive cell indexed 39, and the run died. The first pass now drops such a promotion. Battery: 33/37 space groups matching XDS with one hard failure becomes 34/37 with none, every other crystal identical.
|
||||
* rugnux: New **detector-calibration mode** (`--mode calibration`) - fit PONI x/y, the two tilts and the distance to a calibrant's powder rings over the whole dataset and write a pyFAI `.poni` file. `--calibrant lab6|agbh|ceo2|si|ice` (ice calibrates a real experiment against its own ice rings, so no calibrant exposure is needed); `--calibration rings` (default) sums the azimuthally-binned profile over every image and fits the ring arcs in it, `--calibration spots` pools the found spots instead. Measured on a LaB6 distance series, both routes land within 0.1 px of an independent reference, and on a crystal with smooth ice the beam centre moves from 5.5 px off to 0.3 px.
|
||||
* Spot finding: Self-calibrating **adaptive detection** added, now the **default** for rotation as well as stills; a fused GPU engine runs it together with azimuthal integration in one image pass.
|
||||
* Spot finding: Serial stills pick `--min-pix-per-spot` per image, connected components run on the GPU, and detection is configurable over the API.
|
||||
* Spot finding: `--spot-sigma` now defaults to 4.0 (was 3.0) and `--max-spots` to 1000 spots per image (was 250).
|
||||
* Resolution limits: Bragg integration, azimuthal integration and spot finding all default to **as far as the detector reaches**; `--integration-high-resolution` and `--spot-high-resolution` still set one by hand, and 0 means "no limit" at either end.
|
||||
* Bragg prediction: How far the predictor walks the lattice is a setting (`bragg_integration_settings.max_hkl`, `--max-hkl`) instead of a fixed 100, derived per crystal offline and held fixed online.
|
||||
* Bragg integration: The local background ring is made robust with a **high-side sigma clip** (`--background-clip <n>`, default 4) instead of the symmetric trimmed mean; the trim stays reachable with `--background-trim <f>`.
|
||||
* Bragg integration: The **uncertainty of the background estimate** is now propagated into `sigma`, which both engines previously omitted.
|
||||
* Bragg integration: New **radial background correction** (`--background-radial[=on|off|auto]`, default off); `auto` applies it per image where that image's ice score shows a smooth powder ring.
|
||||
* rugnux: **Ice-ring handling now runs only when the crystal is measured to have ice**, detected on two channels - the spot finder's peak-excluded radial profile (`--ice-min-score`, default 1.5) and found spots on the rings against ice-free flanks (`--ice-min-spot-ratio`, default 2.0).
|
||||
* rugnux: The merge-time **ice-ring mask has been removed**; ice reflections are still kept out of the scale fit and the space-group search, and kept in the final merge.
|
||||
* rugnux: `spot_finding_settings.ice_ring_width_q_recipA` now defaults to 0.03 1/Å (was 0.02), matching the offline default.
|
||||
* rugnux: **Multiplicity is no longer over-reported** by the rotation merge; only the counts were affected, never intensities, sigmas, `R_meas`, CC1/2, completeness or ISa.
|
||||
* rugnux: **The first-pass rotation spots are now found rather than taken from the file**; `--redo-rotation-spots` (now the behaviour) and the reuse path are removed.
|
||||
* rugnux: Rotation indexing **no longer keeps a metric symmetry that indexes almost nothing**.
|
||||
* rugnux: New **detector-calibration mode** (`--mode calibration`) fits PONI x/y, the two tilts and the distance to a calibrant's powder rings over the whole dataset and writes a pyFAI `.poni` file, with `--calibrant lab6|agbh|ceo2|si|ice` and `--calibration rings|spots` (default `rings`).
|
||||
* Powder calibration: The face-centred calibrants (`ceo2`, `si`) no longer list their systematically absent rings, so the fit starts from (111) rather than the extinct (100), and the triclinic `agbh` ring list covers both signs of h and k instead of one octant.
|
||||
* rugnux: `--polarization` now reaches `--mode azint`, which parsed the flag and then discarded it; that mode also applies the same polarization default as every other mode.
|
||||
* rugnux: `--stride` no longer mis-links the images in the written `_process.h5` - image *i* of the file now carries source image *start + i x stride*, not *start + i*, so each frame's picture sits next to its own analysis. The per-image arrays are also sized correctly when the stride does not divide the range evenly.
|
||||
* Spot finding: `--spot-low-resolution 0` (and an omitted `low_resolution_limit` over the API) means "no low-resolution limit", mirroring the high-resolution end.
|
||||
* rugnux: **`--mode` replaces `--azint-only` and `--scale`** (see below).
|
||||
* jfjoch_viewer: **"Analyze dataset" can run the detector calibration** over the whole dataset. The settings panel's MX/AzInt toggle gains a third *Calib* page carrying the calibrant (now including `ice`, matching the CLI) and the rings/spots method; the run writes a `.poni` next to the other outputs and reports the fitted PONI, tilts and distance against the header values in their own window.
|
||||
* Powder calibration: the ring geometry fit can now read its rings off an **azimuthally-binned profile summed over a run** instead of a spot list from one image (`RingsFromAzimuthalProfile`). A powder ring is an arc, not a set of spots, and its roundness fixes the beam centre without reference to the calibrant's d-spacings or the detector distance - the one parameter Bragg data constrain worst. The extraction window is capped at half the gap to the neighbouring ring (hexagonal ice has three rings within 0.06 1/A, which a fixed window merged into one), and where only one ring is in reach the tilts are held rather than fitted, since a single ring cannot separate them from the centre.
|
||||
* jfjoch_viewer: **"Analyze dataset" can run the detector calibration** over the whole dataset, from a new *Calib* page in the settings panel; the run writes a `.poni` next to the other outputs and reports the fitted PONI, tilts and distance against the header values.
|
||||
* Powder calibration: the ring geometry fit can now read its rings off an **azimuthally-binned profile summed over a run** instead of a spot list from one image, and holds the tilts rather than fitting them where only one ring is in reach.
|
||||
* Powder calibration: `.poni` files write `Poni1`/`Poni2` as (beam + 0.5) x pixel size, matching pyFAI's edge-of-sensor origin; the pixel-coordinate convention is documented in `docs/DETECTOR_GEOMETRY.md`.
|
||||
* jfjoch_viewer: the beam-centre cross is aligned with the spot and prediction markers.
|
||||
* rugnux: De-novo **space-group search** substantially more robust - centering ranked by net absences and judged on absent-class strength, merohedral-twin over-promotion vetoed, and genuine high-symmetry groups recovered on weak data.
|
||||
* rugnux: The space-group search takes systematic absences from the merge of all observations, needs at least three control reflections on an axial row to claim a **screw axis**, and no longer alters the production merge.
|
||||
* rugnux: The space-group search no longer **starves on a low-ISa merge**: its fixed `I/sigma >= 3` cut could select nothing at all, since a merged sigma is floored so that no reflection reads above `ISa = 1/b`, leaving every operator correlation undefined and the point group at 1. The cut is now capped at the merge's own quantile, and is unchanged on healthy merges.
|
||||
* rugnux: The one-line summary **names every space group the data cannot separate** (`I23 (No. 197) or I2(1)3 (No. 199) - indistinguishable from these data`), instead of reporting only the representative as if it had been measured.
|
||||
* rugnux: The space-group search no longer **starves on a low-ISa merge** - its `I/sigma` cut is capped at the merge's own quantile, and is unchanged on healthy merges.
|
||||
* rugnux: The one-line summary **names every space group the data cannot separate** (`I23 (No. 197) or I2(1)3 (No. 199) - indistinguishable from these data`), instead of reporting only the representative.
|
||||
* rugnux: The per-image mosaicity is fitted from the strongest 250 spots, so the indexing spot budget no longer sets it.
|
||||
* rugnux: The per-frame crystal orientation is smoothed before scaling and partiality recomputed from it, so per-image refinement noise no longer reaches the merged intensities. Frames that never indexed are now excluded from that smoothing - their all-zero lattice passed the validity check and was averaged into their neighbours', which on a partly-indexing crystal chose a smoothing window nine times too wide.
|
||||
* rugnux: Rotation post-refinement **reports the goniometer rotation scale** and warns beyond 0.5 %. A stage that turns further than commanded is invisible in the file, since the stored angles are the commanded ones, and reads as crystal drift instead. Report only - nothing is corrected.
|
||||
* rugnux: Stills **partiality post-refinement** added, on by default (`--simple-stills` disables); several non-helping stills scaling/detection knobs removed.
|
||||
* rugnux: The per-frame crystal orientation is smoothed before scaling and partiality recomputed from it, with frames that never indexed excluded from the smoothing.
|
||||
* rugnux: Rotation post-refinement **reports the goniometer rotation scale** and warns beyond 0.5 %; nothing is corrected.
|
||||
* rugnux: Stills **partiality post-refinement** added, on by default (`--simple-stills` disables); `--still-partiality`, `--partiality-uncertainty` and several other non-helping stills knobs are removed.
|
||||
* rugnux: **Scaling** hardened against a collapsed per-frame scale on the stills path as well as rotation, and the merged-sigma systematic floor is kept when ISa is too degenerate to report.
|
||||
* rugnux: `--min-image-cc` now works for rotation data (opt-in); new `--search-min-zeta` (rotation default 0.85); reports how close a symmetry axis lies to the spindle.
|
||||
* Performance: bitshuffle+LZ4 images are **decoded on the GPU** rather than on the host (1.8x on a large rotation run), with the bitshuffle inverse fused into preprocessing so the decompressed frame is never held in device memory (0.62 -> 0.41 ms/frame at 18 Mpx, ~144 MB less per worker); first-pass rotation indexing refines candidate cells in parallel instead of on one thread per scheme.
|
||||
* GPU decoding: a corrupt or truncated chunk was decoded as far as it went, leaving the rest of the frame holding the previous image, so the decode is now checked and reported instead; bitshuffle blocks larger than 48 kB decode rather than failing, and anything the device decoder cannot handle falls back to host decompression instead of stopping the run.
|
||||
* Performance: bitshuffle+LZ4 images are **decoded on the GPU** rather than on the host, with the bitshuffle inverse fused into preprocessing so the decompressed frame is never held in device memory; first-pass rotation indexing refines candidate cells in parallel instead of on one thread per scheme.
|
||||
* GPU decoding: a corrupt or truncated chunk is now detected and reported instead of leaving the rest of the frame holding the previous image; bitshuffle blocks larger than 48 kB decode rather than failing, and anything the device decoder cannot handle falls back to host decompression.
|
||||
* Performance: Assorted work removed from the hot paths - GPU lookup tables shared per device, fewer device allocations, cheaper candidate filtering and merged-file writing; jfjoch_viewer image rendering and interaction improved.
|
||||
* Reader: A stored dataset is read as **int32**, so the saturation cap comes from the file's own `saturation_value` (32-bit files were capping at 32767 and dropping their strongest reflections; 16- and 8-bit files at half their range).
|
||||
* GPU: Spot finding, merging and Bragg prediction now match the CPU path and reproduce run to run. Adaptive detection sums its per-ring statistics in integers, so the threshold no longer depends on the order the GPU happens to schedule the reduction in and the two engines agree exactly.
|
||||
* rugnux: Stills geometry refinement took frames from a shared cursor and stopped on a shared counter, so the refined geometry varied between runs and with `-N` and was fitted to the start of the run; it now draws fixed stripes spanning the whole run.
|
||||
* Reader: A stored dataset is read as **int32**, so the saturation cap comes from the file's own `saturation_value` rather than from the stored bit depth.
|
||||
* GPU: Spot finding, merging and Bragg prediction now match the CPU path and reproduce run to run; adaptive detection sums its per-ring statistics in integers, so the two engines agree exactly.
|
||||
* rugnux: Stills geometry refinement draws fixed stripes spanning the whole run, so the refined geometry no longer varies between runs or with `-N`.
|
||||
* rugnux: **Geometry refinement** no longer commits a solve that did not converge, and per-image refinement weights spots by confidence.
|
||||
* Lattice search: Corrected the reindex matrices for two Niggli characters.
|
||||
* Azimuthal integration: A flat ring no longer yields a NaN standard deviation, and a low-Q limit above the maximum is rejected instead of producing undefined behaviour.
|
||||
* rugnux: Bad command-line input is **reported** (exit 1) instead of terminating the process, and a run in which nothing indexes still writes a `_process.h5`.
|
||||
* Broker / writer / image pusher: Image-slot lifetime, locking and deactivation fixes; tearing a TCP connection down is serialised per connection; and per-image datasets are sized by the images actually written, so a frame that failed mid-run no longer truncated the last image out of the file.
|
||||
* rugnux / broker: rugnux exited before reading an image on hosts with more than 128 hardware threads, so the refinement thread count is clamped; a config without a `bragg_integration` block now gets the documented fixed `max_hkl` rather than deriving it per crystal; and the device decode time is reported as compression time again, so that plot and `/entry/profiling/compressionTime` are populated.
|
||||
* Packaging: The Debian/Ubuntu broker package is named `jfjoch`, not `jfjoch-jfjoch` (it declares the rename, so `apt upgrade` handles it); the DKMS driver package's `postinst`/`prerm` are executable, so the `.deb` runs them; and the Windows/macOS installers no longer carry the Linux-only Freedesktop menu entry and icon.
|
||||
* jfjoch_writer: `--verbose` no longer swallows the following argument, and the usage message names the root directory as a path. The `-H`/`--http_port` option has done nothing since the HTTP status interface was dropped.
|
||||
* Build: `PROJECT_VERSION` is derived from the `VERSION` file rather than hardcoded, and the version is read from it once instead of in two places; and the Bragg integrator no longer uses `M_PI`, which is not standard C++ and left `image_analysis` - which the Windows viewer builds - failing to compile under MSVC.
|
||||
* Docs: Document device-side bitshuffle+LZ4 image decoding in CPU_DATA_ANALYSIS.md; add a Release contents page (what each artefact ships, its CPU baseline, and how the CUDA builds behave without a GPU); correct the RHEL 9 repository URLs and package names; bring the rugnux (`--model`), viewer (processing jobs) and writer pages up to date.
|
||||
* rugnux / broker: The refinement thread count is clamped, so rugnux starts on hosts with more than 128 hardware threads; a config without a `bragg_integration` block gets the documented fixed `max_hkl`; and the device decode time is reported as compression time again, so that plot and `/entry/profiling/compressionTime` are populated.
|
||||
* Packaging: The Debian/Ubuntu broker package is named `jfjoch`, not `jfjoch-jfjoch` (it declares the rename, so `apt upgrade` handles it); the DKMS driver package's `postinst`/`prerm` are executable; and the Windows/macOS installers no longer carry the Linux-only Freedesktop menu entry and icon.
|
||||
* jfjoch_writer: `--verbose` no longer swallows the following argument, and the usage message names the root directory as a path; `-H`/`--http_port` has done nothing since the HTTP status interface was dropped.
|
||||
* Build: `PROJECT_VERSION` is derived from the `VERSION` file rather than hardcoded, and the Bragg integrator no longer uses `M_PI`, which left `image_analysis` failing to compile under MSVC.
|
||||
* Docs: New Release contents page; device-side bitshuffle+LZ4 image decoding documented in CPU_DATA_ANALYSIS.md; corrected RHEL 9 repository URLs and package names; the rugnux, viewer and writer pages brought up to date.
|
||||
|
||||
**Breaking change to the rugnux command line:**
|
||||
* `--azint-only` and `--scale` are **removed**, replaced by `--mode azint` and `--mode scale`; the full pipeline is `--mode mx` and remains the default. A script passing the old flags now fails with the list of valid modes rather than silently running the wrong one.
|
||||
|
||||
**Breaking changes to OpenAPI** - regenerate the client (`jfjoch-client` 1.0.0-rc.161, `frontend/src/client`) or read the affected fields as optional:
|
||||
* `image_scale_b` is removed from the `plot_type` enum, so a client requesting that plot now gets an error rather than a curve.
|
||||
* `azim_int_settings.high_q_recipA` and `spot_finding_settings.high_resolution_limit` are no longer `required`. Both mean "as far as the detector reaches" when unset and are omitted from the response instead of carrying a placeholder value, which raises in a client generated from an rc.160-or-earlier spec.
|
||||
* `azim_int_settings.high_q_recipA`, `spot_finding_settings.high_resolution_limit` and `spot_finding_settings.low_resolution_limit` are no longer `required`. All three mean "no limit at that end" when unset and are omitted from the response instead of carrying a placeholder value, which raises in a client generated from an rc.160-or-earlier spec. A value of 0 is still accepted and means the same thing.
|
||||
|
||||
**Breaking changes to the stored formats** - a consumer reading these fields must treat them as optional:
|
||||
* The per-image image-scale B factor is no longer computed, so `/entry/MX/imageScaleBFactor` is absent from newly written HDF5 files and the corresponding key is absent from the CBOR DataMessage and END blocks. Files written by rc.160 and earlier still contain it and still open; nothing in the pipeline reads it any more.
|
||||
|
||||
+1
-1
@@ -260,7 +260,7 @@ Spot finding:
|
||||
| `--no-adaptive-spots` | Turn adaptive detection off and use the fixed `--spot-threshold` / `--spot-sigma` finder |
|
||||
| `--spot-false-pixels <num>` | Adaptive-detection operating point: expected noise pixels tolerated per frame (default: 100; implies `--adaptive-spots`) |
|
||||
| `--spot-high-resolution <num>` | High-resolution limit for spot finding, Å. Omitted (or 0): no resolution clipping — spot finding extends as far as the detector reaches, for rotation data as well as stills |
|
||||
| `--spot-low-resolution <num>` | Low-resolution limit for spot finding, Å (default: 50; lower it, e.g. 24, to exclude the direct-beam halo on weak serial data) |
|
||||
| `--spot-low-resolution <num>` | Low-resolution limit for spot finding, Å (default: 50; lower it, e.g. 24, to exclude the direct-beam halo on weak serial data; 0 removes the limit) |
|
||||
| `--min-pix-per-spot <num>` | Minimum connected strong pixels per spot. **If omitted, min-pix is chosen per image** (stills indexing): the frame is indexed at min-pix 3/2/1 and the one maximising indexed-spot count × indexed fraction is kept. Give an explicit value to force a fixed min-pix instead. |
|
||||
| `--max-spots <num>` | Maximum spots kept per image (the strongest ones) and handed to indexing (default: 1000) |
|
||||
| `--detect-ice-rings[=on\|off]` | Flag ice-ring spots (de-prioritised in indexing) and exclude ice-ring reflections from scaling. Default: the master file's `detect_ice_rings`, or — where the file carries no such key — **on for rotation and off for stills** |
|
||||
|
||||
@@ -53,6 +53,7 @@ These are copied into the source tree (see the path) rather than fetched.
|
||||
| [LZ4](https://github.com/lz4/lz4) | `compression/lz4/` | Yann Collet | BSD-2-Clause | [lz4.txt](https://gitea.psi.ch/mx/jungfraujoch/src/branch/main/licenses/lz4.txt) |
|
||||
| [HLS arbitrary-precision types](https://github.com/Xilinx/HLS_arbitrary_Precision_Types) | `fpga/include/` | Xilinx, Inc. | Apache-2.0 | [xilinx-hls-headers.txt](https://gitea.psi.ch/mx/jungfraujoch/src/branch/main/licenses/xilinx-hls-headers.txt) |
|
||||
| [GEMMI](https://github.com/project-gemmi/gemmi) | `gemmi_gph/` | Global Phasing Ltd. | MPL-2.0 | [gemmi.txt](https://gitea.psi.ch/mx/jungfraujoch/src/branch/main/licenses/gemmi.txt) |
|
||||
| [traccc (ACTS)](https://github.com/acts-project/traccc) | `image_analysis/spot_finding/StrongPixelSet.cpp`, `SpotExtractorGPU.cu` | CERN, for the benefit of the ACTS project | MPL-2.0 | [traccc.txt](https://gitea.psi.ch/mx/jungfraujoch/src/branch/main/licenses/traccc.txt) |
|
||||
| [xbflash.qspi](https://github.com/Xilinx/XRT) | `tools/xbflash.qspi/` | Xilinx / AMD | Apache-2.0 | [xbflash-qspi.txt](https://gitea.psi.ch/mx/jungfraujoch/src/branch/main/licenses/xbflash-qspi.txt) |
|
||||
| [wingetopt](https://github.com/alex85k/wingetopt) | `tools/wingetopt/` | Todd C. Miller; The NetBSD Foundation | ISC AND BSD-2-Clause | [wingetopt.txt](https://gitea.psi.ch/mx/jungfraujoch/src/branch/main/licenses/wingetopt.txt) |
|
||||
|
||||
@@ -79,9 +80,14 @@ served frontend, so the shipped web UI carries its own attribution.
|
||||
|
||||
## Notes on weak-copyleft and attribution-sensitive components
|
||||
|
||||
* **MPL-2.0** (Eigen, GEMMI, libzmq): file-level copyleft. GEMMI is vendored in `gemmi_gph/` in
|
||||
trimmed form; libzmq is fetched at build time; Eigen is provided externally (header-only). The
|
||||
* **MPL-2.0** (Eigen, GEMMI, libzmq, traccc): file-level copyleft. GEMMI is vendored in `gemmi_gph/`
|
||||
in trimmed form; libzmq is fetched at build time; Eigen is provided externally (header-only). The
|
||||
corresponding source is available from each project upstream.
|
||||
* **traccc** is the one entry that is not a vendored directory. Its sparse connected-component
|
||||
labelling enters two otherwise first-party files: `StrongPixelSet.cpp` adapts the SparseCCL source,
|
||||
and `SpotExtractorGPU.cu` follows the design of its GPU counterpart. MPL-2.0 is file-level, so both
|
||||
files name the origin at the top and are covered by `licenses/traccc.txt`. See
|
||||
[ACKNOWLEDGEMENT.md](ACKNOWLEDGEMENT.md) for the citation.
|
||||
* **FFTW** is GPL-2.0-or-later — compatible with, and absorbed by, this project's GPL-3.0 license.
|
||||
* **Apache-2.0** components: where upstream ships a `NOTICE` file, it is reproduced in the
|
||||
corresponding `licenses/` text.
|
||||
|
||||
@@ -12,7 +12,7 @@ Name | Type | Description | Notes
|
||||
**min_pix_per_spot** | **int** | |
|
||||
**max_pix_per_spot** | **int** | |
|
||||
**high_resolution_limit** | **float** | High resolution limit for spot finding [Angstrom]. Optional: if omitted, spot finding extends as far as the detector reaches, i.e. the detection is not clipped in resolution. | [optional]
|
||||
**low_resolution_limit** | **float** | Low resolution limit for spot finding [Angstrom] |
|
||||
**low_resolution_limit** | **float** | Low resolution limit for spot finding [Angstrom]. Optional: if omitted, spot finding is not clipped at the low-resolution end. A value of 0 is accepted and means the same thing. | [optional]
|
||||
**high_resolution_limit_for_spot_count_low_res** | **float** | High resolution threshold to consider spot \"low resolution\" [Angstrom] |
|
||||
**quick_integration** | **bool** | Quick integration of Bragg spots in diffraction images. If enabled it will likely reduce performance of Jungfraujoch for datasets with a very high indexing rate. (experimental feature) | [default to False]
|
||||
**ice_ring_width_q_recip_a** | **float** | Half-width of the ice ring band in q (1/A). Matches the offline default in image_analysis/spot_finding/SpotFindingSettings.h, which was set from a measured ring FWHM of ~0.06; the two must agree or the same data gets a narrower ice band online. | [default to 0.03]
|
||||
|
||||
@@ -1480,8 +1480,16 @@ namespace {
|
||||
message.rotation_extra_lattices.emplace_back(tmp);
|
||||
}
|
||||
cborErr(cbor_value_leave_container(&value, &array_value));
|
||||
} else
|
||||
} else {
|
||||
// Skip a key this build does not know, so an older reader survives a newer sender.
|
||||
// A tag takes two advances - the first lands ON the tagged item, not past it - and the
|
||||
// typed arrays in this block are all tagged, so one advance would leave the parser
|
||||
// sitting on the payload and the next key would be read as a string. Same as the image
|
||||
// block above.
|
||||
if (cbor_value_is_tag(&value))
|
||||
cbor_value_advance(&value);
|
||||
cbor_value_advance(&value);
|
||||
}
|
||||
return true;
|
||||
} catch (const JFJochException &e) {
|
||||
throw JFJochException(JFJochExceptionCategory::CBORError,
|
||||
|
||||
@@ -718,9 +718,11 @@ export type spot_finding_settings = {
|
||||
*/
|
||||
high_resolution_limit?: number;
|
||||
/**
|
||||
* Low resolution limit for spot finding [Angstrom]
|
||||
* Low resolution limit for spot finding [Angstrom]. Optional: if omitted, spot finding is not
|
||||
* clipped at the low-resolution end. A value of 0 is accepted and means the same thing.
|
||||
*
|
||||
*/
|
||||
low_resolution_limit: number;
|
||||
low_resolution_limit?: number;
|
||||
/**
|
||||
* High resolution threshold to consider spot "low resolution" [Angstrom]
|
||||
*/
|
||||
|
||||
@@ -301,7 +301,7 @@ export const zSpotFindingSettings = z.object({
|
||||
min_pix_per_spot: z.coerce.bigint().gte(BigInt(1)).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }),
|
||||
max_pix_per_spot: z.coerce.bigint().gte(BigInt(1)).max(BigInt('9223372036854775807'), { error: 'Invalid value: Expected int64 to be <= 9223372036854775807' }),
|
||||
high_resolution_limit: z.number().optional(),
|
||||
low_resolution_limit: z.number(),
|
||||
low_resolution_limit: z.number().optional(),
|
||||
high_resolution_limit_for_spot_count_low_res: z.number().gte(2).lte(8),
|
||||
quick_integration: z.boolean().default(false),
|
||||
ice_ring_width_q_recipA: z.number().gte(0).lte(1).default(0.03),
|
||||
|
||||
@@ -331,12 +331,14 @@ void MXAnalysisWithoutFPGA::RunROIOnly(DataMessage &output) {
|
||||
void MXAnalysisWithoutFPGA::UpdateMaskResolution(const SpotFindingSettings &settings) {
|
||||
mask_low_res = settings.low_resolution_limit;
|
||||
mask_high_res = settings.high_resolution_limit;
|
||||
// No high-resolution limit requested -> mask nothing at the high-resolution end: no pixel has d < 0,
|
||||
// and the detector's own edge is where the pixels stop anyway.
|
||||
// An unset limit masks nothing at that end. At the high-resolution end 0 does that on its own - no
|
||||
// pixel has d < 0, and the detector's own edge is where the pixels stop anyway; at the low-resolution
|
||||
// end every pixel lies above any finite stand-in, so it takes an infinite one.
|
||||
const float high_res = mask_high_res.value_or(0.0f);
|
||||
const float low_res = mask_low_res.value_or(INFINITY);
|
||||
auto const &resolution_map = integration.Resolution();
|
||||
for (int i = 0; i < mask_resolution.size(); i++)
|
||||
mask_resolution[i] = (resolution_map[i] > mask_low_res) || (resolution_map[i] < high_res);
|
||||
mask_resolution[i] = (resolution_map[i] > low_res) || (resolution_map[i] < high_res);
|
||||
|
||||
// The finders keep their own copy (the GPU ones a bit-packed device copy), so the mask is handed
|
||||
// over here - when the limits change - rather than with every image.
|
||||
|
||||
@@ -61,7 +61,7 @@ class MXAnalysisWithoutFPGA {
|
||||
// The limits mask_resolution was built for. Kept as the OPTIONAL the caller passed, so an unset
|
||||
// high-resolution limit compares equal to itself and the mask is not rebuilt on every image.
|
||||
std::optional<float> mask_high_res;
|
||||
float mask_low_res;
|
||||
std::optional<float> mask_low_res;
|
||||
void UpdateMaskResolution(const SpotFindingSettings& settings);
|
||||
#ifdef JFJOCH_USE_CUDA
|
||||
std::shared_ptr<CudaStream> stream; // kept so RebuildROI() can recreate the GPU ROI engine
|
||||
|
||||
@@ -151,7 +151,23 @@ std::vector<RingClusters> AnalyzeClusters(const std::vector<float>& r, const std
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::vector<float> CalculateXtalRings(const UnitCell &cell, int hkl_max) {
|
||||
namespace {
|
||||
bool reflection_present(ReflectionCondition condition, int h, int k, int l) {
|
||||
const bool all_odd = (h % 2 != 0) && (k % 2 != 0) && (l % 2 != 0);
|
||||
const bool all_even = (h % 2 == 0) && (k % 2 == 0) && (l % 2 == 0);
|
||||
switch (condition) {
|
||||
case ReflectionCondition::FaceCentred:
|
||||
return all_odd || all_even;
|
||||
case ReflectionCondition::Diamond:
|
||||
return all_odd || (all_even && ((h + k + l) % 4 == 0));
|
||||
case ReflectionCondition::All:
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<float> CalculateXtalRings(const UnitCell &cell, ReflectionCondition condition, int hkl_max) {
|
||||
CrystalLattice latt(cell);
|
||||
|
||||
Coord Astar = latt.Astar();
|
||||
@@ -159,10 +175,14 @@ std::vector<float> CalculateXtalRings(const UnitCell &cell, int hkl_max) {
|
||||
Coord Cstar = latt.Cstar();
|
||||
|
||||
std::vector<float> u;
|
||||
for (int h = 0; h <= hkl_max; h++) {
|
||||
for (int k = 0; k <= hkl_max; k++) {
|
||||
// Both signs of h and k: only for a diagonal metric does |h a* + k b*| equal |h a* - k b*|, so on a
|
||||
// triclinic cell (silver behenate) the positive octant alone misses more rings than it finds. l stays
|
||||
// non-negative because hkl and -h-k-l are the same ring.
|
||||
for (int h = -hkl_max; h <= hkl_max; h++) {
|
||||
for (int k = -hkl_max; k <= hkl_max; k++) {
|
||||
for (int l = 0; l <= hkl_max; l++) {
|
||||
if (h == 0 && k == 0 && l == 0) continue;
|
||||
if (!reflection_present(condition, h, k, l)) continue;
|
||||
auto p = Astar * h + Bstar * k + Cstar * l;
|
||||
float Q = 2.0f * PI * p.Length();
|
||||
u.push_back(Q);
|
||||
@@ -181,7 +201,7 @@ std::vector<float> CalculateXtalRings(const UnitCell &cell, int hkl_max) {
|
||||
|
||||
|
||||
std::vector<float> CalculateCubicXtalRings(float a, int hkl_max) {
|
||||
return CalculateXtalRings(UnitCell(a,a,a,90,90,90), hkl_max);
|
||||
return CalculateXtalRings(UnitCell(a,a,a,90,90,90), ReflectionCondition::All, hkl_max);
|
||||
}
|
||||
|
||||
float GuessDetectorDistance(const DiffractionGeometry& geom, float ring_radius_pxl, float d_A) {
|
||||
|
||||
@@ -28,7 +28,20 @@ FindCircleCenterResult FindCircleCenter(const std::vector<SpotToSave> &v,
|
||||
|
||||
std::vector<std::vector<int>> ClusterSpotsIntoRings(const std::vector<float>& r, float eps = 1.5, int minPts = 5);
|
||||
std::vector<RingClusters> AnalyzeClusters(const std::vector<float>& r, const std::vector<std::vector<int>> &clusters);
|
||||
std::vector<float> CalculateXtalRings(const UnitCell &cell, int hkl_max = 6);
|
||||
|
||||
// Which hkl a lattice actually diffracts into. A cell alone does not say - the centring and any glide
|
||||
// plane do - and a ring that is systematically absent is worse than a missing one, because the fit
|
||||
// pairs the innermost OBSERVED ring with the innermost listed one. Only the conditions the calibrants
|
||||
// need are here; a cell of unknown symmetry (the viewer's "current sample") takes All.
|
||||
enum class ReflectionCondition {
|
||||
All, // primitive: no absences
|
||||
FaceCentred, // h, k, l all even or all odd
|
||||
Diamond // face-centred, and h + k + l = 4n when they are all even
|
||||
};
|
||||
|
||||
std::vector<float> CalculateXtalRings(const UnitCell &cell,
|
||||
ReflectionCondition condition = ReflectionCondition::All,
|
||||
int hkl_max = 6);
|
||||
std::vector<float> CalculateCubicXtalRings( float a, int hkl_max = 4);
|
||||
float GuessDetectorDistance(const DiffractionGeometry& geom, float ring_radius_pxl, float d_A);
|
||||
|
||||
|
||||
@@ -20,13 +20,15 @@ namespace {
|
||||
|
||||
const std::vector<Calibrant> &Calibrants() {
|
||||
static const std::vector<Calibrant> table = {
|
||||
// LaB6, Pm-3m: primitive, so every hkl is present.
|
||||
{"LaB6", UnitCell(LAB6_CELL_A, LAB6_CELL_A, LAB6_CELL_A, 90, 90, 90)},
|
||||
// Silver behenate. T. C. Huang, H. Toraya, T. N. Blanton, Y. Wu, J. Appl. Cryst. 26 (1993), 180-184.
|
||||
// Silver behenate, triclinic P. T. C. Huang, H. Toraya, T. N. Blanton, Y. Wu,
|
||||
// J. Appl. Cryst. 26 (1993), 180-184.
|
||||
{"AgBh", UnitCell(5.1769, 4.7218, 58.380, 89.440, 89.634, 75.854)},
|
||||
// CeO2, cubic fluorite, NIST SRM 674b.
|
||||
{"CeO2", UnitCell(5.4115, 5.4115, 5.4115, 90, 90, 90)},
|
||||
// Silicon, cubic, NIST SRM 640.
|
||||
{"Si", UnitCell(5.43102, 5.43102, 5.43102, 90, 90, 90)},
|
||||
// CeO2, cubic fluorite Fm-3m, NIST SRM 674b: first ring 111, not 100.
|
||||
{"CeO2", UnitCell(5.4115, 5.4115, 5.4115, 90, 90, 90), ReflectionCondition::FaceCentred},
|
||||
// Silicon, cubic Fd-3m, NIST SRM 640: face centring plus the diamond glide, so 222 is out too.
|
||||
{"Si", UnitCell(5.43102, 5.43102, 5.43102, 90, 90, 90), ReflectionCondition::Diamond},
|
||||
// Hexagonal ice: measured ring positions, not a cell (see the header).
|
||||
{"ice", std::nullopt}
|
||||
};
|
||||
@@ -39,7 +41,7 @@ std::vector<float> CalibrantRings(const std::string &name) {
|
||||
if (lower_case(c.name) != key)
|
||||
continue;
|
||||
if (c.cell)
|
||||
return CalculateXtalRings(*c.cell);
|
||||
return CalculateXtalRings(*c.cell, c.condition);
|
||||
|
||||
std::vector<float> q;
|
||||
for (const float d : ICE_RING_RES_A)
|
||||
|
||||
@@ -8,12 +8,16 @@
|
||||
#include <vector>
|
||||
|
||||
#include "../../common/UnitCell.h"
|
||||
#include "AssignSpotsToRings.h" // ReflectionCondition
|
||||
|
||||
// The powder standards the detector geometry can be calibrated against, shared by
|
||||
// rugnux --mode=calibration and the viewer's powder-calibration panel.
|
||||
struct Calibrant {
|
||||
std::string name; // as offered on the command line and in the GUI (matched case-insensitively)
|
||||
std::optional<UnitCell> cell; // unset for ice - see CalibrantRings()
|
||||
// The cell alone does not give the rings: a centred lattice extinguishes some of them, and the
|
||||
// innermost ring the fit starts from is the first one that is NOT extinguished.
|
||||
ReflectionCondition condition = ReflectionCondition::All;
|
||||
};
|
||||
|
||||
const std::vector<Calibrant> &Calibrants();
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
#include <type_traits>
|
||||
|
||||
#include "ImagePreprocessorGPU.h"
|
||||
#include "../../common/JFJochException.h"
|
||||
|
||||
namespace {
|
||||
void cuda_err(cudaError_t val) {
|
||||
if (val != cudaSuccess)
|
||||
throw JFJochException(JFJochExceptionCategory::GPUCUDAError, cudaGetErrorString(val));
|
||||
}
|
||||
}
|
||||
|
||||
template<class T>
|
||||
__global__ void preprocess_kernel(
|
||||
@@ -237,9 +245,9 @@ ImagePreprocessorGPU::ImagePreprocessorGPU(const DiffractionExperiment &experime
|
||||
// Setup GPU settings. The current device, not device 0: workers are pinned round-robin across GPUs,
|
||||
// so device 0's SM count can belong to a different card than the one these kernels launch on.
|
||||
int device = 0;
|
||||
cudaGetDevice(&device);
|
||||
cuda_err(cudaGetDevice(&device));
|
||||
cudaDeviceProp prop{};
|
||||
cudaGetDeviceProperties(&prop, device);
|
||||
cuda_err(cudaGetDeviceProperties(&prop, device));
|
||||
|
||||
threads = 128;
|
||||
blocks = 4 * prop.multiProcessorCount;
|
||||
@@ -322,7 +330,7 @@ ImageStatistics ImagePreprocessorGPU::UntransposeAndAnalyze(ImagePreprocessorBuf
|
||||
sat_value = static_cast<T>(saturation_limit);
|
||||
|
||||
cpu_stats[0] = ImageStatistics{.max_value = INT64_MIN, .min_value = INT64_MAX};
|
||||
cudaMemcpyAsync(gpu_stats, cpu_stats.data(), sizeof(ImageStatistics), cudaMemcpyHostToDevice, *stream);
|
||||
cuda_err(cudaMemcpyAsync(gpu_stats, cpu_stats.data(), sizeof(ImageStatistics), cudaMemcpyHostToDevice, *stream));
|
||||
|
||||
// One CUDA block per bitshuffle block, plus one for the verbatim tail when there is one.
|
||||
const int nb = shuffled.nblocks + (shuffled.tail_elems > 0 ? 1 : 0);
|
||||
@@ -338,12 +346,16 @@ ImageStatistics ImagePreprocessorGPU::UntransposeAndAnalyze(ImagePreprocessorBuf
|
||||
shuffled.tail_src,
|
||||
shuffled.tail_elems,
|
||||
shuffled.tail_elem0);
|
||||
cuda_err(cudaGetLastError());
|
||||
|
||||
if (copy_image_to_host)
|
||||
cudaMemcpyAsync(processed_image.data(), processed_image.getGPUBuffer(), npixels * sizeof(int32_t), cudaMemcpyDeviceToHost, *stream);
|
||||
cudaMemcpyAsync(cpu_stats.data(), gpu_stats, sizeof(ImageStatistics), cudaMemcpyDeviceToHost, *stream);
|
||||
cuda_err(cudaMemcpyAsync(processed_image.data(), processed_image.getGPUBuffer(), npixels * sizeof(int32_t), cudaMemcpyDeviceToHost, *stream));
|
||||
cuda_err(cudaMemcpyAsync(cpu_stats.data(), gpu_stats, sizeof(ImageStatistics), cudaMemcpyDeviceToHost, *stream));
|
||||
|
||||
cudaStreamSynchronize(*stream);
|
||||
// Check the synchronise: this path decodes bytes we did not produce, and cpu_stats still holds the
|
||||
// sentinel the host wrote above, so an unchecked failure here returns it as if it were a real
|
||||
// measurement and the caller's fallback to the host decoder never fires.
|
||||
cuda_err(cudaStreamSynchronize(*stream));
|
||||
|
||||
// Only now can the device tell us whether every block actually decoded.
|
||||
bslz4_decoder->ThrowIfDecodeFailed();
|
||||
@@ -365,7 +377,7 @@ ImageStatistics ImagePreprocessorGPU::Analyze(ImagePreprocessorBuffer &processed
|
||||
// On this engine's own stream, not the NULL stream: a NULL-stream copy implicitly synchronises with
|
||||
// every blocking stream in the process, which serialised all workers behind whichever one was
|
||||
// uploading. The stream is synchronised at the end of this function, so the ordering is unchanged.
|
||||
cudaMemcpyAsync(gpu_decompressed_image, input, npixels * sizeof(T), cudaMemcpyHostToDevice, *stream);
|
||||
cuda_err(cudaMemcpyAsync(gpu_decompressed_image, input, npixels * sizeof(T), cudaMemcpyHostToDevice, *stream));
|
||||
return AnalyzeOnDevice<T>(processed_image, err_value, sat_value);
|
||||
}
|
||||
|
||||
@@ -378,7 +390,7 @@ ImageStatistics ImagePreprocessorGPU::AnalyzeOnDevice(ImagePreprocessorBuffer &p
|
||||
sat_value = static_cast<T>(saturation_limit);
|
||||
|
||||
cpu_stats[0] = ImageStatistics{.max_value = INT64_MIN, .min_value = INT64_MAX};
|
||||
cudaMemcpyAsync(gpu_stats, cpu_stats.data(), sizeof(ImageStatistics), cudaMemcpyHostToDevice, *stream);
|
||||
cuda_err(cudaMemcpyAsync(gpu_stats, cpu_stats.data(), sizeof(ImageStatistics), cudaMemcpyHostToDevice, *stream));
|
||||
preprocess_kernel<T> <<< blocks, threads, 0, *stream >>>(
|
||||
reinterpret_cast<const T *>(gpu_decompressed_image.get()),
|
||||
gpu_mask->get(),
|
||||
@@ -387,13 +399,14 @@ ImageStatistics ImagePreprocessorGPU::AnalyzeOnDevice(ImagePreprocessorBuffer &p
|
||||
sat_value,
|
||||
err_value,
|
||||
npixels);
|
||||
cuda_err(cudaGetLastError());
|
||||
// The preprocessed image is 4 bytes per pixel - by far the largest transfer here - and every GPU
|
||||
// engine reads it straight from the device buffer, so it only comes back when a CPU engine needs it.
|
||||
if (copy_image_to_host)
|
||||
cudaMemcpyAsync(processed_image.data(), processed_image.getGPUBuffer(), npixels * sizeof(int32_t), cudaMemcpyDeviceToHost, *stream);
|
||||
cudaMemcpyAsync(cpu_stats.data(), gpu_stats, sizeof(ImageStatistics), cudaMemcpyDeviceToHost, *stream);
|
||||
cuda_err(cudaMemcpyAsync(processed_image.data(), processed_image.getGPUBuffer(), npixels * sizeof(int32_t), cudaMemcpyDeviceToHost, *stream));
|
||||
cuda_err(cudaMemcpyAsync(cpu_stats.data(), gpu_stats, sizeof(ImageStatistics), cudaMemcpyDeviceToHost, *stream));
|
||||
|
||||
cudaStreamSynchronize(*stream);
|
||||
cuda_err(cudaStreamSynchronize(*stream));
|
||||
|
||||
return cpu_stats[0];
|
||||
}
|
||||
|
||||
@@ -736,7 +736,16 @@ SearchSpaceGroupResult SearchSpaceGroup(
|
||||
// weak axial reflection with an under-estimated sigma (fake high I/sigma) from faking a
|
||||
// screw-axis violation; it only relaxes "present", so it cannot over-call a screw whose
|
||||
// predicted-absent class carries real intensity.
|
||||
const bool present = IoverSigma[i] > opt.present_i_over_sigma &&
|
||||
//
|
||||
// present_cut, not the fixed cut: merged sigma is floored at b*|I| (SigmaWithSystematicFloor),
|
||||
// so no reflection can read I/sigma above ISa = 1/b. On a merge whose ISa sits at or below the
|
||||
// fixed cut NOTHING is ever present - screw_violations is identically zero, so every screw axis
|
||||
// passes unchallenged, and present_strong is zero, so the centering rescue below switches
|
||||
// itself off on exactly the weak data it exists for. Stage A already caps the cut at the
|
||||
// merge's own 75th percentile; reusing it here keeps the two stages on one definition. Where
|
||||
// the fixed cut is already the smaller of the two - any merge with ISa comfortably above it -
|
||||
// present_cut EQUALS it and this is a no-op.
|
||||
const bool present = IoverSigma[i] > present_cut &&
|
||||
(opt.present_e_squared <= 0.0 || Esq[i] > opt.present_e_squared);
|
||||
|
||||
if (CenteringAbsent(gops, hkl)) {
|
||||
@@ -792,7 +801,11 @@ SearchSpaceGroupResult SearchSpaceGroup(
|
||||
for (const auto& a : screw_absent_refl) {
|
||||
const auto it = row_median.find(a.row);
|
||||
const double row_scale = it == row_median.end() ? 1.0 : std::max(1.0, it->second);
|
||||
if (a.i_over_sigma > opt.present_i_over_sigma &&
|
||||
// present_cut for the same reason as the general "present" above - with the fixed cut this
|
||||
// test cannot fire at all on a low-ISa merge. The row-relative E^2 gate is the other half of
|
||||
// the test and is unchanged, so an absence still has to look strong against its OWN axial row
|
||||
// to count as a violation.
|
||||
if (a.i_over_sigma > present_cut &&
|
||||
(opt.present_e_squared <= 0.0 || a.e_squared > opt.present_e_squared * row_scale))
|
||||
++screw_violations;
|
||||
screw_absent_esq.push_back(a.e_squared);
|
||||
|
||||
@@ -39,8 +39,9 @@ void FindSpots(DeviceOutput &output,
|
||||
|| (col == 767) || (col == 768))
|
||||
bad_pixel = 1;
|
||||
|
||||
// An unset limit masks nothing at that end (see MXAnalysisWithoutFPGA::UpdateMaskResolution).
|
||||
if ((d_array[coord] < settings.high_resolution_limit.value_or(0.0f))
|
||||
|| (d_array[coord] > settings.low_resolution_limit))
|
||||
|| (d_array[coord] > settings.low_resolution_limit.value_or(INFINITY)))
|
||||
bad_pixel = 1;
|
||||
|
||||
if ((std::is_signed<Tpixel>::value
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
// Sparse connected-component labelling adapted from the design the ACTS/traccc project arrived at
|
||||
// for sparse silicon-detector clusterization (backward-neighbour graph over a sorted hit list, then
|
||||
// a parallel union-find), which is itself the GPU counterpart of the SparseCCL that
|
||||
// StrongPixelSet.cpp uses on the host.
|
||||
// Sparse connected-component labelling following the design the ACTS/traccc project arrived at for
|
||||
// sparse silicon-detector clusterization (backward-neighbour graph over a sorted hit list, then a
|
||||
// parallel union-find), which is itself the GPU counterpart of the SparseCCL that
|
||||
// StrongPixelSet.cpp adapts on the host.
|
||||
// https://github.com/acts-project/traccc
|
||||
// (c) 2021-2025 CERN for the benefit of the ACTS project
|
||||
// Mozilla Public License Version 2.0
|
||||
// The kernels below are this project's own - the algorithm is traccc's. Cited in
|
||||
// docs/ACKNOWLEDGEMENT.md: P. Gessinger et al., "traccc: GPU track reconstruction library for HEP
|
||||
// experiments" (2025), arXiv:2505.22822.
|
||||
|
||||
#include <climits>
|
||||
|
||||
|
||||
@@ -20,7 +20,12 @@ struct SpotFindingSettings {
|
||||
// resolution clipping of the detection at all (DiffractionExperiment::GetDetectorMaxResolution_A
|
||||
// supplies the number where one is needed, e.g. for the spot plot's shells).
|
||||
std::optional<float> high_resolution_limit;
|
||||
float low_resolution_limit = 50.0;
|
||||
// Low-resolution limit for spot finding [A]. std::nullopt = no limit at the low-resolution end, the
|
||||
// mirror of high_resolution_limit above. Optional rather than a zero sentinel because zero is not a
|
||||
// natural "no limit" here: every pixel has d above it, so the plain comparison would mask the whole
|
||||
// image rather than none of it. Defaults to a concrete value, which is where the detection normally
|
||||
// stops - the direct beam and its halo sit beyond it.
|
||||
std::optional<float> low_resolution_limit = 50.0;
|
||||
float cutoff_spot_count_low_res = 5.0;
|
||||
std::optional<float> high_res_gap_Q_recipA = 1.5; // 0.25 * 2 * pi
|
||||
|
||||
|
||||
+1
-1
@@ -78,4 +78,4 @@ copy_license Qt6-LGPL-3.0.txt /usr/share/common-licenses/LGPL-3
|
||||
copy_license NVIDIA-CUDA-EULA.txt \
|
||||
/usr/local/cuda/EULA.txt /usr/local/cuda-12.8/EULA.txt
|
||||
|
||||
echo "Done. base64-macaron.txt, Qt6-NOTICE.txt and NVIDIA-CUDA-NOTICE.txt are maintained by hand."
|
||||
echo "Done. base64-macaron.txt, traccc.txt, Qt6-NOTICE.txt and NVIDIA-CUDA-NOTICE.txt are maintained by hand."
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
@@ -86,13 +86,21 @@ HDF5ImageLocator::Location HDF5ImageLocator::Resolve(int64_t global_image) const
|
||||
|
||||
std::vector<HDF5DataSourceMessage> HDF5ImageLocator::GetSourceMapping(uint64_t first_image,
|
||||
std::optional<uint64_t> image_count,
|
||||
uint64_t total_images) const {
|
||||
uint64_t total_images,
|
||||
uint64_t stride) const {
|
||||
if (stride == 0)
|
||||
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
||||
"Image stride cannot be zero");
|
||||
|
||||
if (first_image > total_images)
|
||||
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
||||
"First image outside dataset range");
|
||||
|
||||
const uint64_t requested_count = image_count.value_or(total_images - first_image);
|
||||
if (first_image + requested_count > total_images)
|
||||
// With a stride the caller's count is the number of OUTPUT images, which spans
|
||||
// (count - 1) * stride + 1 source images - not count of them.
|
||||
const uint64_t requested_count =
|
||||
image_count.value_or((total_images - first_image + stride - 1) / stride);
|
||||
if (requested_count > 0 && first_image + (requested_count - 1) * stride >= total_images)
|
||||
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
||||
"Requested image range outside dataset range");
|
||||
|
||||
@@ -102,15 +110,18 @@ std::vector<HDF5DataSourceMessage> HDF5ImageLocator::GetSourceMapping(uint64_t f
|
||||
|
||||
// Integrated / contiguous source: link directly to the original master file.
|
||||
if (layout_.format == FileWriterFormat::NXmxVDS && layout_.data_layout != HDF5DataSetLayout::VIRTUAL) {
|
||||
AppendOrExtendSourceMapping(ret, layout_.master_filename, "/entry/data/data",
|
||||
first_image, 0, requested_count);
|
||||
// One extendable run when the images are consecutive; strided output needs one mapping per image,
|
||||
// which AppendOrExtendSourceMapping leaves separate because the source indices are not adjacent.
|
||||
for (uint64_t local_image = 0; local_image < requested_count; ++local_image)
|
||||
AppendOrExtendSourceMapping(ret, layout_.master_filename, "/entry/data/data",
|
||||
first_image + local_image * stride, local_image, 1);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// VDS source: expand VDS mappings to original source files, not to the VDS master.
|
||||
if (layout_.format == FileWriterFormat::NXmxVDS && layout_.data_layout == HDF5DataSetLayout::VIRTUAL) {
|
||||
for (uint64_t local_image = 0; local_image < requested_count; ++local_image) {
|
||||
const hsize_t virtual_image = first_image + local_image;
|
||||
const hsize_t virtual_image = first_image + local_image * stride;
|
||||
|
||||
bool found = false;
|
||||
for (const auto &mapping: layout_.vds_mappings) {
|
||||
@@ -140,7 +151,7 @@ std::vector<HDF5DataSourceMessage> HDF5ImageLocator::GetSourceMapping(uint64_t f
|
||||
"Cannot generate HDF5 source mapping: images_per_file is zero");
|
||||
|
||||
for (uint64_t local_image = 0; local_image < requested_count; ++local_image) {
|
||||
const uint64_t source_global_image = first_image + local_image;
|
||||
const uint64_t source_global_image = first_image + local_image * stride;
|
||||
const uint64_t file_id = source_global_image / layout_.images_per_file;
|
||||
const uint64_t source_image = source_global_image % layout_.images_per_file;
|
||||
|
||||
|
||||
@@ -49,9 +49,14 @@ public:
|
||||
|
||||
// Source mapping for re-writing a derived file (e.g. _process.h5) so it links back to the
|
||||
// original pixel sources rather than to a master. total_images is supplied by the caller.
|
||||
// stride is the step between consecutive images of the derived file in the SOURCE: image i of the
|
||||
// output comes from source image first_image + i * stride. It has to match the stride the caller
|
||||
// processed with, or the pictures and the per-image analysis in the derived file describe
|
||||
// different frames.
|
||||
std::vector<HDF5DataSourceMessage> GetSourceMapping(uint64_t first_image,
|
||||
std::optional<uint64_t> image_count,
|
||||
uint64_t total_images) const;
|
||||
uint64_t total_images,
|
||||
uint64_t stride = 1) const;
|
||||
|
||||
private:
|
||||
Layout layout_;
|
||||
|
||||
@@ -18,8 +18,9 @@ HDF5ImageLocator::Location HDF5ImageSource::Resolve(int64_t global) const {
|
||||
|
||||
std::vector<HDF5DataSourceMessage> HDF5ImageSource::GetSourceMapping(uint64_t first_image,
|
||||
std::optional<uint64_t> image_count,
|
||||
uint64_t total_images) const {
|
||||
return locator_.GetSourceMapping(first_image, image_count, total_images);
|
||||
uint64_t total_images,
|
||||
uint64_t stride) const {
|
||||
return locator_.GetSourceMapping(first_image, image_count, total_images, stride);
|
||||
}
|
||||
|
||||
CompressedImage HDF5ImageSource::ReadImageAt(std::vector<uint8_t> &buffer,
|
||||
|
||||
@@ -28,7 +28,8 @@ public:
|
||||
|
||||
std::vector<HDF5DataSourceMessage> GetSourceMapping(uint64_t first_image,
|
||||
std::optional<uint64_t> image_count,
|
||||
uint64_t total_images) const;
|
||||
uint64_t total_images,
|
||||
uint64_t stride = 1) const;
|
||||
|
||||
private:
|
||||
HDF5ImageLocator locator_;
|
||||
|
||||
@@ -92,14 +92,15 @@ bool JFJochHDF5Reader::LoadImage_i(std::shared_ptr<JFJochReaderDataset> &dataset
|
||||
}
|
||||
|
||||
std::vector<HDF5DataSourceMessage> JFJochHDF5Reader::GetHDF5DataSource(uint64_t first_image,
|
||||
std::optional<uint64_t> image_count) const {
|
||||
std::optional<uint64_t> image_count,
|
||||
uint64_t stride) const {
|
||||
std::unique_lock ul(hdf5_mutex);
|
||||
|
||||
if (!active_metadata_)
|
||||
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
||||
"Cannot generate HDF5 source mapping if file not loaded");
|
||||
|
||||
return image_source_.GetSourceMapping(first_image, image_count, number_of_images);
|
||||
return image_source_.GetSourceMapping(first_image, image_count, number_of_images, stride);
|
||||
}
|
||||
|
||||
std::vector<IntegrationOutcome> JFJochHDF5Reader::ReadReflections(size_t start_image,
|
||||
|
||||
@@ -44,9 +44,13 @@ public:
|
||||
|
||||
void Close() override;
|
||||
|
||||
// stride is the step in the SOURCE between consecutive images of the derived file: image i of the
|
||||
// output comes from source image first_image + i * stride. Pass the stride the caller processed
|
||||
// with, or the linked pictures and the per-image analysis describe different frames.
|
||||
std::vector<HDF5DataSourceMessage> GetHDF5DataSource(
|
||||
uint64_t first_image = 0,
|
||||
std::optional<uint64_t> image_count = {}
|
||||
std::optional<uint64_t> image_count = {},
|
||||
uint64_t stride = 1
|
||||
) const;
|
||||
|
||||
std::vector<IntegrationOutcome> ReadReflections(size_t start_image = 0, std::optional<size_t> end_image = {}) const;
|
||||
|
||||
+8
-2
@@ -447,7 +447,10 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
|
||||
if (end_image < 0 || end_image > total_images_in_file)
|
||||
end_image = total_images_in_file;
|
||||
const int start_image = config_.start_image;
|
||||
const int images_to_process = (end_image - start_image) / config_.stride;
|
||||
// Round UP: the worker loop below walks ordinals until start + ordinal * stride reaches end_image,
|
||||
// so a stride that does not divide the range evenly still processes a final partial step. Rounding
|
||||
// down here would size the per-image arrays one short of the ordinals that index them.
|
||||
const int images_to_process = (end_image - start_image + config_.stride - 1) / config_.stride;
|
||||
if (images_to_process <= 0) {
|
||||
logger.Warning("No images to process (start {}, end {}, stride {}, total {})",
|
||||
start_image, end_image, config_.stride, total_images_in_file);
|
||||
@@ -525,7 +528,10 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b
|
||||
start_message.file_format = FileWriterFormat::NXmxIntegrated;
|
||||
start_message.write_master_file = true;
|
||||
start_message.write_images = false;
|
||||
start_message.hdf5_source_data = reader_.GetHDF5DataSource(start_image, images_to_process);
|
||||
// With the stride: image i of the process file is source image start_image + i * stride, so the
|
||||
// linked pictures line up with the per-image analysis written beside them.
|
||||
start_message.hdf5_source_data =
|
||||
reader_.GetHDF5DataSource(start_image, images_to_process, config_.stride);
|
||||
|
||||
std::unique_ptr<FileWriter> writer;
|
||||
if (write_files && config_.write_process_h5)
|
||||
|
||||
+28
-10
@@ -97,7 +97,7 @@ void print_usage() {
|
||||
std::cout << " --no-adaptive-spots Turn adaptive detection off and use the fixed --spot-threshold / --spot-sigma finder instead" << std::endl;
|
||||
std::cout << " --spot-false-pixels <num> Adaptive detection operating point: expected noise pixels tolerated per frame (default: 100; implies --adaptive-spots)" << std::endl;
|
||||
std::cout << " --spot-high-resolution <num> High resolution limit for spot finding. If omitted (or 0), spot finding is not clipped in resolution and extends as far as the detector reaches" << std::endl;
|
||||
std::cout << " --spot-low-resolution <num> Low resolution limit for spot finding, in A (default: 50; lower it, e.g. 24, to exclude the direct-beam halo on weakly-diffracting serial data)" << std::endl;
|
||||
std::cout << " --spot-low-resolution <num> Low resolution limit for spot finding, in A (default: 50; lower it, e.g. 24, to exclude the direct-beam halo on weakly-diffracting serial data; 0 removes the limit)" << std::endl;
|
||||
std::cout << " --max-spots <num> Max spot count per image, the strongest ones, handed to indexing (default: 1000)" << std::endl;
|
||||
std::cout << " --detect-ice-rings[=on|off] Flag ice-ring spots (de-prioritised in indexing) and exclude ice-ring reflections from scaling. Default: the master file's setting, or - where the file says nothing - on for rotation and off for stills" << std::endl;
|
||||
std::cout << std::endl;
|
||||
@@ -602,7 +602,9 @@ static int RunRugnux(int argc, char **argv) {
|
||||
GeomRefinementAlgorithmEnum refinement_algorithm = GeomRefinementAlgorithmEnum::BeamCenter;
|
||||
|
||||
std::optional<float> d_min_spot_finding; // unset -> as far as the detector reaches
|
||||
float d_max_spot_finding = 0; // 0 = keep the SpotFindingSettings default (50 A)
|
||||
// The low-resolution limit to apply: starts at the SpotFindingSettings default rather than restating
|
||||
// it, and --spot-low-resolution 0 resets it to unset, i.e. no limit at that end.
|
||||
std::optional<float> d_max_spot_finding = SpotFindingSettings{}.low_resolution_limit;
|
||||
std::optional<float> d_min_scale_merge;
|
||||
std::optional<ResolutionCutoffMethod> resolution_cutoff_method; // --resolution-cutoff cc-logistic|off
|
||||
std::optional<double> resolution_cc_target; // --resolution-cc-target
|
||||
@@ -819,10 +821,19 @@ static int RunRugnux(int argc, char **argv) {
|
||||
adaptive_spots = true;
|
||||
logger.Info("Adaptive spot detection: expected false pixels/frame set to {:.0f}", false_pixels_per_frame);
|
||||
break;
|
||||
case OPT_SPOT_LOW_RESOLUTION:
|
||||
d_max_spot_finding = parse_number_arg<float>(optarg, "--spot-low-resolution", logger, 0.0f);
|
||||
logger.Info("Low resolution limit for spot finding set to {:.1f} A", d_max_spot_finding);
|
||||
case OPT_SPOT_LOW_RESOLUTION: {
|
||||
// Same shape as --spot-high-resolution below: 0 means "no limit at that end", expressed as
|
||||
// the unset optional the rest of the code understands.
|
||||
const auto d_max = parse_number_arg<float>(optarg, "--spot-low-resolution", logger, 0.0f);
|
||||
if (d_max > 0.0f) {
|
||||
d_max_spot_finding = d_max;
|
||||
logger.Info("Low resolution limit for spot finding set to {:.1f} A", d_max);
|
||||
} else {
|
||||
d_max_spot_finding.reset();
|
||||
logger.Info("No low resolution limit for spot finding");
|
||||
}
|
||||
break;
|
||||
}
|
||||
case OPT_SPOT_RESOLUTION: {
|
||||
// 0 has always meant "no limit" for this setting; keep that, but express it as the unset
|
||||
// optional the rest of the code understands. Passing the 0 through instead reached
|
||||
@@ -1428,7 +1439,10 @@ static int RunRugnux(int argc, char **argv) {
|
||||
exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
int images_to_process = (end_image - start_image) / image_stride;
|
||||
// Round UP: the worker loop walks ordinals until start + ordinal * stride reaches end_image, so a
|
||||
// stride that does not divide the range evenly still processes a final partial step. Rounding down
|
||||
// here would size the per-image arrays one short of the ordinals that index them.
|
||||
int images_to_process = (end_image - start_image + image_stride - 1) / image_stride;
|
||||
|
||||
if (images_to_process <= 0) {
|
||||
logger.Warning("No images to process (Start: {}, End: {} Stride: {}, Total: {})", start_image, end_image,
|
||||
@@ -1481,6 +1495,12 @@ static int RunRugnux(int argc, char **argv) {
|
||||
// --mode azint: azimuthal integration only (no spot finding / indexing / scaling). Rugnux reads
|
||||
// the geometry and azimuthal-integration settings configured above off the experiment.
|
||||
if (mode == RugnuxMode::Azint) {
|
||||
// As for --mode calibration below: the polarization factor is a rugnux analysis default and it
|
||||
// scales the profile this mode exists to produce, so apply the defaults and then let an explicit
|
||||
// --polarization override them. Without this the flag was parsed and then dropped on this path.
|
||||
ApplyRugnuxExperimentDefaults(experiment);
|
||||
if (polarization_factor) experiment.PolarizationFactor(polarization_factor.value());
|
||||
|
||||
ProcessConfig config;
|
||||
config.mode = ProcessMode::AzimuthalIntegration;
|
||||
config.start_image = start_image;
|
||||
@@ -1541,8 +1561,7 @@ static int RunRugnux(int argc, char **argv) {
|
||||
config.spot_finding.adaptive_threshold = adaptive_spots.value_or(true);
|
||||
config.spot_finding.high_resolution_limit = d_min_spot_finding;
|
||||
config.spot_finding.false_pixels_per_frame = false_pixels_per_frame;
|
||||
if (d_max_spot_finding > 0.0f)
|
||||
config.spot_finding.low_resolution_limit = d_max_spot_finding;
|
||||
config.spot_finding.low_resolution_limit = d_max_spot_finding;
|
||||
experiment.MaxSpotCount(max_spot_count_override.value_or(RUGNUX_MAX_SPOT_COUNT));
|
||||
|
||||
logger.Info("Powder calibration: calibrant {} ({} rings), method {}, {} azimuthal bins",
|
||||
@@ -1841,8 +1860,7 @@ static int RunRugnux(int argc, char **argv) {
|
||||
spot_settings.adaptive_threshold = adaptive_spots.value_or(true);
|
||||
spot_settings.high_resolution_limit = d_min_spot_finding;
|
||||
spot_settings.false_pixels_per_frame = false_pixels_per_frame;
|
||||
if (d_max_spot_finding > 0.0f)
|
||||
spot_settings.low_resolution_limit = d_max_spot_finding;
|
||||
spot_settings.low_resolution_limit = d_max_spot_finding;
|
||||
|
||||
// Validate the assembled spot-finding settings the same way the online receivers do (broker and
|
||||
// receiver call this same function). It enforces the cross-field constraints that per-argument
|
||||
|
||||
+12
-9
@@ -7,6 +7,7 @@
|
||||
#include "../frame_serialize/CBORStream2Deserializer.h"
|
||||
#include "../compression/JFJochCompressor.h"
|
||||
#include "../frame_serialize/CborUtil.h"
|
||||
#include "../common/DiffractionExperiment.h" // kImageMetadataHeadroom
|
||||
|
||||
TEST_CASE("CBORSerialize_Start", "[CBOR]") {
|
||||
std::vector<uint8_t> buffer(8 * 1024 * 1024);
|
||||
@@ -1027,17 +1028,19 @@ TEST_CASE("CBORSerialize_Image_Reflections") {
|
||||
TEST_CASE("CBORSerialize_Image_MetadataHeadroom", "[CBOR]") {
|
||||
// The receiver serializes the per-image CBOR metadata (spots, reflections,
|
||||
// azimuthal profile, ...) into the same buffer slot as the compressed image, and
|
||||
// DiffractionExperiment::GetImageBufferLocationSize() reserves 4 MB of headroom
|
||||
// on top of GetMaxCompressedSize() for it. A frame whose metadata exceeds that
|
||||
// headroom is dropped by the receiver. This test guards that even the largest
|
||||
// possible metadata still fits, so ordinary frames are never dropped: the
|
||||
// kMaxReflections (10000, IndexAndRefine.cpp) reflection cap, the API maximum of
|
||||
// 2000 spots (jfjoch_api.yaml), and the 65534-bin azimuthal cap
|
||||
// (AzimuthalIntegrationMapping, UINT16_MAX - 1), all serialized together.
|
||||
// DiffractionExperiment::GetImageBufferLocationSize() reserves
|
||||
// kImageMetadataHeadroom on top of GetMaxCompressedSize() for it. A frame whose
|
||||
// metadata exceeds that headroom is dropped by the receiver. This test guards that
|
||||
// even the largest possible metadata still fits, so ordinary frames are never
|
||||
// dropped: the kOnlineMaxReflections (10000, BraggPrediction.h) reflection cap, the
|
||||
// API maximum of 2000 spots (jfjoch_api.yaml), and the 65534-bin azimuthal cap
|
||||
// (AzimuthalIntegrationMapping, UINT16_MAX - 1), all serialized together. Take the
|
||||
// headroom from the constant rather than restate it: written down twice, the two
|
||||
// drifted apart and this test kept passing against the value the code had left.
|
||||
constexpr size_t max_reflections = 10000;
|
||||
constexpr size_t max_spots = 2000;
|
||||
constexpr size_t max_az_bins = 65534;
|
||||
constexpr size_t headroom = 4 * 1024 * 1024;
|
||||
constexpr size_t headroom = DiffractionExperiment::kImageMetadataHeadroom;
|
||||
|
||||
std::vector<Reflection> reflections(max_reflections);
|
||||
for (size_t i = 0; i < reflections.size(); ++i)
|
||||
@@ -1077,7 +1080,7 @@ TEST_CASE("CBORSerialize_Image_MetadataHeadroom", "[CBOR]") {
|
||||
REQUIRE_NOTHROW(serializer.SerializeImage(message));
|
||||
|
||||
// GetImageAppendOffset() is where the compressed image is appended, i.e. the size
|
||||
// of everything-but-the-image; it must stay below the 4 MB metadata headroom.
|
||||
// of everything-but-the-image; it must stay below the metadata headroom.
|
||||
INFO("worst-case metadata size = " << serializer.GetImageAppendOffset() << " B");
|
||||
CHECK(serializer.GetImageAppendOffset() < headroom);
|
||||
}
|
||||
|
||||
@@ -19,17 +19,49 @@ TEST_CASE("Calibrants_LookupIsCaseInsensitive", "[DetGeomCalib]") {
|
||||
CHECK(CalibrantRings("nonsense").empty());
|
||||
}
|
||||
|
||||
// The innermost ring of a cubic standard is its (100), so the first q is 2*pi/a. This is what fixes
|
||||
// the distance in GuessInitialGeometry, so a wrong table would put every calibration off by that scale.
|
||||
TEST_CASE("Calibrants_CubicStandardsHaveTheirLatticeConstant", "[DetGeomCalib]") {
|
||||
const std::map<std::string, double> a_A = {{"lab6", LAB6_CELL_A}, {"ceo2", 5.4115}, {"si", 5.43102}};
|
||||
for (const auto &[name, a] : a_A) {
|
||||
const auto q = CalibrantRings(name);
|
||||
// GuessInitialGeometry pairs the innermost OBSERVED ring with the first entry of this list to fix the
|
||||
// detector distance, so the first entry has to be a reflection that is really there. Only the primitive
|
||||
// standard starts at (100): both face-centred ones extinguish it and start at (111), i.e. sqrt(3) times
|
||||
// further out. Listing a forbidden ring first would scale every calibration by that ratio.
|
||||
TEST_CASE("Calibrants_FirstRingIsThePresentOne", "[DetGeomCalib]") {
|
||||
struct Standard { std::string name; double a_A; double first_hkl_norm; };
|
||||
// sqrt(h^2+k^2+l^2) of the innermost present reflection: LaB6 Pm-3m -> 100, CeO2 Fm-3m and Si Fd-3m -> 111.
|
||||
const std::vector<Standard> standards = {
|
||||
{"lab6", LAB6_CELL_A, 1.0},
|
||||
{"ceo2", 5.4115, std::sqrt(3.0)},
|
||||
{"si", 5.43102, std::sqrt(3.0)}
|
||||
};
|
||||
for (const auto &s : standards) {
|
||||
const auto q = CalibrantRings(s.name);
|
||||
REQUIRE(!q.empty());
|
||||
CHECK(q.front() == Catch::Approx(2.0 * PI / a).epsilon(1e-5));
|
||||
CHECK(q.front() == Catch::Approx(2.0 * PI * s.first_hkl_norm / s.a_A).epsilon(1e-5));
|
||||
}
|
||||
}
|
||||
|
||||
// The centring conditions themselves, checked ring by ring rather than only on the first one: an
|
||||
// extinct reflection anywhere in the list mis-assigns the observed rings around it.
|
||||
TEST_CASE("Calibrants_CentredStandardsOmitTheExtinctRings", "[DetGeomCalib]") {
|
||||
auto has_ring = [](const std::vector<float> &q, double a_A, double hkl_norm) {
|
||||
const auto want = static_cast<float>(2.0 * PI * hkl_norm / a_A);
|
||||
return std::any_of(q.begin(), q.end(),
|
||||
[&](float v) { return std::fabs(v - want) < 1e-3f; });
|
||||
};
|
||||
|
||||
const auto ceo2 = CalibrantRings("ceo2");
|
||||
CHECK(has_ring(ceo2, 5.4115, std::sqrt(3.0))); // 111 - all odd
|
||||
CHECK(has_ring(ceo2, 5.4115, std::sqrt(4.0))); // 200 - all even
|
||||
CHECK(has_ring(ceo2, 5.4115, std::sqrt(12.0))); // 222 - all even, present without a glide plane
|
||||
CHECK_FALSE(has_ring(ceo2, 5.4115, 1.0)); // 100 - mixed parity
|
||||
CHECK_FALSE(has_ring(ceo2, 5.4115, std::sqrt(2.0))); // 110 - mixed parity
|
||||
|
||||
const auto si = CalibrantRings("si");
|
||||
CHECK(has_ring(si, 5.43102, std::sqrt(3.0))); // 111 - all odd
|
||||
CHECK(has_ring(si, 5.43102, std::sqrt(8.0))); // 220 - all even, h+k+l = 4n
|
||||
CHECK_FALSE(has_ring(si, 5.43102, 1.0)); // 100 - mixed parity
|
||||
CHECK_FALSE(has_ring(si, 5.43102, std::sqrt(4.0))); // 200 - all even, h+k+l = 2
|
||||
CHECK_FALSE(has_ring(si, 5.43102, std::sqrt(12.0))); // 222 - the diamond glide takes it out
|
||||
}
|
||||
|
||||
// Ice is the reason the calibrant abstraction is a ring list and not a UnitCell: its entries are
|
||||
// measured ring positions, and enumerating hkl from the hexagonal cell would add rings that are
|
||||
// systematically absent in P6_3/mmc.
|
||||
|
||||
@@ -2069,6 +2069,124 @@ TEST_CASE("JFJochReader_ProcessingHDF5_FromVDS_MapsToDataFiles", "[HDF5][Full]")
|
||||
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
|
||||
}
|
||||
|
||||
// rugnux --stride N processes every Nth image, so image i of the _process.h5 IS source image
|
||||
// start + i*N. The mapping used to be built without the stride, linking the first N images instead,
|
||||
// which put each frame's picture next to a different frame's analysis. Read the pixels back through
|
||||
// the written VDS rather than only inspecting the mapping: that is what a user opening the file sees.
|
||||
TEST_CASE("JFJochReader_ProcessingHDF5_Strided_LinksTheProcessedImages", "[HDF5][Full]") {
|
||||
DiffractionExperiment x(DetJF(1));
|
||||
|
||||
x.FilePrefix("proc_stride_src").ImagesPerTrigger(10).ImagesPerFile(2).OverwriteExistingFiles(true);
|
||||
x.BitDepthImage(16).SetFileWriterFormat(FileWriterFormat::NXmxVDS).PixelSigned(true);
|
||||
x.Compression(CompressionAlgorithm::NO_COMPRESSION);
|
||||
|
||||
std::vector<int16_t> image(x.GetPixelsNum(), 51);
|
||||
|
||||
RegisterHDF5Filter();
|
||||
{
|
||||
StartMessage start_message;
|
||||
x.FillMessage(start_message);
|
||||
FileWriter writer(start_message);
|
||||
|
||||
for (int i = 0; i < x.GetImageNum(); i++) {
|
||||
image[5678] = static_cast<int16_t>(500 + i); // per-image tag: which source frame is this?
|
||||
|
||||
DataMessage message{};
|
||||
message.image = CompressedImage(image, x.GetXPixelsNum(), x.GetYPixelsNum());
|
||||
message.number = i;
|
||||
REQUIRE_NOTHROW(writer.WriteHDF5(message));
|
||||
}
|
||||
|
||||
EndMessage end_message;
|
||||
end_message.max_image_number = x.GetImageNum();
|
||||
writer.WriteHDF5(end_message);
|
||||
writer.Finalize();
|
||||
}
|
||||
|
||||
// start 1, 3 images, stride 3 -> source images 1, 4, 7. Two images per data file, so those sit in
|
||||
// data_000001 (holds 0,1), data_000003 (holds 4,5) and data_000004 (holds 6,7).
|
||||
std::vector<HDF5DataSourceMessage> source_data;
|
||||
{
|
||||
JFJochHDF5Reader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFile("proc_stride_src_master.h5"));
|
||||
source_data = reader.GetHDF5DataSource(1, 3, 3);
|
||||
|
||||
// Non-adjacent source images cannot be merged into one run, so one mapping per image.
|
||||
REQUIRE(source_data.size() == 3);
|
||||
|
||||
CHECK(source_data[0].filename == "proc_stride_src_data_000001.h5");
|
||||
CHECK(source_data[0].source_first_image == 1);
|
||||
CHECK(source_data[0].virtual_first_image == 0);
|
||||
CHECK(source_data[0].image_count == 1);
|
||||
|
||||
CHECK(source_data[1].filename == "proc_stride_src_data_000003.h5");
|
||||
CHECK(source_data[1].source_first_image == 0);
|
||||
CHECK(source_data[1].virtual_first_image == 1);
|
||||
CHECK(source_data[1].image_count == 1);
|
||||
|
||||
CHECK(source_data[2].filename == "proc_stride_src_data_000004.h5");
|
||||
CHECK(source_data[2].source_first_image == 1);
|
||||
CHECK(source_data[2].virtual_first_image == 2);
|
||||
CHECK(source_data[2].image_count == 1);
|
||||
}
|
||||
|
||||
{
|
||||
DiffractionExperiment proc_x = x;
|
||||
proc_x.FilePrefix("proc_from_stride")
|
||||
.ImagesPerTrigger(3)
|
||||
.SetFileWriterFormat(FileWriterFormat::NXmxIntegrated)
|
||||
.OverwriteExistingFiles(true);
|
||||
|
||||
StartMessage start_message;
|
||||
proc_x.FillMessage(start_message);
|
||||
start_message.number_of_images = 3;
|
||||
start_message.images_per_file = 3;
|
||||
start_message.write_images = false;
|
||||
start_message.write_master_file = true;
|
||||
start_message.hdf5_source_data = source_data;
|
||||
|
||||
FileWriter writer(start_message);
|
||||
|
||||
for (int i = 0; i < 3; i++) {
|
||||
DataMessage message{};
|
||||
message.number = i;
|
||||
message.original_number = 1 + i * 3;
|
||||
message.image = CompressedImage(image, x.GetXPixelsNum(), x.GetYPixelsNum());
|
||||
message.spot_count = 200 + i;
|
||||
REQUIRE_NOTHROW(writer.WriteHDF5(message));
|
||||
}
|
||||
|
||||
EndMessage end_message;
|
||||
end_message.max_image_number = 3;
|
||||
writer.WriteHDF5(end_message);
|
||||
writer.Finalize();
|
||||
}
|
||||
|
||||
{
|
||||
JFJochHDF5Reader reader;
|
||||
REQUIRE_NOTHROW(reader.ReadFile("proc_from_stride_master.h5"));
|
||||
|
||||
// The whole point: frame i of the process file carries source frame 1 + 3i, not 1 + i.
|
||||
auto img0 = reader.LoadImage(0);
|
||||
REQUIRE(img0);
|
||||
CHECK(img0->Image()[5678] == 501);
|
||||
|
||||
auto img1 = reader.LoadImage(1);
|
||||
REQUIRE(img1);
|
||||
CHECK(img1->Image()[5678] == 504);
|
||||
|
||||
auto img2 = reader.LoadImage(2);
|
||||
REQUIRE(img2);
|
||||
CHECK(img2->Image()[5678] == 507);
|
||||
}
|
||||
|
||||
remove("proc_stride_src_master.h5");
|
||||
for (int i = 1; i <= 5; i++)
|
||||
remove(("proc_stride_src_data_00000" + std::to_string(i) + ".h5").c_str());
|
||||
remove("proc_from_stride_master.h5");
|
||||
REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0);
|
||||
}
|
||||
|
||||
// One reflection with a distinct value in EVERY field that is meant to survive a write/read cycle,
|
||||
// keyed on the image and the reflection index so no two are alike and a field read back from the
|
||||
// wrong place shows up. Fractional image_number on purpose: a 3D-integrated reflection has one, and
|
||||
|
||||
@@ -581,8 +581,16 @@ void JFJochImageReadingWorker::UpdateSpotFindingSettings(const SpotFindingSettin
|
||||
indexing_settings.ViableCellMinSpots(indexing.GetViableCellMinSpots());
|
||||
indexing_settings.IndexIceRings(indexing.GetIndexIceRings());
|
||||
indexing_settings.UnitCellDistTolerance(indexing.GetUnitCellDistTolerance());
|
||||
curr_experiment.ImportIndexingSettings(indexing_settings);
|
||||
curr_experiment.MaxSpotCount(max_spots);
|
||||
// The setters range-check and throw. This runs as a queued slot on the worker's event loop, where
|
||||
// an escaping exception is std::terminate rather than a message, so report it as FindCenter does
|
||||
// and keep the previous settings.
|
||||
try {
|
||||
curr_experiment.ImportIndexingSettings(indexing_settings);
|
||||
curr_experiment.MaxSpotCount(max_spots);
|
||||
} catch (const JFJochException &e) {
|
||||
logger.ErrorException(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (auto_reanalyze) {
|
||||
ReanalyzeImage_i();
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include "../../common/Definitions.h" // MAX_SPOT_COUNT
|
||||
#include "SliderPlusBox.h"
|
||||
#include "NumberLineEdit.h"
|
||||
#include "PowderCalibrationWidget.h"
|
||||
@@ -253,7 +254,10 @@ QWidget *JFJochViewerSettingsDock::BuildMXPage() {
|
||||
"3/2/1 and keep whichever maximises indexed count x indexed fraction. "
|
||||
"Stills only - a rotation dataset builds one lattice from all frames and "
|
||||
"always uses the fixed min-pixels/spot value below.");
|
||||
auto *maxSpots = new NumberLineEdit(10.0f, 100000.0f, static_cast<float>(max_spots_), 0, "", page);
|
||||
// The same bounds DatasetSettings::MaxSpotCount enforces - offering more than it accepts only got
|
||||
// the value rejected further down.
|
||||
auto *maxSpots = new NumberLineEdit(10.0f, static_cast<float>(MAX_SPOT_COUNT),
|
||||
static_cast<float>(max_spots_), 0, "", page);
|
||||
spot->addRow("", adaptive);
|
||||
spot->addRow("Signal/noise", snr);
|
||||
spot->addRow("Photon count", count);
|
||||
|
||||
Reference in New Issue
Block a user