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>
90 lines
4.9 KiB
C++
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);
|
|
|
|
|