diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ca9fe8f35..859ca9929 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,7 @@ * `rugnux` reports twinning measured before and after the space group was decided, and no longer reports it when the L-test contradicts it. * `rugnux --mode calibration` writes `.json` beside the `.poni`, holding the geometry as a `jfjoch_broker` `dataset_settings` body. * `rugnux --no-refine-tilt` holds the detector tilt at the value in the file instead of zeroing it. +* `rugnux` refines only the detector-tilt component the data determine, holding the one along the rotation axis, so a beam-centre error is no longer reported as a tilt. * `jfjoch_writer` writes `direct_beam_x`/`direct_beam_y` in the HDF5 master - where the undeflected beam lands - beside the `beam_center_x`/`beam_center_y` PONI. * `jfjoch_broker` sends `direct_beam_x`/`direct_beam_y` on the CBOR start message. * `jfjoch_viewer` reads PILATUS miniCBF sweeps natively, and draws grid scan cells in the proportion of the scan steps. diff --git a/image_analysis/geom_refinement/XtalOptimizer.cpp b/image_analysis/geom_refinement/XtalOptimizer.cpp index a04251122..58c357762 100644 --- a/image_analysis/geom_refinement/XtalOptimizer.cpp +++ b/image_analysis/geom_refinement/XtalOptimizer.cpp @@ -365,14 +365,23 @@ bool XtalOptimizerInternal(XtalOptimizerData &data, // as the data's does, so the split it picks between two aliased parameters is the same however // much data the stage has. More frames, a longer sweep or a later stage cannot break it. constexpr double sigma_px = 3.0; - // The tilt's own budget, in the same direct-beam pixels. It is TIGHTER than the beam's on - // purpose: the data determine the sum of the two, so with equal budgets the shift splits - // evenly and half of a beam-centre error still arrives as an angle (measured: the coupling to - // the starting beam centre falls only from 79% to 41% of one-for-one at equal budgets). The - // detector tilt is a property of the mounting, re-measured when the detector is calibrated; - // the beam centre drifts between runs. When both ends of an alias have to be restrained, the - // tighter restraint belongs on the one that moves less. - constexpr double SIGMA_TILT_PX = 1.0; + // The tilt's budgets, in those same direct-beam pixels: one for the spindle-parallel + // combination and one for the perpendicular one. Zero means no restraint at all, so which + // component is held and which is refined is these two numbers and nothing else. + // + // The parallel one is TIGHTER than the beam's on purpose: the data determine the SUM of the + // two, so with equal budgets the shift splits evenly and half of a beam-centre error still + // arrives as an angle (measured: the coupling to the starting beam centre falls only from + // 79% to 41% of one-for-one at equal budgets, and to 8% at this one). The detector tilt is a + // property of the mounting, re-measured when the detector is calibrated; the beam centre + // drifts between runs. When both ends of an alias have to be restrained, the tighter + // restraint belongs on the one that moves less. + // + // The perpendicular one is free. That is the arrangement the data support today: it is the + // component whose conditioning tracks the 2theta the fit reaches, i.e. the one the data speak + // about, while the parallel one's does not move with 2theta at all. + constexpr double SIGMA_TILT_PARALLEL_PX = 1.0; + constexpr double SIGMA_TILT_PERPENDICULAR_PX = 0.0; const double gauge_w = data.geom.GetPixelSize_mm() / (distance_mm * data.geom.GetWavelength_A()) * std::sqrt(effective_spots) / sigma_px; @@ -424,15 +433,24 @@ bool XtalOptimizerInternal(XtalOptimizerData &data, // has no such compensator, and whether the parallel component is measurable there is a // different question with a different answer. This restraint is local to the fit that // co-refines the orientation and does not speak for any other. - if (data.axis) - problem.AddResidualBlock( - new ceres::AutoDiffCostFunction( - new GaugeDirectionPrior(gauge_rot_x, gauge_rot_y, - gauge_rot_x * detector_rot[0] - + gauge_rot_y * detector_rot[1], - gauge_w * (sigma_px / SIGMA_TILT_PX) - * distance_mm / data.geom.GetPixelSize_mm())), - nullptr, detector_rot); + if (data.axis) { + const double lever = distance_mm / data.geom.GetPixelSize_mm(); + // Parallel first, then the perpendicular direction (-gy, gx). Both go through the + // same restraint, so swapping which one is held is a change to the two budgets. + const double dirs[2][2] = {{gauge_rot_x, gauge_rot_y}, {-gauge_rot_y, gauge_rot_x}}; + const double budget[2] = {SIGMA_TILT_PARALLEL_PX, SIGMA_TILT_PERPENDICULAR_PX}; + for (int i = 0; i < 2; ++i) { + if (budget[i] <= 0.0) + continue; + problem.AddResidualBlock( + new ceres::AutoDiffCostFunction( + new GaugeDirectionPrior(dirs[i][0], dirs[i][1], + dirs[i][0] * detector_rot[0] + + dirs[i][1] * detector_rot[1], + gauge_w * (sigma_px / budget[i]) * lever)), + nullptr, detector_rot); + } + } } if (!data.refine_rotation_axis) {