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

90 lines
4.9 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <vector>
#include <gemmi/symmetry.hpp>
#include "../../common/UnitCell.h"
#include "../../common/SpotToSave.h"
#include "RingOptimizer.h"
struct RingClusters {
std::vector<int> spots;
float R_obs;
int ring_idx = -1;
};
struct FindCircleCenterResult {
int64_t total_votes;
int64_t votes_for_beam_center;
float x;
float y;
};
// Beam centre from the circumcentres of spot triples, voted into a pixel grid. The grid spans the
// spots' own bounding box: a powder ring encloses the beam centre, so that is where the answer has to
// be, and it is the only bound the spots themselves justify. It used to be a fixed 4000x4000 box,
// which both allocated 128 MB whatever the detector and put the centre outside the grid - so every
// vote was discarded - on any detector larger than that in either direction.
FindCircleCenterResult FindCircleCenter(const std::vector<SpotToSave> &v,
int64_t max_spots = 500);
std::vector<std::vector<int>> ClusterSpotsIntoRings(const std::vector<float>& r, float eps = 1.5, int minPts = 5);
std::vector<RingClusters> AnalyzeClusters(const std::vector<float>& r, const std::vector<std::vector<int>> &clusters);
// Which hkl a lattice actually diffracts into. A cell alone does not say - the centring and any glide
// plane do - and a ring that is systematically absent is worse than a missing one, because the fit
// pairs the innermost OBSERVED ring with the innermost listed one. Only the conditions the calibrants
// need are here; a cell of unknown symmetry (the viewer's "current sample") takes All.
enum class ReflectionCondition {
All, // primitive: no absences
FaceCentred, // h, k, l all even or all odd
Diamond // face-centred, and h + k + l = 4n when they are all even
};
std::vector<float> CalculateXtalRings(const UnitCell &cell,
ReflectionCondition condition = ReflectionCondition::All,
int hkl_max = 6);
// The same, for a cell whose symmetry is known: the absences come from the space group itself rather
// than from the three conditions above, so a centring, a glide and a screw are all handled. This is what
// a user-supplied calibrant cell needs - the fit pairs the innermost OBSERVED ring with the innermost
// LISTED one, so a list that opens with a forbidden reflection scales the whole calibration by the ratio
// between them.
std::vector<float> CalculateXtalRings(const UnitCell &cell, const gemmi::SpaceGroup &sg,
int hkl_max = 6);
std::vector<float> CalculateCubicXtalRings( float a, int hkl_max = 4);
float GuessDetectorDistance(const DiffractionGeometry& geom, float ring_radius_pxl, float d_A);
// The calibrant enters the three functions below as its ring q values (2*pi/d, 1/A, ascending - what
// CalculateXtalRings and CalibrantRings return), not as a UnitCell, so a standard whose rings are
// measured rather than enumerated from a cell - hexagonal ice - can be calibrated against as well.
// GuessInitialGeometry needs only the outermost d-spacing, to seed the distance from the innermost ring.
std::vector<RingClusters> GuessInitialGeometry(DiffractionGeometry &geom, const std::vector<SpotToSave> &v, float largest_ring_d_A);
// refine_tilt = false pins rot1/rot2 where the geometry already has them (see RingOptimizer).
void GuessGeometry(DiffractionGeometry &geom, const std::vector<SpotToSave> &v, const std::vector<float> &ring_q,
bool refine_tilt = true);
void OptimizeGeometry(DiffractionGeometry &geom, const std::vector<SpotToSave> &v, const std::vector<float> &ring_q,
bool refine_tilt = true, RingFitUncertainty *unc = nullptr);
// Widest a ring's match window is allowed to be, in q. The clamp below narrows it where the rings
// crowd; this is the value for a well-separated ring.
constexpr float RING_MATCH_Q_RECIPA = 0.1f;
// Half-width of the window ring i may be matched in, clamped so it can never reach the neighbouring
// ring. Silver behenate's orders sit 0.108 1/A apart and hexagonal ice has three rings within 0.06, so
// a fixed window takes in the neighbour's flank - and a search that stops at its first match then puts
// every point on the lower-q ring of the pair.
float RingMatchWindow(const std::vector<float> &ring_q, size_t i, float max_window);
// Each spot paired with the calibrant ring nearest its observed q, as the points RingOptimizer fits.
// Spots further from every ring than RingMatchWindow allows are dropped rather than forced onto the
// closest one.
std::vector<RingOptimizerInput> AssignSpotsToRings(const DiffractionGeometry &geom,
const std::vector<SpotToSave> &v,
const std::vector<float> &ring_q);