diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index dfcdbd49..04796300 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -5,6 +5,7 @@ This is an UNSTABLE release. It includes many experimental features, as well as * rugnux: The error-model **a** and **b** are reported in XDS's convention, and `_reflns.jfjoch_diffrn_ISa` now carries the whole-range `1/sqrt(a*b)` that XDS's ISa denotes; the strong-reflection asymptote moves to `_reflns.jfjoch_diffrn_ISa_asymptotic`. **A file written by an earlier version carries the asymptote under the old name.** * Bragg integration: the background ring's outer radius default changes from 10 px to **13 px**, which roughly doubles the pixels behind each background estimate; the signal disk is unchanged. +* Scaling: the partials of one rocking event now share a single exact-Bragg angle, instead of each solving its own from its frame's refined orientation, which had been feeding per-frame refinement noise into partiality in proportion to 1/zeta. * Bragg integration: signal pixels shared with a neighbouring reflection are now dropped from the profile fit (`--overlap off|reject|exclude`, **default `exclude`**), so a crowded reflection no longer reads its neighbour's flux as its own; `reject` instead discards a reflection whose cleanly-observed profile fraction is below `--overlap-minpk` (default 0.75). * rugnux: The background ring can be **elongated radially per reflection** for broadband data (`--integration-stencil `, default 0 = the fixed circular ring), by `k` times the beam's radial streak; the `r1` signal box stays circular. * rugnux: New **beam-stop shadow detection**, **on by default** (`--detect-beam-stop[=N|off]`), finds the beam stop and its holder in a projection of N images (default 60) and adds them to the pixel mask as bit 9, which is cleared at the start of every run. diff --git a/image_analysis/scale_merge/RotationScaleMerge.cpp b/image_analysis/scale_merge/RotationScaleMerge.cpp index ad7516a0..e494c8af 100644 --- a/image_analysis/scale_merge/RotationScaleMerge.cpp +++ b/image_analysis/scale_merge/RotationScaleMerge.cpp @@ -556,6 +556,64 @@ void RotationScaleMerge::SmoothGeometry() { void RotationScaleMerge::SmoothMosaicityAndPartiality() { SmoothGeometry(); + // One rocking event, one exact-Bragg angle. Every partial's delta_phi is solved from its OWN + // frame's lattice - by the predictor, and again by SmoothGeometry above - so the per-frame + // orientation jitter enters each frame of an event independently and the frames stop sitting + // exactly one oscillation apart on the rocking curve. + // Their partialities then no longer tile it, and the error grows with the number of frames the + // event spans - which is 1/zeta - so it appears as a zeta-graded systematic that nothing in the + // integrator can reach. For the frames of ONE event the geometry is exact: each frame has already + // turned one oscillation further, so delta_phi(f) is linear in f with slope MINUS the increment. + // Sign checked against the data rather than derived - the measured mean frame-to-frame slope is + // -0.19996 deg/frame at an increment of 0.20000. So fit the only free number, the offset, + // averaging the jitter over the event, and lay the partials back on that line. Measured rms + // departure from it: 0.25 deg, larger than the oscillation itself, because a small orientation + // wobble is amplified by 1/zeta. + const auto gon_ev = x.GetGoniometer(); + const double increment_deg = gon_ev ? gon_ev->GetIncrement_deg() : 0.0; + if (std::fabs(increment_deg) > 0.0) { + std::atomic n_events{0}, n_partials{0}; + ParallelChunks(static_cast(rawrun_start.size()), nthreads, [&](int lo_r, int hi_r) { + int64_t l_ev = 0, l_pa = 0; + for (int r = lo_r; r < hi_r; ++r) { + const int lo = rawrun_start[r], hi = lo + rawrun_count[r]; + int i = lo; + while (i < hi) { + int j = i + 1; + while (j < hi && partials[perm[j]].image_number + - partials[perm[j - 1]].image_number <= MAX_FRAME_GAP) + ++j; + if (j - i >= 2) { + const float f0 = partials[perm[i]].image_number; + double sum = 0.0; + int n = 0; + for (int m = i; m < j; ++m) { + const auto &o = partials[perm[m]]; + if (!std::isfinite(o.delta_phi)) continue; + sum += o.delta_phi + increment_deg * (o.image_number - f0); + ++n; + } + if (n >= 2) { + const double c = sum / n; + for (int m = i; m < j; ++m) { + auto &o = partials[perm[m]]; + o.delta_phi = static_cast(c - increment_deg + * (o.image_number - f0)); + } + ++l_ev; + l_pa += j - i; + } + } + i = j; + } + } + n_events += l_ev; + n_partials += l_pa; + }); + logger.Info("One exact-Bragg angle per rocking event: {} events, {} partials relaid", + n_events.load(), n_partials.load()); + } + // Per-frame mosaicity to recompute partiality from. A forced (fixed) mosaicity overrides every frame; // otherwise use the per-frame value measured at integration (image-local, deterministic). const auto forced_mosaicity = x.GetScalingSettings().GetForcedMosaicity();