Build Packages / build:windows:nocuda (push) Successful in 20m4s
Build Packages / Unit tests (push) Skipped
Build Packages / build:viewer-tgz:cpu (push) Successful in 16m5s
Build Packages / build:viewer-tgz:cuda (push) Successful in 17m26s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 27m46s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 20m17s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 26m13s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 23m17s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 28m11s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m30s
Build Packages / build:rpm (rocky8) (push) Successful in 24m34s
Build Packages / build:rpm (rocky9) (push) Successful in 21m30s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 23m33s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 20m18s
Build Packages / DIALS test (push) Successful in 18m23s
Build Packages / XDS test (durin plugin) (push) Successful in 11m30s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 10m16s
Build Packages / XDS test (neggia plugin) (push) Successful in 8m2s
Build Packages / Generate python client (push) Successful in 49s
Build Packages / Build documentation (push) Successful in 1m21s
Build Packages / Create release (push) Skipped
Build Packages / build:windows:cuda (push) Successful in 29m45s
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. * **rugnux: significantly better quality of results, and faster.** A large rework of integration, scaling, merging, geometry refinement and space-group determination, together with measurements the program previously made no attempt at - the direct beam before indexing, the beam stop, the goniometer rotation scale, and the stretches of a sweep the crystal did not deliver. A rotation dataset typically gains observations at better <I/sigma> and R_meas, and every `mx` and `scale` run writes a `<prefix>_report.txt` results report modelled on XDS's `CORRECT.LP`. Many defaults moved with it: spot detection is self-calibrating, beam-stop detection and rotation geometry post-refinement are on, resolution limits default to as far as the detector reaches, and ice-ring handling engages only where the crystal is measured to have ice. * **jfjoch_viewer:** the beam-stop shadow, the detector calibration and the beam-centre measurement are reachable from "Analyze dataset"; the settings panel reports how the sample moved and how polarized the beam was; image rendering and interaction are faster. * **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. * **Broker, writer, packaging and build:** image-slot lifetime and locking fixes, per-image datasets sized by the images actually written, the Debian/Ubuntu broker package renamed to `jfjoch`, and `image_analysis` compiling under MSVC again. **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. * `-t`/`--stride` is **refused on rotation data**: skipping frames cuts every reflection's rocking curve, so the combined fulls and their partiality would be measured over frames the sweep never recorded. Select a contiguous range with `-s`/`-e` instead. `--mode azint` and `--force-still` still take a stride. **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`, `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. * `_reflns.jfjoch_diffrn_ISa` now carries the whole-range `1/sqrt(a*b)` that XDS's ISa denotes, and the error-model `a` and `b` are reported in XDS's convention; the strong-reflection asymptote moves to `_reflns.jfjoch_diffrn_ISa_asymptotic`. **A file written by an earlier version carries the asymptote under the plain `ISa` name.** Reviewed-on: #71 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
335 lines
14 KiB
Plaintext
335 lines
14 KiB
Plaintext
// 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 "BraggPredictionRotGPU.h"
|
|
|
|
#ifdef JFJOCH_USE_CUDA
|
|
#include "../indexing/CUDAMemHelpers.h"
|
|
#include <cuda_runtime.h>
|
|
#include <cmath>
|
|
|
|
namespace {
|
|
__host__ __device__ inline bool is_odd(int v) { return (v & 1) != 0; }
|
|
|
|
__host__ __device__ inline void cross3(float ax, float ay, float az,
|
|
float bx, float by, float bz,
|
|
float &cx, float &cy, float &cz) {
|
|
cx = ay * bz - az * by;
|
|
cy = az * bx - ax * bz;
|
|
cz = ax * by - ay * bx;
|
|
}
|
|
|
|
__host__ __device__ inline float dot3(float ax, float ay, float az,
|
|
float bx, float by, float bz) {
|
|
return ax * bx + ay * by + az * bz;
|
|
}
|
|
|
|
__host__ __device__ inline void normalize3(float &x, float &y, float &z) {
|
|
float len = sqrtf(x * x + y * y + z * z);
|
|
if (len < 1e-12f) { x = 0.0f; y = 0.0f; z = 0.0f; return; }
|
|
float inv = 1.0f / len;
|
|
x *= inv; y *= inv; z *= inv;
|
|
}
|
|
|
|
__device__ inline int compute_reflections_rot(const KernelConstsRot &C, int h, int k, int l, Reflection out[2]) {
|
|
if (h == 0 && k == 0 && l == 0)
|
|
return 0;
|
|
|
|
switch (C.centering) {
|
|
case 'I':
|
|
if (is_odd(h + k + l)) return false;
|
|
break;
|
|
case 'A':
|
|
if (is_odd(k + l)) return false;
|
|
break;
|
|
case 'B':
|
|
if (is_odd(h + l)) return false;
|
|
break;
|
|
case 'C':
|
|
if (is_odd(h + k)) return false;
|
|
break;
|
|
case 'F':
|
|
if (is_odd(h + k) || is_odd(h + l) || is_odd(k + l)) return false;
|
|
break;
|
|
case 'R': {
|
|
int mod = (-h + k + l) % 3;
|
|
if (mod < 0) mod += 3;
|
|
if (mod != 0) return false;
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// p0 = A* h + B* k + C* l
|
|
float p0x = C.Astar.x * h + C.Bstar.x * k + C.Cstar.x * l;
|
|
float p0y = C.Astar.y * h + C.Bstar.y * k + C.Cstar.y * l;
|
|
float p0z = C.Astar.z * h + C.Bstar.z * k + C.Cstar.z * l;
|
|
|
|
float p0_sq = p0x * p0x + p0y * p0y + p0z * p0z;
|
|
if (p0_sq <= 0.0f || p0_sq > C.one_over_dmax_sq)
|
|
return 0;
|
|
|
|
float p0_m1 = p0x * C.m1.x + p0y * C.m1.y + p0z * C.m1.z;
|
|
float p0_m2 = p0x * C.m2.x + p0y * C.m2.y + p0z * C.m2.z;
|
|
float p0_m3 = p0x * C.m3.x + p0y * C.m3.y + p0z * C.m3.z;
|
|
|
|
float rho_sq = p0_sq - (p0_m2 * p0_m2);
|
|
float p_m3 = (-p0_sq / 2.0f - p0_m2 * C.m2_S0) / C.m3_S0;
|
|
float p_m2 = p0_m2;
|
|
|
|
if (rho_sq < p_m3 * p_m3) return 0;
|
|
if (p0_sq > 4.0f * dot3(C.S0.x, C.S0.y, C.S0.z, C.S0.x, C.S0.y, C.S0.z)) return 0;
|
|
|
|
float p_m1_pos = sqrtf(rho_sq - p_m3 * p_m3);
|
|
float p_m1_arr[2] = {p_m1_pos, -p_m1_pos};
|
|
|
|
// Effective rocking width: mosaicity broadened by the bandwidth term (consistent with CPU),
|
|
// dtheta = (dlambda/lambda) tan(theta_B) in quadrature with sigma_M, zeta-free.
|
|
float mos_eff_rad = C.mos_angle_rad;
|
|
if (C.bandwidth_sigma > 0.0f) {
|
|
float sin_theta = C.half_wavelength_A * sqrtf(p0_sq);
|
|
float dphi_bw = C.bandwidth_sigma * sin_theta / sqrtf(1.0f - sin_theta * sin_theta);
|
|
mos_eff_rad = sqrtf(C.mos_angle_rad * C.mos_angle_rad + dphi_bw * dphi_bw);
|
|
}
|
|
|
|
int count = 0;
|
|
for (int idx = 0; idx < 2; ++idx) {
|
|
float p_m1 = p_m1_arr[idx];
|
|
|
|
float cosphi = (p_m1 * p0_m1 + p_m3 * p0_m3) / rho_sq;
|
|
float sinphi = (p_m1 * p0_m3 - p_m3 * p0_m1) / rho_sq;
|
|
|
|
float px = C.m1.x * p_m1 + C.m2.x * p_m2 + C.m3.x * p_m3;
|
|
float py = C.m1.y * p_m1 + C.m2.y * p_m2 + C.m3.y * p_m3;
|
|
float pz = C.m1.z * p_m1 + C.m2.z * p_m2 + C.m3.z * p_m3;
|
|
|
|
float Sx = C.S0.x + px;
|
|
float Sy = C.S0.y + py;
|
|
float Sz = C.S0.z + pz;
|
|
|
|
float phi = -1.0f * atan2f(sinphi, cosphi);
|
|
|
|
// e1 = normalize(S x S0) - direction perpendicular to both S and S0
|
|
float e1x, e1y, e1z;
|
|
cross3(Sx, Sy, Sz, C.S0.x, C.S0.y, C.S0.z, e1x, e1y, e1z);
|
|
normalize3(e1x, e1y, e1z);
|
|
|
|
// zeta = |m2 · e1| - the "lorentz-like" geometric factor for partiality
|
|
float zeta_abs = fabsf(dot3(C.m2.x, C.m2.y, C.m2.z, e1x, e1y, e1z));
|
|
|
|
// Check min_zeta threshold (consistent with CPU)
|
|
if (zeta_abs < C.min_zeta)
|
|
continue;
|
|
|
|
// epsilon3 cutoff check (consistent with CPU, Kabsch formulation): measured against the
|
|
// frame's EDGE, not its centre, or a reflection whose rocking curve overlaps the exposure
|
|
// is rejected because its exact diffracting condition falls outside it - and since the
|
|
// nearest frame centre is at most half a wedge away, it is then rejected on every frame
|
|
// and lost entirely. See the CPU engine for the regime that reaches.
|
|
float epsilon3 = fabsf(phi * zeta_abs) - 0.5f * C.wedge_angle_rad * zeta_abs;
|
|
if (epsilon3 > C.mosaicity_multiplier * mos_eff_rad)
|
|
continue;
|
|
|
|
float cx, cy, cz;
|
|
cross3(Sx, Sy, Sz, C.S0.x, C.S0.y, C.S0.z, cx, cy, cz);
|
|
// Reciprocal Lorentz (Kabsch 2010): |m2 . (S x S0)| / (|S| |S0|) = zeta * sin(2theta).
|
|
// Dividing by the scalar product S.S0 = |S||S0|cos(2theta) would add a spurious
|
|
// 1/cos(2theta) to the absolute scale.
|
|
float S_len = sqrtf(dot3(Sx, Sy, Sz, Sx, Sy, Sz));
|
|
float S0_len = sqrtf(dot3(C.S0.x, C.S0.y, C.S0.z, C.S0.x, C.S0.y, C.S0.z));
|
|
float lorentz = fabsf(dot3(C.m2.x, C.m2.y, C.m2.z, cx, cy, cz)) / (S_len * S0_len);
|
|
|
|
// Partiality calculation (Kabsch formulation)
|
|
// c1 = sqrt(2) * sigma / zeta, where sigma = mosaicity
|
|
float c1 = zeta_abs / (sqrtf(2.0f) * mos_eff_rad);
|
|
float half_wedge = C.wedge_angle_rad / 2.0f;
|
|
float partiality = (erff((phi + half_wedge) * c1)
|
|
- erff((phi - half_wedge) * c1)) / 2.0f;
|
|
|
|
|
|
// Use S (rotated) for projection
|
|
float Srx = C.rot[0] * Sx + C.rot[1] * Sy + C.rot[2] * Sz;
|
|
float Sry = C.rot[3] * Sx + C.rot[4] * Sy + C.rot[5] * Sz;
|
|
float Srz = C.rot[6] * Sx + C.rot[7] * Sy + C.rot[8] * Sz;
|
|
|
|
if (Srz <= 0.0f) continue;
|
|
|
|
float coeff = C.coeff_const / Srz;
|
|
float x = C.beam_x + Srx * coeff;
|
|
float y = C.beam_y + Sry * coeff;
|
|
|
|
if (x < 0.0f || x >= C.det_width_pxl || y < 0.0f || y >= C.det_height_pxl)
|
|
continue;
|
|
|
|
float dist_ewald = fabsf(sqrtf(Sx * Sx + Sy * Sy + Sz * Sz) - C.one_over_wavelength);
|
|
|
|
out[count].h = h;
|
|
out[count].k = k;
|
|
out[count].l = l;
|
|
out[count].delta_phi_deg = phi * 180.0 / PI;
|
|
out[count].predicted_x = x;
|
|
out[count].predicted_y = y;
|
|
out[count].observed_x = NAN;
|
|
out[count].observed_y = NAN;
|
|
out[count].d = 1.0f / sqrtf(p0_sq);
|
|
out[count].dist_ewald = dist_ewald;
|
|
out[count].rlp = lorentz;
|
|
out[count].partiality = partiality;
|
|
out[count].zeta = zeta_abs;
|
|
out[count].image_scale_corr = lorentz / partiality;
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
__global__ void bragg_rot_kernel_3d(const KernelConstsRot *__restrict__ kc,
|
|
int max_h, int max_k, int max_l,
|
|
int max_reflections,
|
|
Reflection *__restrict__ out,
|
|
int *__restrict__ counter) {
|
|
int hi = blockIdx.x * blockDim.x + threadIdx.x;
|
|
int ki = blockIdx.y * blockDim.y + threadIdx.y;
|
|
int li = blockIdx.z * blockDim.z + threadIdx.z;
|
|
if (hi > 2 * max_h || ki > 2 * max_k || li > 2 * max_l) return;
|
|
int h = hi - max_h;
|
|
int k = ki - max_k;
|
|
int l = li - max_l;
|
|
|
|
Reflection r[2];
|
|
int n = compute_reflections_rot(*kc, h, k, l, r);
|
|
|
|
for (int i = 0; i < n; ++i) {
|
|
// Do NOT clamp the counter back down on overflow: it then saturates at the capacity and the
|
|
// host cannot tell a full buffer from an overflowing one. Let it count the true total.
|
|
const int pos = atomicAdd(counter, 1);
|
|
if (pos < max_reflections)
|
|
out[pos] = r[i];
|
|
}
|
|
}
|
|
|
|
inline KernelConstsRot BuildKernelConstsRot(const DiffractionExperiment &experiment,
|
|
const CrystalLattice &lattice,
|
|
const BraggPredictionSettings &settings) {
|
|
KernelConstsRot kc{};
|
|
auto geom = experiment.GetDiffractionGeometry();
|
|
|
|
kc.det_width_pxl = static_cast<float>(experiment.GetXPixelsNum());
|
|
kc.det_height_pxl = static_cast<float>(experiment.GetYPixelsNum());
|
|
kc.beam_x = geom.GetBeamX_pxl();
|
|
kc.beam_y = geom.GetBeamY_pxl();
|
|
kc.coeff_const = geom.GetDetectorDistance_mm() / geom.GetPixelSize_mm();
|
|
|
|
float one_over_dmax = 1.0f / settings.high_res_A;
|
|
kc.one_over_dmax_sq = one_over_dmax * one_over_dmax;
|
|
kc.one_over_wavelength = 1.0f / geom.GetWavelength_A();
|
|
|
|
// Store mosaicity and wedge in radians for partiality calculation
|
|
kc.mos_angle_rad = settings.mosaicity_deg * static_cast<float>(PI) / 180.0f;
|
|
kc.wedge_angle_rad = settings.wedge_deg * static_cast<float>(PI) / 180.0f;
|
|
kc.min_zeta = settings.min_zeta;
|
|
kc.mosaicity_multiplier = settings.mosaicity_multiplier;
|
|
kc.bandwidth_sigma = settings.bandwidth_sigma;
|
|
kc.half_wavelength_A = geom.GetWavelength_A() / 2.0f;
|
|
|
|
kc.Astar = lattice.Astar();
|
|
kc.Bstar = lattice.Bstar();
|
|
kc.Cstar = lattice.Cstar();
|
|
kc.S0 = geom.GetScatteringVector();
|
|
|
|
auto rotT = geom.GetPoniRotMatrix().transpose().arr();
|
|
for (int i = 0; i < 9; ++i) kc.rot[i] = rotT[i];
|
|
|
|
kc.centering = settings.centering;
|
|
return kc;
|
|
}
|
|
|
|
inline void BuildGoniometerBasis(const DiffractionExperiment &experiment, KernelConstsRot &kc) {
|
|
const auto gon_opt = experiment.GetGoniometer();
|
|
if (!gon_opt.has_value())
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"BraggPredictionRotationGPU requires a goniometer axis");
|
|
const GoniometerAxis &gon = *gon_opt;
|
|
|
|
// m2 = normalize(axis)
|
|
float m2x = gon.GetAxis().x;
|
|
float m2y = gon.GetAxis().y;
|
|
float m2z = gon.GetAxis().z;
|
|
normalize3(m2x, m2y, m2z);
|
|
|
|
// m1 = normalize(m2 x S0)
|
|
float m1x, m1y, m1z;
|
|
cross3(m2x, m2y, m2z, kc.S0.x, kc.S0.y, kc.S0.z, m1x, m1y, m1z);
|
|
normalize3(m1x, m1y, m1z);
|
|
|
|
// m3 = normalize(m1 x m2)
|
|
float m3x, m3y, m3z;
|
|
cross3(m1x, m1y, m1z, m2x, m2y, m2z, m3x, m3y, m3z);
|
|
normalize3(m3x, m3y, m3z);
|
|
|
|
kc.m1 = Coord(m1x, m1y, m1z);
|
|
kc.m2 = Coord(m2x, m2y, m2z);
|
|
kc.m3 = Coord(m3x, m3y, m3z);
|
|
kc.m2_S0 = dot3(m2x, m2y, m2z, kc.S0.x, kc.S0.y, kc.S0.z);
|
|
kc.m3_S0 = dot3(m3x, m3y, m3z, kc.S0.x, kc.S0.y, kc.S0.z);
|
|
}
|
|
} // namespace
|
|
|
|
BraggPredictionRotGPU::BraggPredictionRotGPU(int max_reflections)
|
|
: BraggPrediction(max_reflections),
|
|
reg_out(reflections), d_out(max_reflections),
|
|
dK(1), d_count(1), h_count(1) {
|
|
}
|
|
|
|
// The host buffer is page-locked and the device buffer is sized to match it, so both are rebuilt.
|
|
void BraggPredictionRotGPU::GrowCapacity(int count) {
|
|
reg_out = CudaRegisteredVector<Reflection>(); // unregister before the vector reallocates
|
|
BraggPrediction::GrowCapacity(count);
|
|
reg_out = CudaRegisteredVector<Reflection>(reflections);
|
|
d_out = CudaDevicePtr<Reflection>(count);
|
|
}
|
|
|
|
int BraggPredictionRotGPU::Calc(const DiffractionExperiment &experiment,
|
|
const CrystalLattice &lattice,
|
|
const BraggPredictionSettings &settings) {
|
|
KernelConstsRot hK = BuildKernelConstsRot(experiment, lattice, settings);
|
|
BuildGoniometerBasis(experiment, hK);
|
|
|
|
cudaMemcpyAsync(dK, &hK, sizeof(KernelConstsRot), cudaMemcpyHostToDevice, stream);
|
|
cudaMemsetAsync(d_count, 0, sizeof(int), stream);
|
|
|
|
// Inclusive on both ends, matching the kernel's own bounds and the CPU loops (-max_i .. +max_i).
|
|
dim3 block(8, 8, 8);
|
|
dim3 grid((2 * settings.max_h + 1 + block.x - 1) / block.x,
|
|
(2 * settings.max_k + 1 + block.y - 1) / block.y,
|
|
(2 * settings.max_l + 1 + block.z - 1) / block.z);
|
|
|
|
bragg_rot_kernel_3d<<<grid, block, 0, stream>>>(dK, settings.max_h, settings.max_k, settings.max_l, max_reflections, d_out, d_count);
|
|
|
|
cudaMemcpyAsync(h_count, d_count, sizeof(int), cudaMemcpyDeviceToHost, stream);
|
|
cudaStreamSynchronize(stream);
|
|
|
|
int count = *h_count.get();
|
|
if (count > max_reflections) {
|
|
// The buffer holds an arbitrary subset of what was predicted (whichever slots the atomics
|
|
// reached first), so it cannot be used. Grow to fit and predict again; the buffer stays grown,
|
|
// so a run pays for this a handful of times at most.
|
|
GrowCapacity(count);
|
|
cudaMemsetAsync(d_count, 0, sizeof(int), stream);
|
|
bragg_rot_kernel_3d<<<grid, block, 0, stream>>>(dK, settings.max_h, settings.max_k, settings.max_l, max_reflections, d_out, d_count);
|
|
cudaMemcpyAsync(h_count, d_count, sizeof(int), cudaMemcpyDeviceToHost, stream);
|
|
cudaStreamSynchronize(stream);
|
|
count = std::min(*h_count.get(), max_reflections);
|
|
}
|
|
if (count == 0) return 0;
|
|
|
|
cudaMemcpyAsync(reflections.data(), d_out, sizeof(Reflection) * count, cudaMemcpyDeviceToHost, stream);
|
|
cudaStreamSynchronize(stream);
|
|
|
|
return TruncateToOutput(count);
|
|
}
|
|
|
|
#endif
|