Build Packages / build:rpm (rocky9_sls9) (push) Successful in 18m57s
Build Packages / Unit tests (push) Skipped
Build Packages / build:windows:nocuda (push) Successful in 16m55s
Build Packages / build:windows:cuda (push) Successful in 18m48s
Build Packages / build:viewer-tgz:cpu (push) Successful in 13m10s
Build Packages / build:viewer-tgz:cuda (push) Successful in 14m45s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 22m23s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 20m12s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 23m7s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 20m43s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 23m9s
Build Packages / XDS test (durin plugin) (push) Successful in 12m26s
Build Packages / build:rpm (rocky9) (push) Successful in 24m58s
Build Packages / Generate python client (push) Successful in 50s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 23m20s
Build Packages / Create release (push) Skipped
Build Packages / XDS test (JFJoch plugin) (push) Successful in 12m37s
Build Packages / build:rpm (rocky8) (push) Successful in 27m58s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 25m38s
Build Packages / Build documentation (push) Successful in 59s
Build Packages / DIALS test (push) Successful in 23m16s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m38s
**Files written by Jungfraujoch now import correctly in DIALS, XDS and pyFAI.** A tilted detector, a grid scan, a still recorded at a goniometer position, and saturated or unreadable pixels were each described in a way that a third-party program acted on wrongly. If you process Jungfraujoch data outside Jungfraujoch, prefer this release to any earlier one. * HDF5: the detector tilt (`rot1`/`rot2`/`rot3`) is exported correctly in the NXmx transformation chain; untilted geometries are unaffected. * HDF5: a still recorded at a goniometer position is no longer read back as a single image, and a grid scan records a stationary spindle so a program that requires a rotation axis can open it. * HDF5: the sample transformation chain is written in mounting order, with a Smargon head position told apart from the spindle, one entry per image, `module_offset` as a float unit vector, and `offset_units` on every offset. * HDF5: saturated, underloaded and unreadable pixels are described so a downstream program masks them - `saturation_value`, `underload_value`, `error_value` and `bit_depth_readout` are written correctly, and a data file missing next to a VDS master reads as the error marker rather than as zero counts. * HDF5: the rotation axis is read back under whatever name it carries, and `mirror_y` records whether the assembled image is mirrored in Y relative to the detector's raw readout. * A grid scan and a goniometer axis can both be set; they are no longer alternatives. * `images_per_file` is chosen from the acquisition when it is not given: a rotation sweep of at most 20000 images goes into a single data file, a grid scan splits on whole fast-axis rows, and stills and serial keep 1000. * The writer refuses a stream whose start message declares a different pixel format than its images carry, and a DECTRIS detector sending signed images is no longer declared unsigned. * The image stream can carry the sample transformation chain (`transformations`, in the END message); a producer that does not send it gets the same chain built by the writer. * rugnux: fixing the space group with `-S` no longer prevents the lattice from being found - a lattice indexed in a different setting is reindexed into that group's own setting, and a run whose crystal does not have that group's lattice stops and names the cell it indexed as, rather than reporting statistics that cannot describe it. * rugnux: the per-image resolution estimate now predicts the resolution the merged data reach rather than the highest-resolution spot found, and is reported as `SPOT_RESOLUTION_ESTIMATE`. * rugnux: two runs of the same command on the same images produce the same merged intensities; the azimuthal profile written alongside them is not yet reproducible in the same way. * rugnux: the offline lattice refinement is bounded by iterations rather than by a wall clock, so a loaded machine can no longer refine to a different lattice; a live acquisition keeps its real-time bound. * rugnux: the detector-frame modulation correction is fitted on a grid spanning the detector, so whether it is applied no longer depends on how far integration reached. * rugnux: the geometry pre-pass no longer writes `<prefix>_01.mtz`, `_01.cif`, `_01.hkl` and `_01_image.dat`; the refined second pass writes those files under `<prefix>`, and that is the result to use. * rugnux: `_process.h5` describes the pixel format of the images it links to, and is written on a thread of its own. * rugnux: the detector geometry is also logged in XDS's convention (`ORGX`/`ORGY`, detector axis vectors, rotation axis), so it can be compared with an XDS refinement. * rugnux: an image integrated in pyFAI through the `.poni` file written by `--mode calibration` comes out with the correct azimuth, and the file declares pyFAI's `orientation`, which needs pyFAI 2024.01 or newer. Radial integration is unchanged. * rugnux: a rotation run is substantially faster throughout - beam-stop detection, first-pass indexing, geometry refinement, integration, scaling and merging - and observations outside the scaling resolution range are dropped as they are ingested. The refined geometry, the space group chosen and the merged statistics are unchanged. * Faster spot finding and indexing, on the broker as well as in rugnux; the spots found and the lattices indexed are unchanged. * A run reserves substantially less GPU memory: nothing is allocated for buffers that are never read, and a worker builds only the engines it uses. * rugnux: with `-N` left at its default the per-image loop of `--mode mx` uses at most 16 workers per GPU, rather than one per hardware thread; an explicit `-N` is obeyed as given. * CUDA 12 builds now contain device code for Volta, so the RHEL 8 packages and the portable Linux `.tgz` run on a V100; the CUDA 13 artefacts (RHEL 9, Ubuntu, Windows) remain Turing and newer. * The build resolves a single Eigen for the whole project, and refuses to configure if Ceres picks up a different one; a build that mixed two Eigen versions was undefined behaviour and crashed at -O2. * Documentation: a security page, and the supported GPU generations and minimum NVIDIA driver version of every released artefact. **Breaking change to OpenAPI** - regenerate the client (`jfjoch-client` 1.0.0-rc.162, `frontend/src/client`): * `dataset_settings.images_per_file` is no longer `default: 1000` and no longer accepts `0`; it is optional, and its minimum is 1. A client sending `0` (previously "one file for the whole run") is now rejected - omit the field instead, which for a rotation sweep gives the same single file. * `file_writer_format` now defaults to `NXmxVDS`, matching the server's own default and the layout recommended for DIALS, XDS and CrystFEL. A generated client that fills in schema defaults and does not set the format explicitly will write VDS masters where it previously wrote legacy ones; set `NXmxLegacy` explicitly to keep them. --------- Co-authored-by: jungfrau <jungfrau@mx-aare-test.psi.ch> Reviewed-on: #72 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
240 lines
9.9 KiB
Plaintext
240 lines
9.9 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 "BraggPredictionGPU.h"
|
||
|
||
#ifdef JFJOCH_USE_CUDA
|
||
#include "../indexing/CUDAMemHelpers.h"
|
||
#include <cuda_runtime.h>
|
||
|
||
namespace {
|
||
// Number of bandwidth sigmas included in the (radially thickened) Ewald-shell
|
||
// acceptance window. Mirrors the CPU BraggPrediction path.
|
||
constexpr float kBandwidthCutoffSigmas = 3.0f;
|
||
|
||
__device__ inline bool is_odd(int v) { return (v & 1) != 0; }
|
||
|
||
__device__ inline float angle_from_ewald_sphere_deg(const Coord &S0, float recip_x, float recip_y, float recip_z, float recip_sq) {
|
||
const float epsilon = 1e-5f;
|
||
const float rad_to_deg = 180.0f / static_cast<float>(PI);
|
||
|
||
const float s0_sq = S0.x * S0.x + S0.y * S0.y + S0.z * S0.z;
|
||
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;
|
||
|
||
if (fabsf(val) < epsilon || s0_sq < epsilon) return NAN;
|
||
|
||
const float a_num = (s0_sq - 0.25f * recip_sq) * recip_sq;
|
||
if (a_num < 0.0f) return NAN;
|
||
|
||
const float A = sqrtf(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 = sqrtf(p_star_sq * recip_sq);
|
||
if (denom < epsilon) return NAN;
|
||
|
||
float c = (p_star_x * recip_x + p_star_y * recip_y + p_star_z * recip_z) / denom;
|
||
c = fmaxf(-1.0f, fminf(1.0f, c));
|
||
|
||
return acosf(c) * rad_to_deg;
|
||
}
|
||
|
||
__device__ inline bool compute_reflection(const KernelConsts &C, int h, int k, int l, Reflection &out) {
|
||
if (h == 0 && k == 0 && l == 0)
|
||
return false;
|
||
|
||
// Systematic absences (centering only)
|
||
// P, I, A, B, C, F supported
|
||
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': {
|
||
// Rhombohedral in hexagonal setting (hR, a_h=b_h, gamma=120°):
|
||
// Condition: -h + k + l = 3n
|
||
int mod = (-h + k + l) % 3;
|
||
if (mod < 0) mod += 3;
|
||
if (mod != 0) return false;
|
||
break;
|
||
}
|
||
default:
|
||
break;
|
||
}
|
||
|
||
float Ah_x = C.Astar.x * h;
|
||
float Ah_y = C.Astar.y * h;
|
||
float Ah_z = C.Astar.z * h;
|
||
float AhBk_x = Ah_x + C.Bstar.x * k;
|
||
float AhBk_y = Ah_y + C.Bstar.y * k;
|
||
float AhBk_z = Ah_z + C.Bstar.z * k;
|
||
float recip_x = AhBk_x + C.Cstar.x * l;
|
||
float recip_y = AhBk_y + C.Cstar.y * l;
|
||
float recip_z = AhBk_z + C.Cstar.z * l;
|
||
float recip_sq = recip_x * recip_x + recip_y * recip_y + recip_z * recip_z;
|
||
if (recip_sq > C.one_over_dmax_sq) return false;
|
||
float Sx = recip_x + C.S0.x;
|
||
float Sy = recip_y + C.S0.y;
|
||
float Sz = recip_z + C.S0.z;
|
||
float S_len = sqrtf(Sx * Sx + Sy * Sy + Sz * Sz);
|
||
float dist_ewald = fabsf(S_len - C.one_over_wavelength);
|
||
// Energy bandwidth thickens the Ewald shell radially: σ_bw = |recip_z|·(Δλ/λ)
|
||
// (= bλ/2d²). Broaden the acceptance window in quadrature (see CPU path).
|
||
float radial_cutoff = C.ewald_cutoff;
|
||
if (C.bandwidth_sigma > 0.0f) {
|
||
const float bw_tol = kBandwidthCutoffSigmas * C.bandwidth_sigma * fabsf(recip_z);
|
||
radial_cutoff = sqrtf(radial_cutoff * radial_cutoff + bw_tol * bw_tol);
|
||
}
|
||
if (dist_ewald > radial_cutoff) return false;
|
||
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) return false;
|
||
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) return false;
|
||
out.h = h;
|
||
out.k = k;
|
||
out.l = l;
|
||
out.delta_phi_deg = angle_from_ewald_sphere_deg(C.S0, recip_x, recip_y, recip_z, recip_sq);
|
||
out.predicted_x = x;
|
||
out.predicted_y = y;
|
||
out.observed_x = NAN;
|
||
out.observed_y = NAN;
|
||
out.d = 1.0f / sqrtf(recip_sq);
|
||
out.dist_ewald = dist_ewald;
|
||
out.rlp = 1.0f;
|
||
out.partiality = 1.0f;
|
||
out.zeta = 1.0f;
|
||
out.image_scale_corr = 1.0f;
|
||
return true;
|
||
}
|
||
|
||
__global__ void bragg_kernel_3d(const KernelConsts *__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{};
|
||
if (!compute_reflection(*kc, h, k, l, r)) return;
|
||
// See the rotation kernel: clamping the counter hides an overflow from the host.
|
||
const int pos = atomicAdd(counter, 1);
|
||
if (pos < max_reflections) out[pos] = r;
|
||
}
|
||
|
||
inline KernelConsts BuildKernelConsts(const DiffractionExperiment &experiment,
|
||
const CrystalLattice &lattice,
|
||
float high_res_A,
|
||
float ewald_dist_cutoff,
|
||
char centering,
|
||
float bandwidth_sigma) {
|
||
KernelConsts 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 / high_res_A;
|
||
kc.one_over_dmax_sq = one_over_dmax * one_over_dmax;
|
||
kc.one_over_wavelength = 1.0f / geom.GetWavelength_A();
|
||
kc.ewald_cutoff = ewald_dist_cutoff;
|
||
kc.bandwidth_sigma = bandwidth_sigma;
|
||
kc.Astar = lattice.Astar();
|
||
kc.Bstar = lattice.Bstar();
|
||
kc.Cstar = lattice.Cstar();
|
||
kc.S0 = geom.GetScatteringVector();
|
||
kc.centering = centering;
|
||
auto rotT = geom.GetPoniRotMatrix().transpose().arr();
|
||
for (int i = 0; i < 9; ++i) kc.rot[i] = rotT[i];
|
||
return kc;
|
||
}
|
||
} // namespace
|
||
|
||
void BraggPredictionGPU::GrowCapacity(int count) {
|
||
reg_out = CudaRegisteredVector<Reflection>();
|
||
BraggPrediction::GrowCapacity(count);
|
||
reg_out = CudaRegisteredVector<Reflection>(reflections);
|
||
d_out = CudaDevicePtr<Reflection>(count);
|
||
}
|
||
|
||
BraggPredictionGPU::BraggPredictionGPU(int max_reflections)
|
||
: BraggPrediction(max_reflections),
|
||
reg_out(reflections), d_out(max_reflections),
|
||
dK(1), d_count(1), h_count(1) {
|
||
}
|
||
|
||
int BraggPredictionGPU::Calc(const DiffractionExperiment &experiment,
|
||
const CrystalLattice &lattice,
|
||
const BraggPredictionSettings &settings) {
|
||
// Build constants on host
|
||
KernelConsts hK = BuildKernelConsts(experiment, lattice, settings.high_res_A, settings.ewald_dist_cutoff,
|
||
settings.centering, settings.bandwidth_sigma);
|
||
cudaMemcpyAsync(dK, &hK, sizeof(KernelConsts), cudaMemcpyHostToDevice, stream);
|
||
cudaMemsetAsync(d_count, 0, sizeof(int), stream);
|
||
|
||
// Configure and launch on the 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_kernel_3d<<<grid, block, 0, stream>>>(dK, settings.max_h, settings.max_k, settings.max_l, max_reflections, d_out, d_count);
|
||
|
||
// Async D2H count and synchronize
|
||
cudaMemcpyAsync(h_count, d_count, sizeof(int), cudaMemcpyDeviceToHost, stream);
|
||
cudaStreamSynchronize(stream);
|
||
|
||
int count = *h_count.get();
|
||
if (count > max_reflections) {
|
||
GrowCapacity(count); // see the rotation predictor
|
||
cudaMemsetAsync(d_count, 0, sizeof(int), stream);
|
||
bragg_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 {};
|
||
|
||
cudaMemcpyAsync(reflections.data(), d_out, sizeof(Reflection) * count, cudaMemcpyDeviceToHost, stream);
|
||
cudaStreamSynchronize(stream);
|
||
|
||
OrderOutput(count);
|
||
return TruncateToOutput(count);
|
||
}
|
||
|
||
#endif
|