A detector tilt does NOT appear as a cos(2 phi) modulation of the ring radius, as
the previous comment claimed. To first order a misalignment beta gives
r(phi) = R + (R^2 / F) (beta_x cos phi + beta_y sin phi)
which is a cos(phi) term - the same harmonic a wrong beam centre produces. What
separates them is the radius dependence: the centre's amplitude is the same on
every ring, the tilt's grows as R^2. So they are told apart across rings, not
within one, and on a single ring they are exactly degenerate. Measured on a powder
standard the true cos(2 phi) term is of order R^3 beta^2 / F^2 - hundredths of a
pixel, at the noise floor - so it carries nothing usable.
Also add the tilted round trip, which was missing. It doubles as a check that
RingOptimizer's open-coded rotation agrees with DiffractionGeometry's: the fitter
applies Rx(-rot2) Ry(+rot1) by hand rather than going through the geometry's
Rz(-rot3) Rx(-rot2) Ry(+rot1), and those had never been held against each other.
They agree - 0.020 / -0.015 rad recovered as 0.0197 / -0.0148. Dropping rot3 is
right rather than an omission, since rings cannot constrain in-plane roll.
The tilted case yields fewer ring points than the centred one, which is expected
and worth knowing: the extractor searches a window centred on where each ring is
EXPECTED, so a large enough geometry error carries part of a ring out of it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
44 lines
3.0 KiB
C++
44 lines
3.0 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 "../../common/UnitCell.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.
|
|
std::vector<RingOptimizerInput> RingsFromAzimuthalProfile(const std::vector<float> &profile,
|
|
const AzimuthalIntegrationMapping &mapping,
|
|
const DiffractionGeometry &geom,
|
|
const UnitCell &calibrant,
|
|
float q_window_recipA = 0.06f,
|
|
float min_peak_over_noise = 3.0f);
|