grid scan: one cell is a crystal when that cell is decisive, and the list can be capped

The minimum patch size was a flat floor, so a crystal that covers one or two grid
points was thrown away however strong its diffraction. It is now a floor OR a piece
of evidence: a patch is reported when it has min_blob_cells cells, or when its best
cell reaches decisive_protein_score. One condition, and both halves are parameters.

The bar is the patch PEAK, not the patch mean. A two-cell patch with one strong cell
and one marginal one is the case this exists for, and the mean averages exactly that
evidence away. Over the 67 labelled rasters the two populations do not overlap: no
water raster reaches a peak of 0.15 and no ice raster reaches 0.50, while the weakest
protein raster peaks at 0.67 - so 0.6, the middle of that gap, is the default. The
peak is reported beside the mean, in the table and in the JSON, so an admission can
be checked against the number that decided it.

max_crystals caps the returned list after the sort, best first; 0, the default, is all
of them, because a crystal that was found and then dropped is information the caller
cannot get back.

The four parameters now travel as one GridScanAnalysisParameters, which is the shape
the GridScanAnalysisSettings class in common/ is to take: when it lands it replaces
this struct in the signature and nothing else changes.

On the corpus this reaches 17/17 protein loops (it was 16/17 - the miss was a crystal
covering two grid points of a 4x4 raster) with water still 0/4 and ice still 0/3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N
This commit is contained in:
leonarski_fandClaude Opus 5 committed 2026-09-08 07:15:30 +02:00
1 parent 1f54b3a971
commit cdb60c2916
10 files changed
+234 -59

No files matched your search

+5 -2
View File
@@ -50,8 +50,11 @@ TEST_CASE("AnalyzeGridScan finds nothing in an empty scan", "[AnalyzeGridScan]")
CHECK(result.crystals.empty());
}
SECTION("a two-cell patch is below the minimum blob size") {
auto scan = MakeScan(6, 6, {{2, 2}, {3, 2}}, 0.9f);
SECTION("a two-cell patch is below the minimum blob size and its score is not decisive") {
// Above the admission threshold, so the cells are in a patch at all, but under the score at
// which one cell would be evidence enough on its own. RasterReportTest covers the other
// side of that rule - a patch this small kept because its best cell IS decisive.
auto scan = MakeScan(6, 6, {{2, 2}, {3, 2}}, 0.55f);
auto result = AnalyzeGridScan(scan, grid, 5.0f, 5.0f);
CHECK(result.crystals.empty());
}