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

142 lines
9.4 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <optional>
#include <string>
#include <vector>
#include "../../common/AzimuthalIntegrationMapping.h"
#include "../../common/DiffractionExperiment.h"
#include "../../common/DiffractionGeometry.h"
#include "../../common/SpotToSave.h"
#include "PowderAutoSeed.h"
#include "RingOptimizer.h" // RingFitUncertainty
// How the powder rings the detector geometry is fitted to are measured (rugnux --calibration).
enum class CalibrationMethod {
Rings, // the run-summed (q x azimuth) azimuthal profile: the ring measured at every azimuth
Spots // the pooled per-image spot lists: the ring sampled wherever the spot finder bit
};
struct CalibrationResult {
DiffractionGeometry geometry; // the fitted geometry
size_t ring_points = 0; // ring measurements the fit used
// Scatter of those measurements about the fitted rings, and the standard error it implies on the
// beam centre. Both in pixels - a calibration that has gone wrong (textured ice, one visible ring)
// says so here, and that is the only warning a user gets.
double rms_radial_pxl = 0.0;
double beam_sigma_pxl = 0.0;
// What the fit itself says about how well each parameter is determined, and how badly the tilt is
// correlated with the beam centre. rms/beam_sigma above describe the SCATTER of the measurements;
// this describes the FIT, and the two part company exactly where it matters - a two-ring tilt can
// leave a small rms while being free to move tens of pixels of beam centre with it.
RingFitUncertainty uncertainty;
// The distance the rings themselves asked for before the fit ran, and what the header said. They
// are reported rather than only used because a large gap between them is the one thing that says
// the header was wrong - which is usually why the calibration was run at all. Zero when the profile
// showed too few rings to measure a scale.
float seed_distance_mm = 0.0f;
float header_distance_mm = 0.0f;
// Whether the tilt survived. It is refined by default, but a fit that cannot separate it from the
// beam centre will still return one - spending tens of pixels of PONI on a tilt the rings do not
// support - so a tilt smaller than a few times its own uncertainty is declined and the fit repeated
// with rot1/rot2 pinned. significance is max(|rot| / sigma(rot)) over the two components, 0 where
// the tilt was never a free parameter.
bool tilt_refined = false;
float tilt_significance = 0.0f;
// How far the beam centre the SPOTS vote for is from the one the rings were fitted to, in pixels,
// and where the spots put it. An independent cross-check that costs nothing, because the spots have
// already been found: the circle through them reads nothing from the header, so it holds in exactly
// the regime the profile does not - a profile binned about a badly wrong centre shows each ring
// smeared across its sectors, and no seeding recovers that. A large disagreement is therefore not a
// close call to arbitrate but a statement that this profile could not have been fitted, whatever
// the residual says. Zero when no spots were available.
std::optional<DiffractionGeometry> spots_geometry;
float spots_disagreement_pxl = 0.0f;
// Whether the geometry above is a measurement of this data at all. A calibration is asked for
// precisely because the input file is not believed, so a fit that quietly hands part of that file
// back is not a weaker answer to the question - it is no answer, and it is indistinguishable from a
// good one in everything a caller reads. Two ways out of the fits below produce such a geometry, and
// both are stated rather than repaired here (see JudgeCalibration in the .cpp for why neither is a
// measurement). reason is empty exactly when converged is true, and is written for a person.
bool converged = true;
std::string reason;
};
// How many of its own sigmas the fitted tilt is away from zero - the number the gate above reads.
// Zero when the tilt was held fixed, or when the covariance could not be computed at all (which is
// itself a statement that the tilt is not separable).
float TiltSignificance(const DiffractionGeometry &geom, const RingFitUncertainty &unc);
// Below this the tilt is not a measurement. A tilt the rings genuinely resolve clears it by orders of
// magnitude - measured over a LaB6 distance series, 50, 33, 15 and 8 sigma at 110 to 300 mm - while the
// two-ring fit at 500 mm, whose tilt flips sign and drags the PONI 28 px for no gain in residual,
// reaches 0.1. Any value between 2 and 5 gives the same verdict on all five, so this is a statement
// about which regime a fit is in and not a tuned number.
//
// It is a REJECTION criterion only. Passing it does not certify a tilt: the tilt estimator is
// systematics-limited rather than variance-limited, and a coherent half-pixel error in the ring
// positions fakes a tilt of the size usually seen while leaving sigma small.
constexpr float TILT_MIN_SIGNIFICANCE = 3.0f;
// Decide whether `result` is a measurement of the data and record the verdict on it: sets converged
// and, where that is false, the reason. header is the geometry the fit started from and refine_tilt
// whether a tilt was asked for at all - both fits below call this on the way out, and it is declared
// here so the rule can be tested without a powder pattern. See the definition for what makes a fit not
// a measurement, which is a question of provenance and not of any threshold.
void JudgeCalibration(CalibrationResult &result, const DiffractionGeometry &header, bool refine_tilt);
// Both fits take the detector tilt as a free parameter unless refine_tilt is false, which holds
// rot1/rot2 at the value `geom` came in with and fits only the beam centre and the distance. The
// tilt is real and a PONI carries it, but a program that has nowhere to put it - XDS - is better
// given a geometry measured with the tilt pinned than one measured tilted and then flattened.
// Fit the geometry to the rings found in a run-summed azimuthal profile (CalibrationMethod::Rings).
//
// spots, where they are given, add one more beam-centre hypothesis: the centre of the circle through
// them, voted for by every triple (FindCircleCenter). It reads nothing from the header at all - only
// where the spots landed - so it is the one estimate that survives a header the profile itself cannot
// correct from. The rings' own estimate stops working once the beam centre is out by more than about
// half a ring spacing, because past that the azimuthally averaged profile shows each ring as the two
// horns of a sinusoid rather than as a ring; the circle through the spots has no such limit. Measured
// on a 110 mm LaB6 exposure it returns the same geometry from a header 400 px and 8x in distance wrong.
CalibrationResult CalibrateFromProfile(const std::vector<float> &profile,
const AzimuthalIntegrationMapping &mapping,
const DiffractionGeometry &geom,
const std::vector<float> &calibrant_ring_q,
bool refine_tilt = true,
const std::vector<SpotToSave> &spots = {});
// Fit the geometry to a pooled spot list (CalibrationMethod::Spots): the beam centre from scratch off
// the Hough circle centre, then the same ring fit.
CalibrationResult CalibrateFromSpots(const std::vector<SpotToSave> &spots,
const DiffractionGeometry &geom,
const std::vector<float> &calibrant_ring_q,
bool refine_tilt = true);
// Write the geometry as a pyFAI PONI file, the interchange format every azimuthal-integration tool
// reads. Throws if the file cannot be written.
void WritePoniFile(const std::string &path, const DiffractionExperiment &experiment,
const DiffractionGeometry &geom);
// The same result as JSON, for whatever reads it next rather than for a person.
//
// A PONI file says where the detector is and nothing else - not how well it is known, not whether the
// tilt in it was measured or merely carried over, and not in the spelling anything in this system
// takes. The object written here has two members. "dataset_settings" holds the geometry under the
// property names broker/jfjoch_api.yaml gives them, and holds nothing else, so it is a valid
// dataset_settings body as it stands and can be POSTed or merged without translating a single field;
// the four the schema requires are always present. "calibration" holds what the run knows about that
// geometry - the residual, the fit's own sigmas, whether the tilt survived its significance test, and
// where the spots independently put the beam - because a calibration that has gone wrong looks exactly
// like one that has not until those are read.
//
// beam_x_pxl/beam_y_pxl is the PONI, as everywhere in this system: on a tilted detector it is NOT where
// the direct beam lands, and that point is reported separately under "calibration". Throws if the file
// cannot be written.
void WriteCalibrationJson(const std::string &path, const DiffractionExperiment &experiment,
const CalibrationResult &result,
const std::string &calibrant, const std::string &method);