Files
Jungfraujoch/image_analysis/geom_refinement/RingsFromProfile.h
T
leonarski_fandClaude Opus 5 e2de790867 Powder calibration: cover the tilt round trip, and correct how a tilt shows itself
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>
2026-08-06 21:32:30 +02:00

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);