Files
Jungfraujoch/image_analysis/bragg_prediction/BraggPrediction.cpp
T
leonarski_f b00c668e3a
Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m11s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 10m32s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 18m26s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 22m11s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 23m32s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 25m36s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 27m28s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 28m10s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 28m10s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 29m24s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 21m45s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / Build documentation (push) Successful in 1m21s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 24m2s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 29m25s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 30m27s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 27m12s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 32m20s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 33m36s
Build Packages / Unit tests (push) Successful in 2h7m18s
Build Packages / Validation Battery (push) Successful in 33m19s
v1.0.0-rc.179 (#90)
* Rugnux: Stills processing needs only the unit cell and space group beyond what the file records - detector distance, beam centre, tilt, panel placement, saturation level, gain and polarisation plane are found from the images - and is up to twice as fast.
* Rugnux: Stills can be indexed without a GPU, with a CPU implementation of the fast-feedback indexer (macOS, computers without a GPU).
* Rugnux: Rotation and stills data are integrated in the detector's own units with one gain model, improving data from CCD detectors.
* Web frontend: The Rugnux live page shows the placed model with its electron density and anomalous difference map in a Mol* viewer.

Reviewed-on: #90
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-10-24 14:52:29 +02:00

238 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <algorithm>
#include "../../common/JFJochMath.h"
#include "../../common/Logger.h"
#include "BraggPrediction.h"
#include "../SensorAbsorption.h"
#include "../bragg_integration/SystematicAbsence.h"
void BraggPrediction::GrowCapacity(int count) {
reflections.resize(count);
max_reflections = count;
}
void BraggPrediction::OrderOutput(int count) {
order_keys.resize(count);
for (int i = 0; i < count; i++) {
const Reflection &r = reflections[i];
order_keys[i] = {r.h, r.k, r.l, r.delta_phi_deg, i};
}
std::sort(order_keys.begin(), order_keys.end(),
[](const OrderKey &a, const OrderKey &b) {
if (a.h != b.h) return a.h < b.h;
if (a.k != b.k) return a.k < b.k;
if (a.l != b.l) return a.l < b.l;
return a.delta_phi_deg < b.delta_phi_deg;
});
order_scratch.resize(count);
for (int i = 0; i < count; i++) order_scratch[i] = reflections[order_keys[i].index];
std::copy(order_scratch.begin(), order_scratch.end(), reflections.begin());
}
int BraggPrediction::TruncateToOutput(int count) {
if (count <= output_limit)
return count;
// Partiality first, then excitation error: exactly one of the two says anything on each path. On the
// rotation path dist_ewald is identically zero - BraggPredictionRot picks the rocking coordinate so
// that 2*S0.p + p.p = 0, i.e. |S| = 1/lambda exactly - so ranking by it there left an hkl-lexicographic
// prefix rather than the best-recorded reflections. On the still path every partiality is 1, so the
// ranking falls through to dist_ewald, which is what it has always been.
std::partial_sort(reflections.begin(), reflections.begin() + output_limit,
reflections.begin() + count,
[](const Reflection &a, const Reflection &b) {
if (a.partiality != b.partiality) return a.partiality > b.partiality;
if (a.dist_ewald != b.dist_ewald) return a.dist_ewald < b.dist_ewald;
if (a.h != b.h) return a.h < b.h;
if (a.k != b.k) return a.k < b.k;
return a.l < b.l;
});
return output_limit;
}
namespace {
// Number of bandwidth sigmas included in the (radially thickened) Ewald-shell
// acceptance window. 3σ captures essentially the whole pink-beam smear; matches
// the conservative end of the mosaicity cutoff used by callers.
constexpr float kBandwidthCutoffSigmas = 3.0f;
}
BraggPrediction::BraggPrediction(int max_reflections)
: max_reflections(max_reflections), reflections(max_reflections) {}
const std::vector<Reflection> &BraggPrediction::GetReflections() const {
return reflections;
}
int BraggPrediction::Calc(const DiffractionExperiment &experiment, const CrystalLattice &lattice,
const BraggPredictionSettings &settings) {
const auto geom = experiment.GetDiffractionGeometry();
// The plane the panels span (without panels, the image): a quick reject, a reflection in a gap
// between panels is dropped when it is integrated.
const PlaneBox bounds = experiment.GetModules().PlaneBounds(experiment.GetXPixelsNum(), experiment.GetYPixelsNum());
const auto det_x0 = static_cast<float>(bounds.x0), det_x1 = static_cast<float>(bounds.x1);
const auto det_y0 = static_cast<float>(bounds.y0), det_y1 = static_cast<float>(bounds.y1);
const float one_over_dmax = 1.0f / settings.high_res_A;
const float one_over_dmax_sq = one_over_dmax * one_over_dmax;
float one_over_wavelength = 1.0f / geom.GetWavelength_A();
const Coord Astar = lattice.Astar();
const Coord Bstar = lattice.Bstar();
const Coord Cstar = lattice.Cstar();
const Coord S0 = geom.GetScatteringVector();
std::vector<float> rot = geom.GetDetectorMatrix().transpose().arr();
// Precompute detector geometry constants
float beam_x = geom.GetBeamX_pxl();
float beam_y = geom.GetBeamY_pxl();
float det_distance = geom.GetDetectorDistance_mm();
float pixel_size = geom.GetPixelSize_mm();
float F = det_distance / pixel_size;
// Angle-dependent sensor efficiency; per-dataset constants collapsed to two numbers. Inert
// wherever the sensor is opaque, which is every long wavelength.
const auto &det_setup = experiment.GetDetectorSetup();
const auto sensor_qe = sensor_absorption::SensorQE::Build(
det_setup.GetSensorMaterial(), det_setup.GetSensorThickness_um(), geom.GetWavelength_A());
// The air in the sample-to-pixel flight path carries the same cos(alpha) dependence, with the
// opposite sign. See sensor_absorption::FlightPathAttenuation; off leaves d_over_L at 0, which is
// bit-identical to not applying it.
const auto air = sensor_absorption::FlightPathAttenuation::Build(
experiment.GetBraggIntegrationSettings().GetFlightPath(), det_distance,
geom.GetWavelength_A());
const float epsilon = 1e-5f;
const float s0_sq = S0 * S0;
const float rad_to_deg = 180.0f / static_cast<float>(PI);
int i = 0;
for (int h = -settings.max_h; h <= settings.max_h; h++) {
// Precompute A* h contribution
const float Ah_x = Astar.x * h;
const float Ah_y = Astar.y * h;
const float Ah_z = Astar.z * h;
for (int k = -settings.max_k; k <= settings.max_k; k++) {
// Accumulate B* k contribution
const float AhBk_x = Ah_x + Bstar.x * k;
const float AhBk_y = Ah_y + Bstar.y * k;
const float AhBk_z = Ah_z + Bstar.z * k;
for (int l = -settings.max_l; l <= settings.max_l; l++) {
if (systematic_absence(h, k, l, settings.centering))
continue;
float recip_x = AhBk_x + Cstar.x * l;
float recip_y = AhBk_y + Cstar.y * l;
float recip_z = AhBk_z + Cstar.z * l;
float recip_sq = recip_x * recip_x + recip_y * recip_y + recip_z * recip_z;
if (recip_sq > one_over_dmax_sq)
continue;
float S_x = recip_x + S0.x;
float S_y = recip_y + S0.y;
float S_z = recip_z + S0.z;
float S_len = sqrtf(S_x * S_x + S_y * S_y + S_z * S_z);
float dist_ewald_sphere = std::fabs(S_len - one_over_wavelength);
// Energy bandwidth thickens the Ewald shell radially: at the
// diffraction condition |S|-1/λ shifts by recip_z·(Δλ/λ), i.e.
// σ_bw = |recip_z|·bandwidth_sigma (= bλ/2d²). Broaden the acceptance
// window in quadrature so high-resolution shells (smeared most, ∝1/d²)
// are not clipped.
float radial_cutoff = settings.ewald_dist_cutoff;
if (settings.bandwidth_sigma > 0.0f) {
const float bw_tol = kBandwidthCutoffSigmas * settings.bandwidth_sigma * std::fabs(recip_z);
radial_cutoff = std::sqrt(radial_cutoff * radial_cutoff + bw_tol * bw_tol);
}
if (dist_ewald_sphere <= radial_cutoff ) {
const float s0_p0 = S0.x * recip_x + S0.y * recip_y + S0.z * recip_z;
const float val = s0_sq * recip_sq - s0_p0 * s0_p0;
float delta_phi_deg = NAN;
if (std::fabs(val) >= epsilon && s0_sq > epsilon) {
const float a_num = (s0_sq - 0.25f * recip_sq) * recip_sq;
if (a_num >= 0.0f) {
const float A = std::sqrt(a_num / val);
const float B = (A * s0_p0 + 0.5f * recip_sq) / s0_sq;
const float p_star_x = A * recip_x - B * S0.x;
const float p_star_y = A * recip_y - B * S0.y;
const float p_star_z = A * recip_z - B * S0.z;
const float p_star_sq = p_star_x * p_star_x + p_star_y * p_star_y + p_star_z * p_star_z;
const float denom = std::sqrt(p_star_sq * recip_sq);
if (denom >= epsilon) {
float c = (p_star_x * recip_x + p_star_y * recip_y + p_star_z * recip_z) / denom;
c = std::fmax(-1.0f, std::fmin(1.0f, c));
delta_phi_deg = std::acos(c) * rad_to_deg;
}
}
}
// Inlined RecipToDetector: the full transposed detector matrix, tilt and discrete orientation
// Apply rotation matrix transpose
float S_rot_x = rot[0] * S_x + rot[1] * S_y + rot[2] * S_z;
float S_rot_y = rot[3] * S_x + rot[4] * S_y + rot[5] * S_z;
float S_rot_z = rot[6] * S_x + rot[7] * S_y + rot[8] * S_z;
if (S_rot_z <= 0)
continue;
// Project to detector coordinates
// Assume detector is along x,y,z coordinates after rotation
float x = beam_x + F * S_rot_x / S_rot_z;
float y = beam_y + F * S_rot_y / S_rot_z;
if ((x < det_x0) || (x >= det_x1) || (y < det_y0) || (y >= det_y1))
continue;
// Sensor quantum efficiency at this reflection's angle of incidence on the
// detector - the diffracted direction against the detector normal, so the
// detector tilt is carried for free. See sensor_absorption::SensorQE.
const float cos_alpha =
S_rot_z / std::sqrt(S_x * S_x + S_y * S_y + S_z * S_z);
const float qe_corr = sensor_qe.Factor(cos_alpha);
const float flight_corr = air.Factor(cos_alpha);
if (i == max_reflections)
GrowCapacity(2 * max_reflections);
float d = 1.0f / sqrtf(recip_sq);
reflections[i] = Reflection{
.h = h,
.k = k,
.l = l,
.delta_phi_deg = delta_phi_deg,
.predicted_x = x,
.predicted_y = y,
.observed_x = NAN,
.observed_y = NAN,
.d = d,
.dist_ewald = dist_ewald_sphere,
.prescaling_corr = 1.0f,
.qe_corr = qe_corr,
.flight_corr = flight_corr,
.partiality = 1.0f,
.zeta = 1.0,
.image_scale_corr = qe_corr * flight_corr
};
++i;
}
}
}
}
return TruncateToOutput(i);
}