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
* `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>
142 lines
9.4 KiB
C++
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);
|