Files
Jungfraujoch/image_analysis/geom_refinement/RingsFromProfile.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

155 lines
7.7 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include <algorithm>
#include <cmath>
#include "RingsFromProfile.h"
#include "AssignSpotsToRings.h" // RingMatchWindow
#include "PowderAutoSeed.h" // ProfileRingTrack
#include "../../common/JFJochMath.h"
float SectorPeakQ(const std::vector<float> &profile, int32_t q_bins, int phi_bin,
int lo_bin, int hi_bin, float low_q, float q_spacing, float min_peak_over_noise) {
const size_t row = static_cast<size_t>(phi_bin) * static_cast<size_t>(q_bins);
const auto value = [&](int i) { return profile[row + static_cast<size_t>(i)]; };
const auto q_of = [&](int i) { return low_q + (static_cast<float>(i) + 0.5f) * q_spacing; };
// A bin no pixel fell in is NaN, not zero (AzimuthalIntegrationProfile::GetResult), and the four
// background bins are where a module gap or the beam stop shows up first. Say so rather than
// relying on NaN comparisons to fail the peak test further down: a sector whose background cannot
// be measured has no measurable peak either.
for (int i : {lo_bin, lo_bin + 1, hi_bin - 1, hi_bin}) {
if (!std::isfinite(value(i)))
return NAN;
}
const float bkg_lo = 0.5f * (value(lo_bin) + value(lo_bin + 1));
const float bkg_hi = 0.5f * (value(hi_bin) + value(hi_bin - 1));
const auto bkg_at = [&](int i) {
const float t = static_cast<float>(i - lo_bin) / static_cast<float>(hi_bin - lo_bin);
return bkg_lo + t * (bkg_hi - bkg_lo);
};
int peak = -1;
float peak_height = 0.0f;
for (int i = lo_bin + 2; i <= hi_bin - 2; ++i) {
const float h = value(i) - bkg_at(i);
if (h > peak_height) { peak_height = h; peak = i; }
}
if (peak < 0)
return NAN;
// Scatter of the background shoulders, as the noise this peak has to stand clear of. A sector with
// no ring in it has a "peak" that is just the largest background fluctuation, and this is what
// rejects it - the alternative, an absolute intensity cut, would need a value per detector and beam.
float s = 0.0f;
int n = 0;
for (int i : {lo_bin, lo_bin + 1, hi_bin - 1, hi_bin}) {
const float r = value(i) - bkg_at(i);
s += r * r;
++n;
}
const float noise = std::sqrt(s / static_cast<float>(n));
if (!(peak_height > min_peak_over_noise * noise))
return NAN;
const float half = 0.5f * peak_height;
double sum_wq = 0.0, sum_w = 0.0;
for (int i = peak; i >= lo_bin && value(i) - bkg_at(i) >= half; --i) {
const double w = value(i) - bkg_at(i);
sum_wq += w * q_of(i);
sum_w += w;
}
for (int i = peak + 1; i <= hi_bin && value(i) - bkg_at(i) >= half; ++i) {
const double w = value(i) - bkg_at(i);
sum_wq += w * q_of(i);
sum_w += w;
}
if (!(sum_w > 0.0))
return NAN;
return static_cast<float>(sum_wq / sum_w);
}
std::vector<RingOptimizerInput> RingsFromAzimuthalProfile(const std::vector<float> &profile,
const AzimuthalIntegrationMapping &mapping,
const DiffractionGeometry &geom,
const std::vector<float> &calibrant_ring_q,
float q_window_recipA,
float min_peak_over_noise,
const DiffractionGeometry *seeded) {
std::vector<RingOptimizerInput> out;
const int32_t q_bins = mapping.GetQBinCount();
const int32_t azim_bins = mapping.GetAzimuthalBinCount();
// One azimuthal bin is a plain radial profile: the ring is averaged over every direction at once, so
// nothing remains to say where its centre is. This needs the run to have been integrated with
// azimuthal bins (jfjoch_broker azim_int_settings.azimuthal_bins, rugnux --azim-phi-bins).
if (azim_bins < 4 || q_bins < 8
|| profile.size() != static_cast<size_t>(q_bins) * static_cast<size_t>(azim_bins))
return out;
const auto &settings = mapping.Settings();
const float low_q = settings.GetLowQ_recipA();
const float q_spacing = settings.GetQSpacing_recipA();
const float high_q = low_q + static_cast<float>(q_bins) * q_spacing;
// Where to LOOK, ring by ring and sector by sector. Without a seed a ring is looked for at its own
// q in every sector, which is the right answer only when the geometry that binned the profile was
// already close; with one, each ring is tracked through the profile it really made.
const size_t rings = calibrant_ring_q.size();
std::vector<std::vector<float>> track(rings);
for (size_t i = 0; i < rings; ++i) {
if (seeded)
track[i] = ProfileRingTrack(calibrant_ring_q[i], *seeded, geom, azim_bins);
else
track[i].assign(azim_bins, calibrant_ring_q[i]);
}
for (size_t i = 0; i < rings; ++i) {
for (int phi_bin = 0; phi_bin < azim_bins; ++phi_bin) {
const float q_ring = track[i][phi_bin];
if (!std::isfinite(q_ring))
continue;
// Never let the window reach into the neighbouring ring. SectorPeakQ takes the background
// under the peak from the two bins at each end of the window, so a window wider than half
// the gap to the next ring measures that ring's flank as this one's background. Hexagonal
// ice has three rings within 0.06 1/A of one another, which a fixed window merges into one
// peak. Measured against the neighbours IN THIS SECTOR, since that is where they are here.
float window = q_window_recipA;
if (i > 0 && std::isfinite(track[i - 1][phi_bin]))
window = std::min(window, 0.5f * std::abs(q_ring - track[i - 1][phi_bin]));
if (i + 1 < rings && std::isfinite(track[i + 1][phi_bin]))
window = std::min(window, 0.5f * std::abs(track[i + 1][phi_bin] - q_ring));
if (!(q_ring - window > low_q) || !(q_ring + window < high_q))
continue;
const int window_bins = static_cast<int>(std::lround(window / q_spacing));
const int centre_bin = static_cast<int>((q_ring - low_q) / q_spacing);
const int lo_bin = std::max(0, centre_bin - window_bins);
const int hi_bin = std::min(q_bins - 1, centre_bin + window_bins);
// Two background bins at each end and a peak between them is the least this can work with;
// a ring whose window is narrower than that is not resolved at this q spacing.
if (hi_bin - lo_bin < 6)
continue;
const float q_obs = SectorPeakQ(profile, q_bins, phi_bin, lo_bin, hi_bin,
low_q, q_spacing, min_peak_over_noise);
if (!std::isfinite(q_obs))
continue;
// The sector's CENTRE, not its lower edge: GetBin() floors phi into the sector, so a bin
// stands for [j, j+1) and taking its edge would rotate every ring point by half a sector -
// which is exactly the cos(phi) signal the beam centre is read from.
const float phi_rad = static_cast<float>((static_cast<double>(phi_bin) + 0.5)
* 2.0 * PI / static_cast<double>(azim_bins));
const auto [x, y] = geom.ResPhiToPxl(static_cast<float>(2.0 * PI) / q_obs, phi_rad);
if (!std::isfinite(x) || !std::isfinite(y))
continue;
out.push_back({x, y, calibrant_ring_q[i]});
}
}
return out;
}