Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 23m2s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 30m11s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 31m15s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 31m37s
Build Packages / build:rpm (rocky8) (push) Successful in 31m43s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 32m28s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 34m23s
Build Packages / Unit tests (push) Failing after 40m38s
Build Packages / Generate python client (push) Successful in 46s
Build Packages / Build documentation (push) Successful in 1m46s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (rocky9) (push) Successful in 29m1s
Build Packages / XDS test (durin plugin) (push) Successful in 20m29s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 20m17s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 22m18s
Build Packages / XDS test (neggia plugin) (push) Successful in 19m12s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 24m23s
Build Packages / DIALS test (push) Successful in 28m49s
Full-jet R-free showed the factored model regressed vs the traditional pipeline (R-free 0.34 vs 0.26, CCref 58 vs 76), traced to Term 2 using the raw measured profile width: it is too tight (~0.002 A^-1), so the template sits off the ~0.4 px centroid-floor scatter and underestimates the intensity (box-sum is immune). Fix, XDS-style (integrate over ~6 sigma): multiply the measured R1 by r1_multiplier (default 6) before use. On the jet at 1.3 A this recovers CCref 55.2 -> 59.3 (~ traditional's 60.5); crystal 2 -0.7. Tunable via env PR_RMULT for R-free. Also reinstates the pre-factored per-image orientation refinement (per-pixel ShoeboxResidual against the shoebox, regularised to the spot-centroid orientation, re-predict), behind env PR_ORIENT (off by default), to A/B its effect on serial-data R-free. On the jet CCref it is a no-op (XtalOptimizer orientation already at the optimum), but CCref is a weak R-free proxy here, so it is left for R-free validation. NOTE: PR_RMULT / PR_ORIENT are temporary diagnostic env knobs; the final multiplier value and the orientation-refine decision are to be fixed against R-free, then baked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
146 lines
8.0 KiB
C++
146 lines
8.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 "../bragg_prediction/BraggPrediction.h"
|
|
#include "../common/DiffractionExperiment.h"
|
|
#include "../scale_merge/HKLKey.h"
|
|
|
|
// =============================================================================
|
|
// PixelRefine — reference-driven profile-fit integration + scaling for stills
|
|
// =============================================================================
|
|
//
|
|
// PixelRefine is the still-image integrator: given a reference set of merged
|
|
// intensities I_ref (the current best hypothesis for each reflection's full
|
|
// intensity), it integrates one image and returns already-scaled intensities. It
|
|
// is an *intensity-wise* operation - the detector geometry is taken as fixed (it
|
|
// was refined upstream by XtalOptimizer in IndexAndRefine::RefineGeometryIfNeeded);
|
|
// PixelRefine does not touch orientation, cell or detector parameters.
|
|
//
|
|
// The objective is the factored per-reflection likelihood of FACTORED_MODEL.md,
|
|
// Terms 1 + 2:
|
|
//
|
|
// Term 2 (shape) — for each resolution shell, the tangential profile width R1 is
|
|
// *measured* from the intensity-weighted second moment of the strong spots:
|
|
// R1 = sqrt(2*<eps_t^2>). A second moment is normalised by the total intensity,
|
|
// so it is decoupled from the per-image scale - which is why measuring R1 is
|
|
// stable where *fitting* it (degenerate with G) is not.
|
|
//
|
|
// Term 1 (intensity / scaling) — one residual per reflection: the profile-fit
|
|
// amplitude J (using the Term-2 R1) should equal the scaled reference
|
|
// J_model = G * exp(-B/4d^2) * partiality * pol * I_ref,
|
|
// weighted by the model-expected (Fisher) sigma_J. Only the per-image scale G
|
|
// and Debye-Waller B are optimised. Integration and scaling become one objective;
|
|
// the many empty shoebox pixels enter only through J (with ~zero profile weight)
|
|
// instead of dominating a per-pixel loss.
|
|
//
|
|
// I_ref is NOT refined here - it is a fixed hypothesis for the pass. The intended
|
|
// outer loop is EM-like: run PixelRefine on every image against the current I_ref,
|
|
// re-merge to a new I_ref, repeat.
|
|
//
|
|
// Forward model per pixel (raw detector counts, no per-pixel solid-angle/Lorentz
|
|
// weighting - same units as the classical integrator):
|
|
// signal = G * I_ref * B_term * P_radial * P_tangential * pol , + I_bkg
|
|
// B_term = exp(-B |q|^2 / 4) (Debye-Waller)
|
|
// P_radial = exp(-eps_r^2 / R0_eff^2) (still partiality, <= 1)
|
|
// P_tangential = exp(-eps_t^2 / R1^2) / (pi R1^2) (area-normalized profile)
|
|
// where eps_r / eps_t are the radial / tangential deviations of the pixel from the
|
|
// predicted node, and pol is the per-reflection polarization correction.
|
|
//
|
|
// X-ray bandwidth (optional): a finite bandwidth thickens the Ewald shell radially,
|
|
// adding a fixed, resolution-dependent term to R0 that grows like 1/d^2 (the
|
|
// pink-beam/DMM signature): R0_eff^2 = R0^2 + (b*lambda)^2/(2 d^4). b = 0 (the
|
|
// default) is a monochromatic no-op; set it for DMM-type data, leave it for Si.
|
|
// =============================================================================
|
|
|
|
struct PixelRefineData {
|
|
// --- model state (input as initial guess, output as refined result) ---
|
|
DiffractionGeometry geom; // fixed (refined upstream by XtalOptimizer)
|
|
CrystalLattice latt; // fixed
|
|
gemmi::CrystalSystem crystal_system = gemmi::CrystalSystem::Triclinic;
|
|
char centering = 'P';
|
|
|
|
double B_factor = 0.0; // Debye-Waller B (A^2), refined
|
|
double scale_factor = 1.0; // overall per-image scale G, refined
|
|
double R[2] = {0.005, 0.005}; // R[0] = radial (partiality) width; R[1] = fallback
|
|
// tangential profile width before Term 2 measures it (A^-1)
|
|
bool refine_B = true; // refine the per-image B-factor along with G
|
|
|
|
// Term 2 measures the physical tangential width R1, but the *integration* profile must
|
|
// be generous (XDS-style: integrate over ~6-10 sigma), or a tight template centred on
|
|
// the prediction sits off the ~0.4 px centroid-floor scatter and underestimates the
|
|
// intensity. The measured R1 is multiplied by this before use. 1.0 = the raw physical
|
|
// width (too tight - regressed R-free on the serial jet); ~6 recovers it (closes the
|
|
// CCref gap to box-sum). Tune the final value against R-free.
|
|
double r1_multiplier = 6.0;
|
|
|
|
// Optional per-image orientation refinement (env-gated, off by default): the
|
|
// pre-factored per-pixel step that refines the crystal orientation against the
|
|
// shoebox pixels, regularised to the spot-centroid orientation, before integration.
|
|
// Reinstated to A/B its effect on serial-data R-free vs the fixed-geometry default.
|
|
bool refine_orientation = false;
|
|
double orient_reg_sigma_deg = 1.0; // orientation-prior strength (deg)
|
|
double fit_signal_sigma_pix = 1.5; // signal-weighting sigma for the orientation fit (px)
|
|
|
|
// Relative X-ray bandwidth (sigma of dlambda/lambda), e.g. ~0.004 for a 1% FWHM
|
|
// DMM, ~1e-4 for Si(111). Adds a resolution-dependent radial broadening to R[0].
|
|
// 0 = monochromatic (the term switches off entirely).
|
|
double bandwidth = 0.0;
|
|
|
|
// Per-image scale G is regularized towards 1 with weight sqrt(n_refl/scale_reg_sigma)
|
|
// (mirrors ScaleOnTheFly). Without this the unconstrained G wanders on weakly
|
|
// measured images and 1/G scrambles the cross-image merge. <= 0 disables.
|
|
double scale_reg_sigma = 2.0;
|
|
|
|
// Radial Ewald-sphere acceptance band for prediction (A^-1): a reflection is given
|
|
// a shoebox when ||S|-1/lambda| <= this. Widened from the on-sphere default towards
|
|
// the integrator's profile radius so slightly-misaligned high-resolution reflections
|
|
// are still integrated (multiplicity), while the partiality downweights their tails.
|
|
double ewald_dist_cutoff = 0.0020;
|
|
|
|
double max_time_s = 5.0;
|
|
int shoebox_radius = 3; // half-size of the per-reflection signal box
|
|
// Half-size of the local-background sampling box. Background is the MEAN of the ring
|
|
// shoebox_radius < |dx|,|dy| <= bkg_outer_radius (excluding spot cores), like
|
|
// BraggIntegrate2D. Must be > shoebox_radius.
|
|
int bkg_outer_radius = 6;
|
|
|
|
// --- output ---
|
|
std::vector<Reflection> reflections; // profile-fitted, scaled integration result
|
|
bool solved = false;
|
|
double final_cost = NAN;
|
|
size_t residual_count = 0;
|
|
double cc = NAN; // per-image CC of scaled intensities vs reference
|
|
int64_t cc_n = 0; // number of reflections in the CC
|
|
};
|
|
|
|
class PixelRefine {
|
|
const size_t xpixel, ypixel;
|
|
const DiffractionExperiment &experiment;
|
|
|
|
const HKLKeyGenerator hkl_key_generator;
|
|
std::map<HKLKey, double> reference_data;
|
|
|
|
// Fills the fixed geometry + symmetry-aware lattice parametrization (beam,
|
|
// distance, detector tilt, and the Rodrigues orientation / cell-length / angle
|
|
// vectors) from the current model state, for the per-pixel geometry evaluation.
|
|
void BuildParameterBlocks(const PixelRefineData &data,
|
|
double beam[2], double &dist_mm,
|
|
double detector_rot[2],
|
|
double latt_vec0[3], double latt_vec1[3], double latt_vec2[3]) const;
|
|
public:
|
|
PixelRefine(const DiffractionExperiment &experiment,
|
|
const std::vector<MergedReflection> &reference);
|
|
|
|
// The BraggPrediction is supplied per call (it is mutated): this keeps a single
|
|
// PixelRefine instance usable from several threads, each passing its own prediction
|
|
// buffer. Only `data` is written; PixelRefine state is const. The image is in raw
|
|
// detector counts (masked/saturated pixels carry the type sentinel); background is
|
|
// estimated locally per shoebox from the image itself.
|
|
template<class T>
|
|
void Run(const T *image,
|
|
BraggPrediction &prediction,
|
|
PixelRefineData &data);
|
|
};
|