diff --git a/docs/RUGNUX_REPORT.md b/docs/RUGNUX_REPORT.md index 63e73672d..d992bf483 100644 --- a/docs/RUGNUX_REPORT.md +++ b/docs/RUGNUX_REPORT.md @@ -178,13 +178,13 @@ and a run that produced a textbook data set read identically for their first thr `GONIO_SCALE`, `SPINDLE_CAP`, `ANISOTROPY`, `TWINNING`, `PSEUDO_TRANSLATION`, `LATTICE_TRANSLATION`, `MODEL_HAND`, `MODEL_NOT_VALIDATED`, `REFERENCE_MISMATCH`, `CANCELLED`, `RESOLUTION_FIT`, `FLIGHT_PATH`, `GEOMETRY_NOT_CONVERGED`, `SCALING_NOT_CONVERGED`, `HARMONIC_CONTAMINATION`, - `SUPERCELL_POSSIBLE`, `MULTIPLE_LATTICES`, `ICE_RINGS`, `POWDER_RINGS`, `BEAM_CENTRE_OFF`. `NONE` when nothing fired. A code appears if and only if its + `SUPERCELL_POSSIBLE`, `MULTIPLE_LATTICES`, `ICE_RINGS`, `POWDER_RINGS`, `BEAM_CENTRE_OFF`, `OVERLOADS`, `OVERLAPS`. `NONE` when nothing fired. A code appears if and only if its warning fired, so the flags and the `WARNING:` lines are two renderings of one list. Thresholds are deliberately low: a warning is a prompt to check something, not a verdict, and some of them fire on data that turn out fine — the closed type for machinery, the open sentence for a person. - Below them, one line each, the facts a reader needs before reading further: space group, cell, - mosaicity, the beam centre, the powder and ice rings, multiple lattices, resolution, completeness, signal, anomalous + mosaicity, the beam centre, the powder and ice rings, overloads, overlapping spots, multiple lattices, resolution, completeness, signal, anomalous signal, anisotropy, pseudo-symmetry, twinning, supercell, radiation damage and the sweep. A line states a finding that is not a condition too - weak powder rings, a possible supercell no stronger than correct cells show, an arbitrary indexing choice (`Indexing choice`, a rotation sweep whose cell @@ -519,6 +519,40 @@ scaling. The summary has an `Ice` line, and where the merge found ice an `ICE_RI 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). +## Overloads and overlapping spots + +Two ways the measurement, rather than the crystal, cost reflections - each with what to change at the +next collection. Both are in section 4, both have a summary line, and both warn at a low threshold. + +``` +OVERLOADED_REFLECTIONS= 1740 +OVERLOADED_REFLECTIONS_PCT= 0.017 +OVERLOADED_REFLECTIONS_LOW_RES_PCT= 0.418 +OVERLAPPED_REFLECTIONS_PCT= 11.10 +OVERLAPPED_REFLECTIONS_MAJOR_PCT= 0.44 +``` + +(a strongly diffracting lysozyme sweep: an `OVERLOADS` warning, no `OVERLAPS` one) + +**`OVERLOADED_REFLECTIONS=`** counts the integrated reflections - every frame's partial, before the +merge - with a saturated pixel in the signal disk (or one the detector could not count on that frame), +and **`OVERLOADED_REFLECTIONS_PCT=`** their share. They are not measurements: the merge leaves them out +(a rotation merge drops the whole rocking event, `OBSERVATIONS_REJECTED_OVERLOAD=` in section 3), and +they are the strongest reflections the crystal gave, so they concentrate at low resolution; +**`OVERLOADED_REFLECTIONS_LOW_RES_PCT=`** is their share among the reflections coarser than 4 Å. An +**`OVERLOADS`** warning fires from 0.05% of all integrated reflections or 0.25% of those coarser than 4 Å, +and recommends attenuating the beam or shortening the exposure (or a second, attenuated low-resolution +sweep). + +**`OVERLAPPED_REFLECTIONS_PCT=`** is the share of the predicted reflections whose signal disk a nearer +neighbour's prediction takes pixels from, as the integrator's overlap treatment (`--overlap`, default +`exclude`) assigns them, and **`OVERLAPPED_REFLECTIONS_MAJOR_PCT=`** the share left with less than +`--overlap-minpk` (default 75%) of the disk its own - the ones XDS's MINPK would reject. Under +`exclude` the shared pixels are left out of the profile fit, which keeps the reflection at a cost in +precision. An **`OVERLAPS`** warning fires from 1% in the second number and recommends a longer +detector distance, finer rotation slicing or a smaller beam. Neither key is written with +`--overlap off`, which assigns no pixel to anyone. + ## Index-2 superstructure On rotation data, section 4 reports whether the lattice the run adopted has intensity at half-integer diff --git a/image_analysis/MXAnalysisWithoutFPGA.cpp b/image_analysis/MXAnalysisWithoutFPGA.cpp index 6933bd473..566782e62 100644 --- a/image_analysis/MXAnalysisWithoutFPGA.cpp +++ b/image_analysis/MXAnalysisWithoutFPGA.cpp @@ -201,6 +201,8 @@ void MXAnalysisWithoutFPGA::Analyze(DataMessage &output, probe_counts.bkg_starved += after.bkg_starved - before.bkg_starved; probe_counts.bkg_starved_by_neighbour += after.bkg_starved_by_neighbour - before.bkg_starved_by_neighbour; probe_counts.profile_fallback += after.profile_fallback - before.profile_fallback; + probe_counts.overlapped += after.overlapped - before.overlapped; + probe_counts.overlapped_major += after.overlapped_major - before.overlapped_major; return ret; }; @@ -439,6 +441,8 @@ BraggIntegrationCounts MXAnalysisWithoutFPGA::BraggCounts() const { c.bkg_starved -= probe_counts.bkg_starved; c.bkg_starved_by_neighbour -= probe_counts.bkg_starved_by_neighbour; c.profile_fallback -= probe_counts.profile_fallback; + c.overlapped -= probe_counts.overlapped; + c.overlapped_major -= probe_counts.overlapped_major; return c; } diff --git a/image_analysis/bragg_integration/BraggIntegrationEngine.h b/image_analysis/bragg_integration/BraggIntegrationEngine.h index 981af9e6a..bc62af4b1 100644 --- a/image_analysis/bragg_integration/BraggIntegrationEngine.h +++ b/image_analysis/bragg_integration/BraggIntegrationEngine.h @@ -134,6 +134,12 @@ struct BraggIntegrationCounts { // costs data adds 4.1 percentage points on top of a floor of 0.02%. uint64_t bkg_starved_by_neighbour = 0; uint64_t profile_fallback = 0; // the profile fit disagreed with the box sum; the box sum was kept + // Signal disks a nearer neighbour's prediction takes pixels from (the overlap ownership, so counted + // only while an overlap treatment is on), and of those the ones left with less than + // overlap_min_peak of the disk their own - what --overlap reject drops, and XDS's MINPK. Reported + // only (the crowding of the pattern); nothing reads them. + uint64_t overlapped = 0; + uint64_t overlapped_major = 0; }; // One reflection's extracted intensity, produced by the derived engine and turned into a diff --git a/image_analysis/bragg_integration/BraggIntegrationEngineCPU.cpp b/image_analysis/bragg_integration/BraggIntegrationEngineCPU.cpp index 6cc8b8287..3f57f5744 100644 --- a/image_analysis/bragg_integration/BraggIntegrationEngineCPU.cpp +++ b/image_analysis/bragg_integration/BraggIntegrationEngineCPU.cpp @@ -269,6 +269,11 @@ std::vector BraggIntegrationEngineCPU::RunImpl(const Sampler &img, } } + if (use_owner && n_own < n_disk) { + ++counts.overlapped; + if (n_own < overlap_min_peak * n_disk) ++counts.overlapped_major; + } + int n_bkg_used = n_bkg; // pixels behind the FINAL background value (trim/clip shrink it) // A masked, untrusted, gapped or overloaded pixel inside the signal disk used to discard the // reflection outright. A profile fit does not need it to: the fit is the amplitude of a diff --git a/image_analysis/bragg_integration/BraggIntegrationEngineGPU.cu b/image_analysis/bragg_integration/BraggIntegrationEngineGPU.cu index 2acdbd862..0f458ce6e 100644 --- a/image_analysis/bragg_integration/BraggIntegrationEngineGPU.cu +++ b/image_analysis/bragg_integration/BraggIntegrationEngineGPU.cu @@ -49,7 +49,9 @@ constexpr int MOM_STRIDE = 3; constexpr int COUNT_BKG_STARVED = 0; constexpr int COUNT_BKG_STARVED_NEIGHBOUR = 1; constexpr int COUNT_PROFILE_FALLBACK = 2; -constexpr int COUNT_SLOTS = 3; +constexpr int COUNT_OVERLAPPED = 3; +constexpr int COUNT_OVERLAPPED_MAJOR = 4; +constexpr int COUNT_SLOTS = 5; // --- Mark the r2 signal region of every predicted reflection (race-free: one launch writes one // value). The region is the INNER stencil ellipse in the neighbour's own frame; see the CPU engine. @@ -277,6 +279,10 @@ __global__ void boxsum(const float *px_x, const float *px_y, const float *dd, atomicAdd(counts + COUNT_BKG_STARVED, 1ull); if ((s_full || p.partial_ok) && s_nclean <= 5 && s_nring > 5) atomicAdd(counts + COUNT_BKG_STARVED_NEIGHBOUR, 1ull); + if (p.overlap && s_nown < s_ndisk) { + atomicAdd(counts + COUNT_OVERLAPPED, 1ull); + if (s_nown < p.minpk * s_ndisk) atomicAdd(counts + COUNT_OVERLAPPED_MAJOR, 1ull); + } s_bkg = s_accept ? ((double) (long long) s_bkgsum / (double) s_nbkg) : 0.0; s_thr = s_bkg + (double) p.bkg_clip_nsigma * sqrt(fmax(s_bkg, 1.0)); // The pixel is an integer, so comparing it against the floor of the threshold accepts @@ -1083,11 +1089,13 @@ std::vector BraggIntegrationEngineGPU::Run(const ImagePreprocessorBu BraggIntegrationCounts BraggIntegrationEngineGPU::Counts() const { BraggIntegrationCounts out = counts; // `predicted` is counted on the host, as on the CPU engine - unsigned long long host[COUNT_SLOTS] = {0, 0, 0}; + unsigned long long host[COUNT_SLOTS] = {}; cuda_err(cudaMemcpyAsync(host, d_counts, sizeof(host), cudaMemcpyDeviceToHost, *stream)); cuda_err(cudaStreamSynchronize(*stream)); out.bkg_starved = host[COUNT_BKG_STARVED]; out.bkg_starved_by_neighbour = host[COUNT_BKG_STARVED_NEIGHBOUR]; out.profile_fallback = host[COUNT_PROFILE_FALLBACK]; + out.overlapped = host[COUNT_OVERLAPPED]; + out.overlapped_major = host[COUNT_OVERLAPPED_MAJOR]; return out; } diff --git a/rugnux/ReportDocument.h b/rugnux/ReportDocument.h index 5f1361317..6a171058c 100644 --- a/rugnux/ReportDocument.h +++ b/rugnux/ReportDocument.h @@ -52,6 +52,8 @@ namespace PathologyCode { constexpr const char *POWDER_RINGS = "POWDER_RINGS"; // non-ice powder rings hold many spots constexpr const char *ICE_RINGS = "ICE_RINGS"; // hexagonal-ice rings hold many spots constexpr const char *BEAM_CENTRE_OFF = "BEAM_CENTRE_OFF"; // the file's beam centre is wrong for this geometry + constexpr const char *OVERLOADS = "OVERLOADS"; // saturated pixels took reflections + constexpr const char *OVERLAPS = "OVERLAPS"; // neighbouring spots share signal pixels } // A closed type plus an open description. The type is what machinery switches on; the text is what a diff --git a/rugnux/ResultReport.cpp b/rugnux/ResultReport.cpp index 48b8847c7..eca796bb3 100644 --- a/rugnux/ResultReport.cpp +++ b/rugnux/ResultReport.cpp @@ -100,6 +100,17 @@ namespace { // fired on were below it (0.4-0.6% of the sweep). constexpr double SWEEP_GAPS_WARNING_FRACTION = 0.01; + // Overloads are warned about from this share of the integrated reflections, or of those coarser than + // OVERLOAD_LOW_RES_A, where a handful already takes the strongest low-resolution terms. Low on purpose: + // the strong-diffraction in-house sweep reads 0.017% and 0.42%, and its merge drops 0.12% of its + // rocking events; an ordinary sweep with a few hundred overloaded partials reads 0.001% and 0.012%. + constexpr double OVERLOAD_WARNING_PCT = 0.05; + constexpr double OVERLOAD_LOW_RES_WARNING_PCT = 0.25; + + // Overlapping spots are warned about from this share of the predicted reflections left with less + // than --overlap-minpk of their profile their own - the ones XDS's MINPK would reject. + constexpr double OVERLAP_WARNING_PCT = 1.0; + // Whether <|L|> - on the merge reported, or on the one the space-group search was given - reads // outside its physical range. Then no twin verdict is read from it, in either direction. bool TwinStatisticsAbnormal(const ProcessResult &r) { @@ -411,7 +422,7 @@ ReportDocument BuildReportDocument(const std::string &output_prefix, const bool merged = result.has_merge_statistics; doc.warnings = result.warnings; // SUMMARY rows filled by the sections that measure them; empty leaves the row out. - std::string beam_centre_row; + std::string beam_centre_row, overload_row, overlap_row; // ------------------------------------------------------------------ header block { @@ -1517,6 +1528,53 @@ ReportDocument BuildReportDocument(const std::string &output_prefix, Add(s, Blank()); } + // ---- overloads and overlapping spots: two ways the detector or the pattern took reflections + // the crystal gave, each with what to change at the next collection + if (result.integrated_reflections > 0) { + const double pct = 100.0 * result.overloaded_reflections / result.integrated_reflections; + const double pct_low = result.integrated_reflections_low_res > 0 + ? 100.0 * result.overloaded_reflections_low_res / result.integrated_reflections_low_res : 0.0; + Add(s, KeyInt("OVERLOADED_REFLECTIONS", result.overloaded_reflections)); + Add(s, KeyReal("OVERLOADED_REFLECTIONS_PCT", pct, "{:.3f}")); + Add(s, KeyReal("OVERLOADED_REFLECTIONS_LOW_RES_PCT", pct_low, "{:.3f}")); + overload_row = fmt::format("{:.3f}% of the integrated reflections, {:.3f}% of those coarser than {:.0f} A", + pct, pct_low, OVERLOAD_LOW_RES_A); + if (pct >= OVERLOAD_WARNING_PCT || pct_low >= OVERLOAD_LOW_RES_WARNING_PCT) + Warn(doc, PathologyCode::OVERLOADS, fmt::format( + "Overloads: {} integrated reflections ({:.3f}%; {:.2f}% of those coarser than {:.0f} A) held " + "a saturated pixel and are not measurements - the merge leaves them out, and they are the " + "strongest reflections the crystal gave. Attenuate the beam or shorten the exposure (or " + "collect a second, attenuated low-resolution sweep)", + result.overloaded_reflections, pct, pct_low, OVERLOAD_LOW_RES_A)); + } + if (result.predicted_reflections > 0 + && experiment.GetBraggIntegrationSettings().GetOverlap() != OverlapMode::Off) { + const double pct = 100.0 * static_cast(result.overlapped_reflections) + / static_cast(result.predicted_reflections); + const double pct_major = 100.0 * static_cast(result.overlapped_reflections_major) + / static_cast(result.predicted_reflections); + const double minpk = 100.0 * experiment.GetBraggIntegrationSettings().GetOverlapMinPeak(); + Add(s, KeyReal("OVERLAPPED_REFLECTIONS_PCT", pct, "{:.2f}")); + Add(s, KeyReal("OVERLAPPED_REFLECTIONS_MAJOR_PCT", pct_major, "{:.2f}")); + overlap_row = fmt::format("{:.2f}% of the predicted reflections share signal pixels, {:.2f}% keep under " + "{:.0f}% of their profile", pct, pct_major, minpk); + if (pct_major >= OVERLAP_WARNING_PCT) + Warn(doc, PathologyCode::OVERLAPS, fmt::format( + "Overlapping spots: {:.2f}% of the predicted reflections keep less than {:.0f}% of their " + "profile as their own ({:.2f}% share signal pixels with a neighbour) - the shared pixels " + "are left out of the fit, which costs precision. Move the detector back, slice the " + "rotation finer or use a smaller beam", pct_major, minpk, pct)); + } + if (result.integrated_reflections > 0 || result.predicted_reflections > 0) { + Add(s, Blank()); + Add(s, Prose(" OVERLOADED_REFLECTIONS counts the integrated reflections (every frame's partial) with a\n" + " saturated pixel in the signal disk, OVERLOADED_REFLECTIONS_LOW_RES_PCT their share among\n" + " those coarser than 4 A, where they concentrate. OVERLAPPED_REFLECTIONS_PCT is the share\n" + " of the predicted reflections whose signal disk a nearer neighbour takes pixels from,\n" + " OVERLAPPED_REFLECTIONS_MAJOR_PCT the share left with less than --overlap-minpk of it.")); + Add(s, Blank()); + } + // ---- translational pseudo-symmetry // Printed immediately before the twinning block, and in the same section, because the two // interact: a pseudo-translation that is not a half-integer vector biases <|L|> upwards and @@ -2444,6 +2502,10 @@ ReportDocument BuildReportDocument(const std::string &output_prefix, "with -z or --model", result.indexing_ambiguity_operators)); if (!beam_centre_row.empty()) checked_row("Beam centre", {PathologyCode::BEAM_CENTRE_OFF}, beam_centre_row); + if (!overload_row.empty()) + checked_row("Overloads", {PathologyCode::OVERLOADS}, overload_row); + if (!overlap_row.empty()) + checked_row("Overlaps", {PathologyCode::OVERLAPS}, overlap_row); if (!result.powder.rings_q_recipA.empty() || result.consensus_cell) { const auto &pw = result.powder; checked_row("Powder", {PathologyCode::POWDER_RINGS}, pw.rings_q_recipA.empty() ? std::string("no rings detected") diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index 2c8068da2..fe821b0ce 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -537,6 +537,17 @@ struct ProcessResult { std::optional background_beam_centre; float beam_centre_need_pxl = NAN; + // Overloads: integrated reflections (partial observations, every frame) a saturated pixel of the + // signal disk made unmeasurable (Reflection::overloaded), over the whole range and over the + // reflections coarser than OVERLOAD_LOW_RES_A, where they concentrate. Counted on the integration + // outcome before the merge; zero where nothing was integrated. + int64_t integrated_reflections = 0, overloaded_reflections = 0; + int64_t integrated_reflections_low_res = 0, overloaded_reflections_low_res = 0; + // Overlapping spots, as the integrator's overlap treatment measured them (BraggIntegrationCounts): + // predicted reflections whose signal disk a nearer neighbour takes pixels from, and of those the + // ones left with less than --overlap-minpk of it. Zero with --overlap off. + uint64_t predicted_reflections = 0, overlapped_reflections = 0, overlapped_reflections_major = 0; + // Which pass this result is (1-based) and how many ran, plus one line saying which pass became the // canonical output and on what evidence. pass_count == 1 outside the rotation two-pass. int pass_number = 1; @@ -568,6 +579,10 @@ struct ProcessResult { std::optional spindle_lost_unique_fraction; }; +// Overloads are counted separately over the reflections coarser than this (A) - the Wilson-plot +// convention's 4 A (CalcGlobalWilsonB), where the strong low-resolution reflections that saturate sit. +constexpr float OVERLOAD_LOW_RES_A = 4.0f; + // The rotation two-pass quality guard: whether the refined-geometry pass (pass2) is worse than the // header-geometry pass (pass1), judged on their search merges. Returns why, in the words the run // reports it in, or an empty string when the refined pass stands. diff --git a/rugnux/RugnuxImagePass.cpp b/rugnux/RugnuxImagePass.cpp index cf698fec3..0f7e3e64a 100644 --- a/rugnux/RugnuxImagePass.cpp +++ b/rugnux/RugnuxImagePass.cpp @@ -127,7 +127,7 @@ void Rugnux::ImagePass(PipelineLocals &p) { // finishes (BraggIntegrationCounts). Reading an engine's counts synchronises its stream, so it is // done once per worker at the end rather than per image. std::atomic bragg_predicted = 0, bragg_starved = 0, bragg_starved_nb = 0, - bragg_fallback = 0; + bragg_fallback = 0, bragg_overlapped = 0, bragg_overlapped_major = 0; // Calibration by spots: the pooled spot list of the whole run. Ring clustering is O(n^2) in it and // the Hough circle centre O(n^3) in its first few hundred, so each image contributes a fair share of @@ -300,6 +300,8 @@ void Rugnux::ImagePass(PipelineLocals &p) { bragg_starved += bragg.bkg_starved; bragg_starved_nb += bragg.bkg_starved_by_neighbour; bragg_fallback += bragg.profile_fallback; + bragg_overlapped += bragg.overlapped; + bragg_overlapped_major += bragg.overlapped_major; }; if (observer) @@ -475,6 +477,16 @@ void Rugnux::ImagePass(PipelineLocals &p) { experiment_.GetBraggIntegrationSettings().GetR3(), bragg_predicted.load(), frac(bragg_starved), frac(bragg_starved_nb), frac(bragg_fallback)); + // Overlapping spots: signal disks a nearer neighbour's prediction takes pixels from, as the + // overlap treatment decided ownership. For the report (OVERLAPPED_REFLECTIONS). + result.predicted_reflections = bragg_predicted; + result.overlapped_reflections = bragg_overlapped; + result.overlapped_reflections_major = bragg_overlapped_major; + if (experiment_.GetBraggIntegrationSettings().GetOverlap() != OverlapMode::Off) + logger.Info("Overlapping spots: {:.2f}% of the predicted reflections share signal pixels with a " + "nearer neighbour, {:.2f}% keep less than {:.0f}% of their profile as their own", + frac(bragg_overlapped), frac(bragg_overlapped_major), + 100.0 * experiment_.GetBraggIntegrationSettings().GetOverlapMinPeak()); } // Every image failing is a total failure, not a run that produced nothing: it used to be reported diff --git a/rugnux/RugnuxScaleMerge.cpp b/rugnux/RugnuxScaleMerge.cpp index 823d15df6..6c205a0d0 100644 --- a/rugnux/RugnuxScaleMerge.cpp +++ b/rugnux/RugnuxScaleMerge.cpp @@ -298,6 +298,32 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) { phase("Scaling and merging"); Step(geometry_prepass ? "Scaling and post-refinement" : "Scaling and merging"); + // Overloads, for the report: the integrated reflections a saturated signal pixel made unmeasurable. + // Counted before anything below drops or flags one. The merge leaves them out (a rotation merge + // drops the whole rocking event) - they are the strongest reflections, mostly at low resolution, + // and what a model's low-resolution terms rest on. + { + for (const auto &io : indexer->GetIntegrationOutcome()) + for (const auto &r : io.reflections) { + const bool low_res = r.d >= OVERLOAD_LOW_RES_A; + ++result.integrated_reflections; + result.integrated_reflections_low_res += low_res; + if (r.overloaded) { + ++result.overloaded_reflections; + result.overloaded_reflections_low_res += low_res; + } + } + if (result.integrated_reflections > 0) + logger.Info("Overloads: {} of {} integrated reflections ({:.3f}%) held a saturated pixel, {:.3f}% of " + "the {} coarser than {:.0f} A", result.overloaded_reflections, + result.integrated_reflections, + 100.0 * result.overloaded_reflections / result.integrated_reflections, + result.integrated_reflections_low_res > 0 + ? 100.0 * result.overloaded_reflections_low_res / result.integrated_reflections_low_res + : 0.0, + result.integrated_reflections_low_res, OVERLOAD_LOW_RES_A); + } + // 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 diff --git a/tests/ResultReportTest.cpp b/tests/ResultReportTest.cpp index 010930e83..cba64eef1 100644 --- a/tests/ResultReportTest.cpp +++ b/tests/ResultReportTest.cpp @@ -1236,3 +1236,30 @@ TEST_CASE("ResultReport_BeamCentreOff", "[Diagnostics]") { CHECK(text.find("\nPATHOLOGY_FLAGS= BEAM_CENTRE_OFF\n") != std::string::npos); CHECK(text.find("the background's word alone") != std::string::npos); } + +TEST_CASE("ResultReport_OverloadsAndOverlaps", "[Diagnostics]") { + DiffractionExperiment x(DetJF(1)); + ProcessResult result; + result.integrated_reflections = 100000; + result.integrated_reflections_low_res = 5000; + result.overloaded_reflections = 5; + result.overloaded_reflections_low_res = 5; + result.predicted_reflections = 100000; + result.overlapped_reflections = 5000; + result.overlapped_reflections_major = 500; + + auto text = RenderResultReport("p", "in.h5", x, result); + CHECK(text.find("\nOVERLOADED_REFLECTIONS= 5\n") != std::string::npos); + CHECK(text.find("\nOVERLOADED_REFLECTIONS_LOW_RES_PCT= 0.100\n") != std::string::npos); + CHECK(text.find("\nOVERLAPPED_REFLECTIONS_MAJOR_PCT= 0.50\n") != std::string::npos); + CHECK(text.find("\nPATHOLOGY_FLAGS= NONE\n") != std::string::npos); + + // 1.2% of the low-resolution reflections overloaded, 1.5% left under MINPK: both warn. + result.overloaded_reflections = 60; + result.overloaded_reflections_low_res = 60; + result.overlapped_reflections_major = 1500; + text = RenderResultReport("p", "in.h5", x, result); + CHECK(text.find("\nPATHOLOGY_FLAGS= OVERLOADS OVERLAPS\n") != std::string::npos); + CHECK(text.find("Attenuate the beam") != std::string::npos); + CHECK(text.find("Move the detector back") != std::string::npos); +}