Per-ring ice verdict in the results report, log and docs

The rotation merge's decision for each ice-ring band (DecideIceRings) is carried to the results
report as ICE_RING_BAND_nn (ring d, band, action, reflections, bias in standard errors, variance
ratio), and the report, the ice log line and the documentation no longer say that the reflections on
the rings are kept in the merge where the rotation merge decides them ring by ring. The stills merge
is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-11-01 07:32:08 +01:00
1 parent 263c699e19
commit f1e7a67ac2
12 files changed
+71 -24

No files matched your search

+1 -1
View File
@@ -53,7 +53,7 @@ struct Reflection {
float zeta;
float image_scale_corr; // I_true = image_scale_corr * I; = prescaling_corr * qe_corr * flight_corr / (partiality * image_scale)
bool observed = false;
bool on_ice_ring = false; // sits on a hexagonal-ice powder ring: excluded from scaling, kept for merging
bool on_ice_ring = false; // sits on a hexagonal-ice powder ring: excluded from scaling; merged on stills, ring by ring on rotation (DecideIceRings)
// The signal disk lost pixels to the mask or to saturation, so the intensity is the profile's
// estimate over what remained. Such a measurement may be low, and the merge does not let it
// testify against a larger observation of the same reflection (WilsonOutliers.h).
+1 -1
View File
@@ -82,7 +82,7 @@ The median needs three observations, so a reflection measured once or twice —
By default the reported/written high-resolution limit is trimmed where $\mathrm{CC}_{1/2}$ falls off: a logistic is fitted to $\mathrm{CC}_{1/2}(s)$, and the limit is set **one reported-shell width past** the point where the fit crosses 0.30 — deliberately "one shell too far", so weak-but-real data below the crossing are kept rather than discarded. The extension is measured over the range that is actually kept, not the full measured range, so a detector reaching far past where the crystal diffracts cannot inflate it. `--scaling-high-resolution` overrides the limit and `--resolution-cutoff off` disables it.
**Ice sits out of this fit, and only this fit.** A powder ring is reproducible: past the crystal's own limit the ice is still there and still the same in both half-sets, so the two halves agree *about the ring*, and a Pearson $\mathrm{CC}_{1/2}$ cannot tell that agreement from diffraction. Shells have been measured at $\mathrm{CC}_{1/2}=0.765$ where $\langle I/\sigma\rangle$ is $-0.1$ and $R_\mathrm{meas}$ is 470 %, inside the written range because the logistic was fitted through them; against archived references, runs whose frames carry ice were written a mean twelve per cent finer than the reference where runs without ice were written four per cent finer, and every over-claim past twenty per cent has rings. The same ring drags the curve the other way where the data are good — one shell of forty falling to 0.51 because the two strongest ice lines cross it, with the shells either side at 0.99 — which is a real defect of those reflections but narrower than the shell it defames. Both signs are the same cause and both leave the fit: the reflections flagged in §3.3 are still merged, still written and still counted in the shell table (§10.10), and what changes is only that the resolution **decision** now reads the same curve that scaling, the error model and the space-group search already read.
**Ice sits out of this fit, and only this fit.** A powder ring is reproducible: past the crystal's own limit the ice is still there and still the same in both half-sets, so the two halves agree *about the ring*, and a Pearson $\mathrm{CC}_{1/2}$ cannot tell that agreement from diffraction. Shells have been measured at $\mathrm{CC}_{1/2}=0.765$ where $\langle I/\sigma\rangle$ is $-0.1$ and $R_\mathrm{meas}$ is 470 %, inside the written range because the logistic was fitted through them; against archived references, runs whose frames carry ice were written a mean twelve per cent finer than the reference where runs without ice were written four per cent finer, and every over-claim past twenty per cent has rings. The same ring drags the curve the other way where the data are good — one shell of forty falling to 0.51 because the two strongest ice lines cross it, with the shells either side at 0.99 — which is a real defect of those reflections but narrower than the shell it defames. Both signs are the same cause and both leave the fit: the reflections flagged in §3.3 are still merged, written and counted in the shell table wherever the per-ring decision of §10.10 keeps them, and what changes is only that the resolution **decision** now reads the same curve that scaling, the error model and the space-group search already read.
**A cut the shell table refutes is not quoted.** Two things keep the number inside what the bins show. The fitted crossing has to lie **inside the fitted bins**: on a real fall-off it sits between the last bin at the target and the first bin below it, with the extension bins to spare, and a crossing past the last fitted bin is an extrapolation of a fall-off these data never showed — the crossing is then read off the bins themselves, between the two that straddle the target. And no single resolution is quoted at all where the curve is not a fall-off: where $\mathrm{CC}_{1/2}$ never reaches the target in any shell, where the fit reads finer than the finest shell whose $\mathrm{CC}_{1/2}$ still reaches it, or where $\mathrm{CC}_{1/2}$ **climbs back over the target** after falling below it — a noise shell that climbs back is not the edge of the data. The report then says which of the three happened and points at the shell table instead.
+2 -4
View File
@@ -158,7 +158,7 @@ The curve $\bar B(r)$ is the adaptive spot finder's peak-excluded background per
**When it is applied.** `--background-radial` takes `on`, `off` or `auto`; in Rugnux it is **`auto` by default** (the broker keeps it off), and auto is decided **per run, in the pre-scan, on empty apertures** — the same measurement that, on stills, also chooses how far the ring reaches (the $r_3$, up to the one the run starts from, whose ring predicts the empty disk best). On a grid of apertures away from every found spot, the ring's prediction of the empty disk is taken twice: plain, and with the correction applied exactly as the integrator applies it (the clip following the curve, and the curve's mean over the ring pixels kept minus its mean over the disk). With nothing in the aperture each difference is the error the ring makes in a reflection's background, noise plus bias. Rotation measures every stencil the spot width can give it (and the one it starts from, which the geometry pre-pass and the starvation guard return to) and reads the one it gets.
- **Stills** have no other correction, so it is applied to every image of a run where it lowers the apertures' mean square error at all.
- **Rotation** has one: under auto, each image whose peak-excluded ice score (§3.3) reaches `--ice-min-score` is corrected with the curve of its reflections' rings. The frame's curve on every image replaces that only where it takes **at least 10 %** of the apertures' mean square error away. On photon-counting sweeps the apertures gain 0.1–5 % (statistically significant, $z\approx3$–4), and correcting every image there was measured to cost ISa and anomalous signal; where it pays — the rings of home-source CCD frames, a smooth ice ring — they gain 13–31 %. A run below the margin keeps the per-image ice correction, bit for bit.
- **Rotation** has one: under auto, each image whose peak-excluded ice score (§3.3) reaches `--ice-min-score` is corrected, with the frame's curve as on stills. (It used the curve of the reflections' own rings until it was measured to miss the narrow peak of a smooth ice ring: on a crystal with smooth ice the ring cores stayed 4–9 standard errors high and their flanks low, against the off-ring reflections, and the frame's curve brought all three low-resolution rings within 3.) Correcting **every** image instead is chosen only where it takes **at least 10 %** of the apertures' mean square error away. On photon-counting sweeps the apertures gain 0.1–5 % (statistically significant, $z\approx3$–4), and correcting every image there was measured to cost ISa and anomalous signal; where it pays — the rings of home-source CCD frames, a smooth ice ring — they gain 13–31 %. A run below the margin keeps the per-image ice correction, bit for bit.
The model is a function of **radius alone**, so it is applied only where that is true of the background. The per-image ice gate rests on this: smooth powder ice *is* a radial feature and is corrected; ice made of discrete crystallite spots — which the profile channel is blind to and the spot channel catches — leaves no smooth ring to model, and correcting it makes matters worse. Measured against a fixed atomic model, comparing ice bands with resolution-matched decoy bands carrying no ice: on a crystal with pure smooth ice the correction removes **43 % of the bands' excess amplitude**, and the improvement is **7× larger inside the bands than outside**, which is its stated mechanism; on a crystal whose ice is textured the same correction *increased* the excess amplitude by half; on a clean crystal it is inert to four decimal places. A strongly polarised beam or panels of different response make the background non-radial too; the apertures see that as a ring the correction predicts *worse*, and a run where they do keeps it off. Auto engages only where a peak-excluded background exists (adaptive spot finding, §3.2) — a plain azimuthal profile carries the Bragg peaks and cannot support an absolute threshold, so without one auto leaves the correction off.
@@ -481,9 +481,7 @@ A twin law is only an ambiguity for crystals whose own lattice cannot tell the i
### 10.10 Ice rings at the scale and merge stages
Where the gate of §3.3 has found ice, reflections falling within $\pm w$ in $q = 2\pi/d$ (§1.2) of a hexagonal-ice band ($w=0.03$ Å$^{-1}$ offline, about the measured ring half-width) are marked. Marked reflections are **excluded where a model is fitted** — the per-frame scale $G$, the per-image correlation, and the $P1$ merge the space-group search runs on — because ice contamination is a *positive bias*, not extra scatter, and a least-squares scale absorbs it into $G$ and into the error-model $b$, where it damages every other reflection on the same frame. They are also left out of the **resolution-cut fit** (§13.4), which is the one consumer that reads the *merged* reflections rather than the observations: an ice-flagged observation marks its merged reflection, on the rotation merge's own accumulator (host and device alike) as well as on the stills one. They are otherwise **kept in the final merge**, which is also what the established scaling programs do by default, so the affected shells keep their completeness.
Nothing on an ice band is deleted from the merged output. Deleting the bands was implemented, measured against an external arbiter rather than against the merge's own statistics, and removed: on the one rotation-battery crystal where a band was both dead by its own merged $\mathrm{CC}_{1/2}$ and scorable by anomalous peak height, dropping it changed the mean anomalous density at the known sites by $-0.001\pm0.018\,\sigma$ — about 2 % of the site height — while removing 1149 unique reflections whose mean $I/\sigma$ was 3.62 against the dataset's own 3.05, i.e. better-than-average data, and costing 6 to 8 points of completeness in the affected shell.
Where the gate of §3.3 has found ice, reflections falling within $\pm w$ in $q = 2\pi/d$ (§1.2) of a hexagonal-ice band ($w=0.03$ Å$^{-1}$ offline, about the measured ring half-width) are marked. Marked reflections are **excluded where a model is fitted** — the per-frame scale $G$, the per-image correlation, and the $P1$ merge the space-group search runs on — because ice contamination is a *positive bias*, not extra scatter, and a least-squares scale absorbs it into $G$ and into the error-model $b$, where it damages every other reflection on the same frame. They are also left out of the **resolution-cut fit** (§13.4), which is the one consumer that reads the *merged* reflections rather than the observations: an ice-flagged observation marks its merged reflection, on the rotation merge's own accumulator (host and device alike) as well as on the stills one. The stills merge keeps them in the final merge. The **rotation merge decides ring by ring**, because what a ring does to its reflections differs between rings and between runs: it can bias them (up on the ring, where its powder intensity is under the spot; down on the flanks, where it is in the background ring), only add scatter, or leave them alone. Deleting every band unconditionally was implemented once and removed: on a crystal whose band was harmless it removed better-than-average reflections and 6 to 8 points of completeness in the affected shell for no measurable gain. So each ring is split into its **core** ($|q-q_\mathrm{ring}|<0.01$ Å$^{-1}$) and its two **flanks** (out to $w$), and every band is compared with the off-ring merged reflections within $0.02$ Å$^{-2}$ of the ring in $1/d^2$. Both are read against a log-linear **Wilson fit** of the mean intensity over those off-ring reflections (the fit behind the reported Wilson $B$; over all off-ring reflections where the window cannot be fitted), as $t=(I-I_\mathrm{exp})/\sqrt{\sigma^2+I_\mathrm{exp}^2}$, which has unit scatter whether a reflection is signal- or noise-dominated. A band whose mean $t$ departs from its neighbours' by more than 3 standard errors is **rejected** — a bias, which no larger $\sigma$ would remove; one whose variance of $t$ exceeds the neighbours' by more than 3 standard errors of the log ratio (taken from the samples' own kurtosis: Wilson intensities are far from Gaussian) is **down-weighted**, its excess variance added to each reflection's own; the rest is **merged** as measured. A band of fewer than 20 reflections, or a ring with fewer than 50 neighbours, is merged. The decision is applied to the merge's accumulators, so the written reflections, the shell table and the half-set $\mathrm{CC}$ all see the same thing, and each band's verdict is logged and reported (`ICE_RING_BAND_nn`, see the results report). It complements the radial background correction (§9), which removes most of the bias at its source: on 58 rotation crystals with ice and a model, it lowered the model's R-free against merging every band on 26 and raised it on one, by 0.003.
---
+2 -2
View File
@@ -214,7 +214,7 @@ The model is taken as placed in the data's cell, and refined in the data's space
| `--spot-low-resolution <num>` | Low-resolution limit for spot finding, Å (default: 50; lower it, e.g. 24, to exclude the direct-beam halo on weak serial data; 0 removes the limit) |
| `--min-pix-per-spot <num>` | Minimum connected strong pixels per spot. **If omitted, min-pix is chosen per image** (stills indexing): the frame is scored at min-pix 2/3/1 (one lattice search per frame, refined on each list) and the one maximising indexed-spot count × indexed fraction is kept. Give an explicit value to force a fixed min-pix instead. |
| `--max-spots <num>` | Maximum spots kept per image (the strongest ones) and handed to indexing. **If omitted, the budget is measured** on rotation data: the first pass reads how deep into an image's spot list its spots still lie on the lattice it found, and the run keeps that many (never more than 1000). Give a value to pin it. Stills always use the fixed 1000. |
| `--detect-ice-rings[=on\|off]` | Flag ice-ring spots (de-prioritised in indexing) and exclude ice-ring reflections from scaling. Default: the master file's `detect_ice_rings`, or — where the file carries no such key — **on for rotation and off for stills** |
| `--detect-ice-rings[=on\|off]` | Flag ice-ring spots (de-prioritised in indexing) and exclude ice-ring reflections from scaling; on rotation data the merge then decides ring by ring whether to merge, down-weight or reject them. Default: the master file's `detect_ice_rings`, or — where the file carries no such key — **on for rotation and off for stills** |
### Azimuthal integration
@@ -294,7 +294,7 @@ serial stills; `fft` (GPU) / `fftw` (CPU) index **de novo** and suit strong rota
| `--resolution-shells <num>` | Number of resolution shells in the reported statistics table (default: 9). The bins are equal steps in 1/d² between the lowest- and highest-resolution reflection merged, which is XDS's rule, and 9 is XDS's count — so at the same resolution limits the two tables have the same shells and can be read row for row |
| `--report-resolution <dmin>[,<dmax>]` | Also report the merging statistics over this resolution range, Å (either order; `dmax` defaults to the run's own low-resolution limit), as a second table beside the run's own — the `REFRES_*` keys of the [results report](RUGNUX_REPORT.md#the-reference-range-table) — binned from the same merged reflections, so a run can be read against another program's table at that program's range. Report-only: the cut, the scaling, the error model and every decision stay the run's own, and shells finer than the run's own limit are reported as not merged rather than filled from data the run did not keep |
| `--min-partiality <num>` | Minimum partiality to accept a reflection (default: 0.02 for rotation, 0.5 for stills) |
| `--ice-min-score <num>` | Ice-presence gate: the measured per-run ice score (1 = no ice) a dataset must reach before **any** ice handling is applied — the flagging and the exclusion from scaling (default: 1.5; 0 = no gate). The fixed hexagonal bands cover 16–26 % of the unique reflections whether or not the crystal has ice, so handling ice on a clean crystal only costs completeness |
| `--ice-min-score <num>` | Ice-presence gate: the measured per-run ice score (1 = no ice) a dataset must reach before **any** ice handling is applied — the flagging, the exclusion from scaling and the per-ring merge decision (default: 1.5; 0 = no gate). The fixed hexagonal bands cover 16–26 % of the unique reflections whether or not the crystal has ice, so handling ice on a clean crystal only costs completeness |
| `--ice-min-spot-ratio <num>` | The second ice-presence channel: found **spots** on the hexagonal rings over the same q width of ice-free flanks beside them (1 = spots spread evenly). Ice in large crystallites diffracts as discrete spots and leaves the radial profile flat, so `--ice-min-score` alone is blind to it (default: 2.0; 0 disables this channel) |
| `--reject-outliers <num>` | Per-observation outlier rejection, N σ from the per-reflection median (default: 6 for rotation, 10 for stills; 0 = off) |
| `--min-image-cc <num>` | Per-image CC limit, percent (default: no limit) |
+12 -2
View File
@@ -625,13 +625,17 @@ summary's `Powder` line gives both. A warning under the `ICE_RINGS` flag is give
at least 5 % of the spots, and one under `POWDER_RINGS` where the other rings do.
Hexagonal ice is also measured by the merge itself, which leaves reflections on the ice rings out of
scaling (and keeps them in the merge) where it finds ice:
scaling where it finds ice and, on rotation data, decides ring by ring what the merge does with them:
```
ICE_RINGS_DETECTED= TRUE
ICE_RING_SCORE= 2.69
ICE_SPOT_RATIO= 1.08
ICE_REFLECTIONS_ON_RINGS_PCT= 25.7
ICE_RING_BAND_01= 3.895 LOW_FLANK MERGE 42 -1.1 0.81
ICE_RING_BAND_02= 3.895 CORE MERGE 52 +1.0 2.59
ICE_RING_BAND_03= 3.895 HIGH_FLANK MERGE 43 -2.0 0.94
ICE_RING_BAND_04= 2.249 CORE REJECT 135 -5.4 7.83
```
**`ICE_RING_SCORE=`** is the strongest ice ring over the smooth radial background (fine-grained ice
@@ -639,7 +643,13 @@ that makes a smooth ring), **`ICE_SPOT_RATIO=`** the pile-up of found spots on t
against the ice-free flanks beside them (ice in large crystallites); 1 is no ice on either.
**`ICE_RINGS_DETECTED=`** is `TRUE` where either passes its gate (1.5 and 2.0), and
**`ICE_REFLECTIONS_ON_RINGS_PCT=`** is then the share of the integrated reflections set aside from
scaling. The summary has an `Ice` line, and where the merge found ice an `ICE_RINGS` warning prompts a
scaling. **`ICE_RING_BAND_nn=`** (rotation only) is one line per band of a ring - its core, within
0.01 Å$^{-1}$ in $q$ of the ring, and its low- and high-$q$ flanks out to the flag width: the ring's
$d$ (Å), the band, what the merge did with its reflections (`MERGE`, `DOWN_WEIGHT`, `REJECT`), how
many merged reflections it holds, their bias against the off-ring reflections around the ring in
standard errors, and the ratio of their variance to those reflections' (both on intensities
normalised by a local Wilson fit). A band is rejected where it is biased beyond 3 standard errors,
down-weighted where it is only noisier beyond 3, and merged otherwise. The summary has an `Ice` line, and where the merge found ice an `ICE_RINGS` warning prompts a
look at the ice-ring shells (one warning line, whichever of the two measurements raised it). The measurement is described in
[CPU/GPU data analysis ▸ Resolution and ice-ring handling](CPU_DATA_ANALYSIS_IMAGE.md#33-resolution-and-ice-ring-handling).
@@ -6014,9 +6014,9 @@ RotationScaleMerge::Result RotationScaleMerge::MergeAndStats(int n_groups, bool
a.swIh[half] += wI; a.swh[half] += w; a.nh[half]++;
a.swh_typ[half] += w * frame_cc_factor[o.frame];
// Any contributing observation on an ice ring marks the merged reflection (as
// MergeOnTheFly does on the stills path): the intensity is still merged and written,
// the flag only lets a consumer that must not read ring signal as crystal signal -
// the CC1/2 resolution-cutoff fit - leave it out.
// MergeOnTheFly does on the stills path): the ice-ring bands decision below reads it,
// and so does the CC1/2 resolution-cutoff fit, which must not read ring signal as
// crystal signal.
if (o.unscaled & UNSCALED_ICE) a.on_ice = true;
if (!std::isfinite(a.d) && std::isfinite(o.d) && o.d > 0.0f) a.d = o.d;
}
@@ -402,7 +402,8 @@ private:
}
// Why an observation stays out of the scale fit (Obs::unscaled), with the bit values the device flags
// use: it sits on an ice ring, whose background is the ring's, or it is coarser than the scale fit's
// low-resolution limit (ScalingSettings::low_resolution_limit_A). Either is still merged and written.
// low-resolution limit (ScalingSettings::low_resolution_limit_A). A low-resolution one is still merged
// and written; what the merge does with one on an ice ring is decided ring by ring (DecideIceRings).
static constexpr uint8_t UNSCALED_ICE = 1;
static constexpr uint8_t UNSCALED_LOW_RES = 8;
uint8_t Unscaled(const Reflection &r) const {
+20 -4
View File
@@ -2005,11 +2005,25 @@ ReportDocument BuildReportDocument(const std::string &output_prefix,
Add(s, KeyReal("ICE_SPOT_RATIO", *result.ice_spot_ratio, "{:.2f}"));
if (result.ice_rings_detected)
Add(s, KeyReal("ICE_REFLECTIONS_ON_RINGS_PCT", 100.0 * result.ice_flagged_fraction, "{:.1f}"));
for (size_t i = 0; i < result.ice_ring_verdicts.size(); ++i) {
const auto &z = result.ice_ring_verdicts[i];
Add(s, KeyText(fmt::format("ICE_RING_BAND_{:02}", i + 1).c_str(),
fmt::format("{:.3f} {} {} {} {:+.1f} {:.2f}", z.ring_d,
z.zone < 0 ? "LOW_FLANK" : (z.zone > 0 ? "HIGH_FLANK" : "CORE"),
z.action == IceRingAction::Reject ? "REJECT"
: (z.action == IceRingAction::DownWeight ? "DOWN_WEIGHT" : "MERGE"),
z.n, z.bias_z, z.var_ratio)));
}
Add(s, Blank());
Add(s, Prose(result.ice_rings_detected
? fmt::format(" Ice rings: DETECTED. The {:.1f}% of the integrated reflections that lie on the\n"
" hexagonal-ice rings were left out of scaling and kept in the merge.",
100.0 * result.ice_flagged_fraction)
" hexagonal-ice rings were left out of scaling. {}",
100.0 * result.ice_flagged_fraction,
result.ice_ring_verdicts.empty()
? "They were kept in the merge."
: "Each ring's core and flanks were then merged, down-weighted or\n"
" rejected by their own agreement with the reflections beside them (ICE_RING_BAND_nn:\n"
" ring d, band, action, reflections, bias in sigma, variance ratio).")
: std::string(" Ice rings: none detected, so no reflection was set aside for them.")));
// The merge's own ice gate is a second route to the same prompt; one warning line is enough.
const bool ice_warned = std::any_of(doc.warnings.begin(), doc.warnings.end(),
@@ -2017,7 +2031,7 @@ ReportDocument BuildReportDocument(const std::string &output_prefix,
if (result.ice_rings_detected && !ice_warned)
Warn(doc, PathologyCode::ICE_RINGS, fmt::format(
"Ice rings: the merge found hexagonal ice, and the {:.1f}% of the reflections on its rings were "
"left out of scaling and kept in the merge - check the ice-ring shells",
"left out of scaling - check the ice-ring shells",
100.0 * result.ice_flagged_fraction));
Add(s, Prose(" ICE_RING_SCORE is the strongest ice ring over the smooth radial background (smooth\n"
" ice), ICE_SPOT_RATIO the pile-up of found spots on the ring positions against the\n"
@@ -3294,7 +3308,9 @@ ReportDocument BuildReportDocument(const std::string &output_prefix,
if (result.ice_ring_score || result.ice_spot_ratio)
checked_row("Ice", {PathologyCode::ICE_RINGS}, result.ice_rings_detected
? fmt::format("rings DETECTED: the {:.1f}% of the reflections on them were left out of "
"scaling, kept in the merge", 100.0 * result.ice_flagged_fraction)
"scaling{}", 100.0 * result.ice_flagged_fraction,
result.ice_ring_verdicts.empty() ? ", kept in the merge"
: ", merged ring by ring")
: std::string("no rings detected"));
else
untested("Ice", {PathologyCode::ICE_RINGS});
+5 -1
View File
@@ -32,6 +32,7 @@
#include "../image_analysis/scale_merge/CentreOfSymmetry.h" // CentreOfSymmetryResult
#include "../image_analysis/scale_merge/StillsPartialityRefine.h" // StillsPartialityRefine::RunWidth
#include "../image_analysis/scale_merge/TranslationalNCS.h" // TranslationalNCSResult
#include "../image_analysis/scale_merge/IceRingVerdict.h" // IceRingZoneVerdict
#include "../image_analysis/indexing/HarmonicContamination.h" // HarmonicContaminationResult
#include "../image_analysis/geom_refinement/CellEsd.h" // CellEsd
#include "../image_analysis/structure_refinement/ModelValidation.h" // ModelValidationResult
@@ -696,11 +697,14 @@ struct ProcessResult {
// Hexagonal ice, as the merge's ice-ring handling measured it: the ring score on the radial
// profile and the spot pile-up on the ring positions (1 = no ice on either), whether either
// passed its gate, and the share of the integrated reflections then flagged as on a ring (left
// out of scaling, kept in the merge). Report only; unset where the merge did not run.
// out of scaling). Report only; unset where the merge did not run.
std::optional<float> ice_ring_score;
std::optional<float> ice_spot_ratio;
bool ice_rings_detected = false;
double ice_flagged_fraction = 0.0;
// What the rotation merge did with each ice-ring band (DecideIceRings): merged, down-weighted or
// rejected. Empty on stills and where nothing was flagged.
std::vector<IceRingZoneVerdict> ice_ring_verdicts;
// The reindex operators a rotation run's cell and space group admit (" ; "-joined), where no
// reference or model fixed the choice. Within one sweep the choice is arbitrary but consistent;
// it matters only against other data. Empty where there is none, or where it was resolved.
+8 -4
View File
@@ -628,8 +628,9 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) {
// Ice-ring handling (--detect-ice-rings): flag reflections sitting on a hexagonal-ice powder
// ring. Their integrated intensity is contaminated by the strong, variable ice background, so
// they drag the per-image scale fit. Flag them here so scaling (the per-image G fit and the
// fulls refit) skips them, while the combine, the merge and the statistics keep them - dropping
// them outright guts low/mid-resolution completeness on crystals that merge them fine.
// fulls refit) skips them. The stills merge keeps them; the rotation merge decides ring by ring
// whether to merge, down-weight or reject them (DecideIceRings) - dropping them all would gut
// low/mid-resolution completeness on crystals whose rings do them no harm.
// The eleven hexagonal-ice bands cover 16-26% of the unique reflections at typical resolutions
// whether or not the crystal has any ice, so handling ice unconditionally taxes clean data for
// nothing. Gate the flagging on the run's own measured ice strength (1 = no ice; the strongest
@@ -760,9 +761,10 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) {
result.ice_flagged_fraction = ice_total > 0 ? static_cast<double>(ice_flagged) / ice_total : 0.0;
logger.Info("Ice-ring handling: ice score {:.2f} (gate {:.2f}), spot ratio {:.2f} (gate "
"{:.2f}); flagged {} of {} reflections on ice rings (half-width {:.3f} A^-1); "
"excluded from scaling, kept for merging",
"excluded from scaling, {}",
ice_score.value_or(NAN), ice_min_score, ice_spot_ratio.value_or(NAN),
ice_min_spot_ratio, ice_flagged, ice_total, ice_width);
ice_min_spot_ratio, ice_flagged, ice_total, ice_width,
experiment_.IsRotationIndexing() ? "merged ring by ring as each band allows" : "kept for merging");
}
// Scale the images and merge. Factored so it can run twice: first in P1 to give the
@@ -1677,6 +1679,8 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) {
result.segment_decay_model = r.segment_decay_model;
if (for_search)
result.search_scaling_converged = r.scaling_converged;
else
result.ice_ring_verdicts = r.ice_ring_verdicts;
Note(fmt::format("merge in {}: {} unique", label, r.merged.size()));
return ScaleMergeResult{std::move(r.merged), std::move(r.statistics),
r.cc_half_before_corrections,
+2 -1
View File
@@ -2473,7 +2473,8 @@ int RugnuxInputRun::PrepareRugnuxInput() {
}
}
logger.Info("Ice-ring handling: flagged {} of {} reflections on ice rings (half-width {:.3f} A^-1); "
"excluded from scaling, kept for merging", flagged, total, ice_width);
"excluded from scaling, {}", flagged, total, ice_width,
experiment.IsRotationIndexing() ? "merged ring by ring as each band allows" : "kept for merging");
}
console->OnStep("Scaling and merging");
+13
View File
@@ -1283,6 +1283,19 @@ TEST_CASE("ResultReport_PowderAndIce", "[Diagnostics]") {
CHECK(text.find("Ice rings DETECTED: the 25.7% of the reflections") != std::string::npos);
// The merge found ice: a prompt to check the ice shells, and not a powder warning.
CHECK(text.find("\nPATHOLOGY_FLAGS= ICE_RINGS\n") != std::string::npos);
CHECK(text.find("ICE_RING_BAND_") == std::string::npos);
// A rotation merge that decided the ring bands lists one line per band.
result.ice_ring_verdicts = {{.ring_d = 3.895f, .zone = 0, .n = 52, .n_neighbours = 705, .bias_z = 6.7,
.var_ratio = 1.74, .var_ratio_z = 2.2, .action = IceRingAction::Reject},
{.ring_d = 3.895f, .zone = 1, .n = 43, .n_neighbours = 705, .bias_z = -0.6,
.var_ratio = 0.88, .var_ratio_z = -0.6, .action = IceRingAction::Merge}};
text = RenderResultReport("p", "in.h5", x, result);
CHECK(text.find("\nICE_RING_BAND_01= 3.895 CORE REJECT 52 +6.7 1.74\n") != std::string::npos);
CHECK(text.find("\nICE_RING_BAND_02= 3.895 HIGH_FLANK MERGE 43 -0.6 0.88\n") != std::string::npos);
CHECK(text.find("rings DETECTED: the 25.7% of the reflections on them were left out of scaling, merged ring by ring")
!= std::string::npos);
result.ice_ring_verdicts.clear();
// Rings that are not ice are a separate prompt, in their own words.
result.ice_rings_detected = false;