The profile is the MEAN of each bin, so a few strong reflections landing in a bin lift it exactly as a smooth powder ring does. That is the wrong quantity whenever the profile is wanted as a background rather than as a measurement of what is in the bin - the ice score being the case in point, where reading a plain profile INVERTED the metric: over 37 rotation crystals the two highest-scoring crystals had no ice at all. The adaptive spot finder already computes the right thing, a sigma-clipped per-resolution-ring background, as a byproduct of its own threshold. Where it runs, the ice score uses that. Where it does not - --no-adaptive-spots, --azint-only, and anything reading the profile the broker wrote - there was no way to get it. This adds one: azim_int_settings.sigma_clip (rugnux --azim-sigma-clip), 0 = off, minimum 2 because a tighter clip rejects a large part of a clean Gaussian bin and biases the estimate low rather than removing outliers. Two clip passes follow the plain one, matching the finder's recipe - the first pass's standard deviation is itself inflated by the peaks being removed, so one pass leaves a threshold that is still too generous. A bin with fewer than eight pixels is left alone: at the detector edge and behind the beam stop there is no spread to clip on. Both engines do it. On the GPU the accept range is computed by a small kernel and stays resident, so a clip pass is one more read of the same pixels and no round trip; the two accumulation kernels take the range as a pointer that is null on the plain pass. Measured on a JUNGFRAU rotation dataset, non-adaptive path: azimuthal integration 0.02 -> 0.06 ms per image, exactly the 3x the extra passes predict, against a 0.34 ms per-image total. Note what the result IS: the smooth background under the peaks, not the bin mean. It should not be switched on where a ring's integrated intensity is wanted - the powder-ring geometry fit reads ring peaks, and those are what a clip is designed to remove. Off by default, so nothing changes unless it is asked for. Not exposed over the REST API - that needs the generated model regenerated, which is a separate step. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
177 lines
6.4 KiB
C++
177 lines
6.4 KiB
C++
// SPDX-FileCopyrightText: 2024 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
#include "AzimuthalIntegrationSettings.h"
|
|
#include "JFJochException.h"
|
|
|
|
#define check_max(param, val, max) if ((val) > (max)) throw JFJochException(JFJochExceptionCategory::InputParameterAboveMax, param)
|
|
#define check_min(param, val, min) if ((val) < (min)) throw JFJochException(JFJochExceptionCategory::InputParameterBelowMin, param)
|
|
#define check_finite(param, val) if (!std::isfinite(val)) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, param)
|
|
|
|
AzimuthalIntegrationSettings::AzimuthalIntegrationSettings() {
|
|
UpdateBinCount();
|
|
}
|
|
|
|
AzimuthalIntegrationSettings &AzimuthalIntegrationSettings::SolidAngleCorrection(bool input) {
|
|
solid_angle_correction = input;
|
|
return *this;
|
|
}
|
|
|
|
AzimuthalIntegrationSettings &AzimuthalIntegrationSettings::QRange_recipA(float low, std::optional<float> high) {
|
|
check_finite("Low Q for azimuthal integration", low);
|
|
check_min("Low Q for azimuthal integration", low, minQ_recipA);
|
|
// The low limit has to leave room for at least one bin below maxQ. This used to follow from the
|
|
// high limit being mandatory (high <= maxQ and high > low); once "unset" became legal, ResolveHighQ
|
|
// was left computing std::clamp(q, low + spacing, maxQ) with the lower bound above the upper one.
|
|
check_max("Low Q for azimuthal integration", low, maxQ_recipA - q_spacing);
|
|
if (high.has_value()) {
|
|
check_finite("High Q for azimuthal integration", *high);
|
|
check_max("High Q for azimuthal integration", *high, maxQ_recipA);
|
|
if (*high <= low)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"High Q must be higher than low Q");
|
|
}
|
|
|
|
requested_high_q_recipA = high;
|
|
low_q_recipA = low;
|
|
// Until ResolveHighQ runs, an unset limit keeps the value the bins were last built from.
|
|
high_q_recipA = high.value_or(high_q_recipA);
|
|
UpdateBinCount();
|
|
return *this;
|
|
}
|
|
|
|
void AzimuthalIntegrationSettings::ResolveHighQ(float detector_max_q_recipA) {
|
|
if (requested_high_q_recipA.has_value())
|
|
return;
|
|
|
|
high_q_recipA = std::clamp(detector_max_q_recipA, low_q_recipA + q_spacing, maxQ_recipA);
|
|
UpdateBinCount();
|
|
}
|
|
|
|
AzimuthalIntegrationSettings &AzimuthalIntegrationSettings::QSpacing_recipA(float input) {
|
|
check_finite("Q spacing for azimuthal integration", input);
|
|
check_min("Q spacing for azimuthal integration", input, minQ_recipA);
|
|
q_spacing = input;
|
|
UpdateBinCount();
|
|
return *this;
|
|
}
|
|
|
|
bool AzimuthalIntegrationSettings::IsSolidAngleCorrection() const {
|
|
return solid_angle_correction;
|
|
}
|
|
|
|
float AzimuthalIntegrationSettings::GetHighQ_recipA() const {
|
|
return high_q_recipA;
|
|
}
|
|
|
|
std::optional<float> AzimuthalIntegrationSettings::GetRequestedHighQ_recipA() const {
|
|
return requested_high_q_recipA;
|
|
}
|
|
|
|
float AzimuthalIntegrationSettings::GetLowQ_recipA() const {
|
|
return low_q_recipA;
|
|
}
|
|
|
|
float AzimuthalIntegrationSettings::GetQSpacing_recipA() const {
|
|
return q_spacing;
|
|
}
|
|
|
|
AzimuthalIntegrationSettings &AzimuthalIntegrationSettings::BkgEstimateQRange_recipA(float low, float high) {
|
|
check_finite("Low Q for background estimation", low);
|
|
check_finite("High Q for background estimation", high);
|
|
check_max("High Q for background estimation", high, maxQ_recipA);
|
|
check_min("Low Q for background estimation", low, minQ_recipA);
|
|
if (high <= low)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"High Q must be higher than low Q");
|
|
|
|
bkg_estimate_low_q_recipA = low;
|
|
bkg_estimate_high_q_recipA = high;
|
|
|
|
return *this;
|
|
}
|
|
|
|
float AzimuthalIntegrationSettings::GetBkgEstimateLowQ_recipA() const {
|
|
return bkg_estimate_low_q_recipA;
|
|
}
|
|
|
|
float AzimuthalIntegrationSettings::GetBkgEstimateHighQ_recipA() const {
|
|
return bkg_estimate_high_q_recipA;
|
|
}
|
|
|
|
AzimuthalIntegrationSettings &AzimuthalIntegrationSettings::PolarizationCorrection(bool input) {
|
|
polarization_correction = input;
|
|
return *this;
|
|
}
|
|
|
|
bool AzimuthalIntegrationSettings::IsPolarizationCorrection() const {
|
|
return polarization_correction;
|
|
}
|
|
|
|
AzimuthalIntegrationSettings &AzimuthalIntegrationSettings::AzimuthalBinCount(int32_t input) {
|
|
check_min("Azimuthal bin count", input, 1);
|
|
check_max("Azimuthal bin count", input, 512);
|
|
azim_bins = input;
|
|
UpdateBinCount();
|
|
return *this;
|
|
}
|
|
|
|
AzimuthalIntegrationSettings &AzimuthalIntegrationSettings::SigmaClip(float input) {
|
|
check_min("Azimuthal integration sigma clip", input, 0.0f);
|
|
// Below ~2 sigma a clip rejects a large part of a clean Gaussian bin, which biases the estimate
|
|
// low rather than removing outliers - so a value in (0, 2) is a mistake, not a tight setting.
|
|
if (input > 0.0f && input < 2.0f)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Azimuthal integration sigma clip must be 0 (off) or at least 2");
|
|
sigma_clip_nsigma = input;
|
|
return *this;
|
|
}
|
|
|
|
float AzimuthalIntegrationSettings::GetSigmaClip() const {
|
|
return sigma_clip_nsigma;
|
|
}
|
|
|
|
AzimuthalIntegrationSettings &AzimuthalIntegrationSettings::ForceCPUinFPGAWorkflow(bool input) {
|
|
force_cpu_in_fpga_workflow = input;
|
|
return *this;
|
|
}
|
|
|
|
bool AzimuthalIntegrationSettings::IsForceCPUinFPGAWorkflow() const {
|
|
return force_cpu_in_fpga_workflow;
|
|
}
|
|
|
|
int32_t AzimuthalIntegrationSettings::GetQBinCount() const {
|
|
return q_bins;
|
|
}
|
|
|
|
int32_t AzimuthalIntegrationSettings::GetAzimuthalBinCount() const {
|
|
return azim_bins;
|
|
}
|
|
|
|
int32_t AzimuthalIntegrationSettings::GetBinCount() const {
|
|
return total_bins;
|
|
}
|
|
|
|
uint16_t AzimuthalIntegrationSettings::QToBin(float q) const {
|
|
return std::min<uint16_t>(GetBinCount() - 1, std::floor(std::max(0.0f, (q - GetLowQ_recipA()) / GetQSpacing_recipA())));
|
|
}
|
|
|
|
uint16_t AzimuthalIntegrationSettings::GetBin(float q, float phi_deg) const {
|
|
if (q < low_q_recipA || q >= high_q_recipA)
|
|
return UINT16_MAX;
|
|
if (phi_deg < 0.0 || phi_deg >= 360)
|
|
return UINT16_MAX;
|
|
|
|
int16_t q_bin = std::floor((q - low_q_recipA) / q_spacing);
|
|
int16_t phi_bin = std::floor(phi_deg / 360.0f * azim_bins);
|
|
return q_bin + phi_bin * GetQBinCount();
|
|
}
|
|
|
|
void AzimuthalIntegrationSettings::UpdateBinCount() {
|
|
q_bins = std::ceil((high_q_recipA - low_q_recipA) / q_spacing);
|
|
total_bins = q_bins * azim_bins;
|
|
}
|