Files
Jungfraujoch/image_analysis/bragg_prediction/BraggPredictionRot.cpp
T
leonarski_f 680c36c20d
Build Packages / Unit tests (push) Successful in 1h22m15s
Build Packages / build:windows:nocuda (push) Successful in 18m0s
Build Packages / build:windows:cuda (push) Successful in 20m30s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m32s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m39s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m55s
Build Packages / build:rugnux:windows (push) Successful in 11m25s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 20m6s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m27s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 20m19s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m34s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 20m25s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m36s
Build Packages / build:rpm (rocky8) (push) Successful in 17m43s
Build Packages / build:rpm (rocky9) (push) Successful in 13m34s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 21m28s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 18m19s
Build Packages / DIALS test (push) Successful in 12m36s
Build Packages / XDS test (durin plugin) (push) Successful in 6m56s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 6m48s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m7s
Build Packages / Generate python client (push) Successful in 11s
Build Packages / Build documentation (push) Successful in 36s
Build Packages / Create release (push) Skipped
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m11s
v1.0.0-rc.166 (#76)
* `rugnux --mode calibration` writes `<prefix>.json` beside the `.poni`, whose `dataset_settings` member is a `jfjoch_broker` `dataset_settings` body as it stands.
* `rugnux` and `jfjoch_viewer` read PILATUS miniCBF sweeps natively, without conversion.
* Masters written by other facilities open, including Eiger 1.x and third-party NXmx variants.
* `rugnux` measures the beam centre on every run, and indexes with it when the file's value indexes nothing.
* A detector swung out on a 2theta arm is placed where the file says it stands, and the calibration can hold the tilt fixed.
* `rugnux` writes the unmerged MTZ by default, and a P1 merge beside it, so a wrong space group can be re-merged without reprocessing.
* Significant improvements to symmetry handling in `rugnux`: the lattice, the point group, the setting and the systematic absences.
* The `rugnux` report gives the resolution the CC1/2 fit reached, beside the range the reflections were written to.
* The `rugnux` report gives the twinning statistics measured before the space group was decided, beside the ones measured after.
* The `rugnux` report gives the strong-direction diffraction limit, and warns when CC1/2 is not monotone with resolution.
* `rugnux` ranks screw axes on the evidence their absences carry, rather than on how many control reflections a candidate happens to have.
* Twinning is no longer reported when the L-test contradicts it.
* The `rugnux` report gives the detector tilt, the measured tilt and the direct beam beside the beam centre, and a post-refined beam centre is judged against the run's own measurement rather than the file's.
* `--no-refine-tilt` holds the detector tilt at the value in the file, instead of zeroing it, when the calibration starts from the spots.
* The `jfjoch_viewer` grid scan view draws the cells in the proportion of the scan steps, so the map has the shape of the scanned area.

Reviewed-on: #76
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-09-02 21:17:31 +02:00

192 lines
9.1 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "../../common/JFJochMath.h"
#include "BraggPredictionRot.h"
#include "../bragg_integration/SystematicAbsence.h"
int BraggPredictionRot::Calc(const DiffractionExperiment &experiment, const CrystalLattice &lattice,
const BraggPredictionSettings &settings) {
const auto geom = experiment.GetDiffractionGeometry();
const auto det_width_pxl = static_cast<float>(experiment.GetXPixelsNum());
const auto det_height_pxl = static_cast<float>(experiment.GetYPixelsNum());
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;
const auto gon_opt = experiment.GetGoniometer();
if (!gon_opt.has_value())
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"BraggPredictionRotationCPU requires a goniometer axis");
const GoniometerAxis& gon = *gon_opt;
const Coord m2 = gon.GetAxis().Normalize();
const Coord m1 = (m2 % S0).Normalize();
const Coord m3 = (m1 % m2).Normalize();
const float m2_S0 = m2 * S0;
const float m3_S0 = m3 * S0;
int i = 0;
const float mos_angle_rad = settings.mosaicity_deg * static_cast<float>(PI) / 180.f;
const float half_wedge_angle_rad = settings.wedge_deg * static_cast<float>(PI) / 180.f / 2.0f ;
// Energy bandwidth widens the rocking curve. Differentiating Bragg's law at fixed d gives
// dtheta = (dlambda/lambda) tan(theta_B), a spread in the same glancing angle the mosaic spread
// smears, so it adds to sigma_M in quadrature. It is NOT divided by zeta here: rotating the
// crystal by dphi changes theta by zeta*dphi, so the 1/zeta that turns an angular width into a
// rotation width is already the one c1 (and the epsilon3 cutoff) applies to sigma_M. The fitted
// sigma_M has this term deconvolved out (CalcMosaicityXDS), so it is not counted twice.
// sin(theta_B) = lambda/(2d) = lambda*|p0|/2. Zero bandwidth leaves every reflection untouched.
const float bandwidth_sigma = settings.bandwidth_sigma;
const float half_wavelength_A = geom.GetWavelength_A() / 2.0f;
for (int h = -settings.max_h; h <= settings.max_h; h++) {
// Precompute A* h contribution
for (int k = -settings.max_k; k <= settings.max_k; k++) {
// Accumulate B* k contribution
for (int l = -settings.max_l; l <= settings.max_l; l++) {
if (systematic_absence(h, k, l, settings.centering))
continue;
if (i >= max_reflections)
continue;
Coord p0 = Astar * h + Bstar * k + Cstar * l;
float p0_sq = p0 * p0;
if (p0_sq <= 0.0f || p0_sq > one_over_dmax_sq)
continue;
const float p0_m1 = p0 * m1;
const float p0_m2 = p0 * m2;
const float p0_m3 = p0 * m3;
const float rho_sq = p0_sq - (p0_m2 * p0_m2);
const float p_m3 = (- p0_sq / 2 - p0_m2 * m2_S0) / m3_S0;
const float p_m2 = p0_m2;
const float p_m1_opt[2] = {
std::sqrt(rho_sq - p_m3 * p_m3),
-std::sqrt(rho_sq - p_m3 * p_m3)
};
// No solution for Laue equations
if ((rho_sq < p_m3 * p_m3) || (p0_sq > 4 * S0 * S0))
continue;
// Effective rocking width for this reflection: mosaicity broadened by the bandwidth
// term. sin(theta_B) <= 1 is guaranteed by the p0_sq test just above.
float mos_eff_rad = mos_angle_rad;
if (bandwidth_sigma > 0.0f) {
const float sin_theta = half_wavelength_A * std::sqrt(p0_sq);
const float dphi_bw = bandwidth_sigma * sin_theta / std::sqrt(1.0f - sin_theta * sin_theta);
mos_eff_rad = std::sqrt(mos_angle_rad * mos_angle_rad + dphi_bw * dphi_bw);
}
for (const auto& p_m1 : p_m1_opt) {
if (i >= max_reflections)
continue;
const float cosphi = (p_m1 * p0_m1 + p_m3 * p0_m3) / rho_sq;
const float sinphi = (p_m1 * p0_m3 - p_m3 * p0_m1) / rho_sq;
Coord p = m1 * p_m1 + m2 * p_m2 + m3 * p_m3; // p0 vector "rotated" to diffracting condition
Coord S = S0 + p;
float phi = -1.0f * std::atan2(sinphi, cosphi);
const Coord e1 = (S % S0).Normalize();
const float zeta_abs = std::fabs(m2 * e1);
if (zeta_abs < settings.min_zeta)
continue;
// Is any of this reflection's rocking curve inside THIS image's oscillation range?
// phi is the offset from the frame's mid-exposure angle to the exact diffracting
// condition, so the curve - a Gaussian of width mos_eff/zeta in phi - has to be
// measured against the frame's EDGE, not its centre. Testing |phi|*zeta alone
// asks whether the frame centre is within the curve, which is a different and
// stricter question: it rejects a reflection whose curve overlaps the exposure
// but whose exact condition falls outside it. Since consecutive frame centres are
// one wedge apart, the nearest centre can be half a wedge away, so a reflection is
// rejected on EVERY frame - lost entirely, not merely clipped - once
// mos_eff < zeta * wedge / (2 * multiplier). That is coarse slicing on a sharp
// crystal at high zeta, where the reflection is fully recorded and cleanest.
float epsilon3 = std::fabs(phi * zeta_abs) - half_wedge_angle_rad * zeta_abs;
if (epsilon3 > settings.mosaicity_multiplier * mos_eff_rad)
continue;
// Reciprocal Lorentz (Kabsch 2010): L^-1 = |m2 . (S x S0)| / (|S| |S0|) =
// |zeta * sin angle(S,S0)|. The original divided by the scalar product
// S.S0 = |S||S0|cos(2theta), adding a spurious 1/cos(2theta) (1.8x at 1 A) that
// corrupts the absolute/Wilson scale (it cancels within a resolution shell, so
// CC1/2 / CCref / R-meas are neutral).
const float lorentz_reciprocal = std::fabs(m2 * (S % S0)) / (S.Length() * S0.Length());
const float c1 = zeta_abs / (std::sqrt(2.0f) * mos_eff_rad);
const float partiality = (std::erf((phi + half_wedge_angle_rad) * c1)
- std::erf((phi - half_wedge_angle_rad) * c1)) / 2.0f;
// 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;
float x = beam_x + F * S_rot_x / S_rot_z;
float y = beam_y + F * S_rot_y / S_rot_z;
if ((x < 0) || (x >= det_width_pxl) || (y < 0) || (y >= det_height_pxl))
continue;
float dist_ewald_sphere = std::fabs(S.Length() - one_over_wavelength);
float d = 1.0f / sqrtf(p0_sq);
reflections[i] = Reflection{
.h = h,
.k = k,
.l = l,
.delta_phi_deg = phi * 180.0f / static_cast<float>(PI),
.predicted_x = x,
.predicted_y = y,
.observed_x = NAN,
.observed_y = NAN,
.d = d,
.dist_ewald = dist_ewald_sphere,
.rlp = lorentz_reciprocal,
.partiality = partiality,
.zeta = zeta_abs,
.image_scale_corr = lorentz_reciprocal / partiality,
};
i++;
}
}
}
}
return TruncateToOutput(i);
}