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

67 lines
4.8 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <vector>
#include "../../common/AzimuthalIntegrationMapping.h"
#include "../../common/DiffractionGeometry.h"
#include "RingOptimizer.h"
// Turn an accumulated (q x azimuth) powder profile into ring points for RingOptimizer.
//
// The calibration this feeds already exists (AssignSpotsToRings + RingOptimizer); what it has always
// been given is a SPOT LIST from a single image. A powder ring is not a set of spots - it is a smooth
// arc - so a spot finder samples it wherever its threshold happens to bite, and one image carries only
// as much of the ring as that image's counting statistics allow. An azimuthally-binned profile summed
// over a run measures the same ring directly, at every azimuth, with the whole run's counts behind it.
//
// Where the ring falls is what carries the geometry. A powder ring is a conic centred on the beam, so
// a wrong beam centre makes its apparent radius oscillate once per turn - a cos(phi) term, the SAME
// amplitude on every ring. A detector tilt beta produces a cos(phi) term as well, not the cos(2 phi)
// one might expect: to first order r(phi) = R + (R^2/F)(beta_x cos phi + beta_y sin phi), so it grows
// as the ring's radius SQUARED. Measured on a powder standard, the genuine cos(2 phi) term is of order
// R^3 beta^2 / F^2, i.e. hundredths of a pixel and below the noise. So the two are told apart by how
// the cos(phi) amplitude scales with radius, which needs at least two rings - on a single ring they are
// exactly degenerate. Neither depends on the calibrant's d-spacings, which is why the beam centre is
// the one thing a powder pattern determines without assuming anything about the standard; the distance,
// by contrast, is only as good as the lattice constant it is measured against, and its lever collapses
// as the detector moves back and the rings crowd into small 2theta.
//
// profile is the mean intensity per bin (AzimuthalIntegrationProfile::GetResult()): q_bins x azimuthal
// bins, indexed bin = q_bin + phi_bin * q_bins. geom supplies the CURRENT geometry, used only to turn a
// measured (q, phi) back into the pixel it came from - RingOptimizer then refines that geometry so the
// q it predicts at that pixel matches the calibrant's. Rings outside the profile's q range, and sectors
// where no peak stands clear of the local background, are skipped rather than guessed at.
//
// Peak position of one ring in one azimuthal sector of the profile, in q, or NaN where there is no peak
// worth using. Shared because the beam-centre seed measures the same thing about a ring it has already
// found, rather than about one a standard predicts.
//
// The window is narrow and centred on where the ring is expected, so the background under it is close to
// a straight line: take it from the two bins at each end and interpolate. The position is the
// intensity-weighted centroid of everything above half the peak height, which needs no line-shape
// assumption - a powder ring is the instrumental profile convolved with whatever strain and size
// broadening the standard has, not a Gaussian.
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);
// calibrant_ring_q is the calibrant's rings as q = 2*pi/d (CalibrantRings). A ring list rather than a
// UnitCell so that ice, whose rings are measured rather than enumerated from a cell, can be used too.
//
// seeded, where it is given, is where the geometry is believed to REALLY be - which is not what the
// profile was binned with, and which moves where each ring has to be looked for. That search runs per
// SECTOR (ProfileRingTrack), because a beam centre wrong by (dx, dy) puts a ring at a different q in
// every sector, and one window centred on one q finds it only where that oscillation happens to be
// small. The points still come back labelled with calibrant_ring_q, because that is the value the fit
// has to drive the geometry to; the track only decides where to look. Null means the profile's own
// geometry is the best guess going in.
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 = 0.06f,
float min_peak_over_noise = 3.0f,
const DiffractionGeometry *seeded = nullptr);