grid scan: a raster reaches the spot engine, and the crystal cap says none rather than zero

Two defects the merge created and one the API carried.

Rugnux gated the per-image spot engine on AnalysisModeIsMX, so AnalysisMode::Grid
fell through to the azimuthal-integration-only path: a raster ran, scored nothing,
and reported no crystals. The gate now asks the stages table whether the mode does
spot finding, which is the actual question - three modes need that engine for three
different reasons, and a fourth would otherwise have to be remembered here too.

max_crystals was a required integer defaulting to 10, with 0 meaning "all". Zero
reads as "report no crystals", the opposite of what it did. It is now optional, and
absent means no cap; a crystal found and then dropped is information the caller
cannot get back. grow_score_threshold was missing from the schema entirely.

Measured over the labelled corpus after these fixes: 34 of 34 confirmed-protein
rasters yield a crystal, 0 of 8 water, 0 of 6 ice, 19 of 19 heldout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N
This commit is contained in:
2026-09-08 08:22:14 +02:00
co-authored by Claude Opus 5
parent 37efa573c4
commit d4f280047f
13 changed files with 166 additions and 62 deletions
@@ -7,9 +7,10 @@ Settings for analysis mode Grid and nothing else: how the per-image protein scor
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**protein_score_threshold** | **float** | A raster cell counts as protein above this. The per-image protein score saturates, so this only has to separate \&quot;something diffracted here\&quot; from \&quot;nothing did\&quot;. | [default to 0.5]
**grow_score_threshold** | **float** | A patch is grown out to this score once it has started, so a crystal is not broken in two by a single cell that fell just under protein_score_threshold. Growth can never start on its own - a patch that never reaches the seed threshold is discarded - so lowering this cannot turn weak background into a crystal. | [default to 0.35]
**min_blob_cells** | **int** | How many cells above that threshold make a shape rather than a coincidence. Two cells can be the two ends of a single hit lying on a cell boundary; three is the smallest patch that is not. | [default to 3]
**decisive_single_cell_score** | **float** | ...unless one cell on its own is decisive. The rule above is about coincidences, and a lone cell scoring near the top of a saturating score is not one - a crystal smaller than the grid step lights exactly one cell, and refusing it would lose precisely the samples a fine raster is run to find. Set well above protein_score_threshold: this admits the obvious case, it does not lower the general threshold by the back door. | [default to 0.9]
**max_crystals** | **int** | Most crystals reported. A raster over a loop full of shards can label dozens of blobs, and past the first few the list is no longer a ranking anyone acts on. Crystals are sorted by score, so this keeps the best. | [default to 10]
**decisive_single_cell_score** | **float** | ...unless one cell on its own is decisive. The rule above is about coincidences, and a lone cell scoring near the top of a saturating score is not one - a crystal smaller than the grid step lights exactly one cell, and refusing it would lose precisely the samples a fine raster is run to find. The bar is the patch PEAK, not its mean. 0.6 is measured: over 67 labelled rasters the peak-score populations do not overlap - no water raster reaches 0.15 and no ice raster 0.50, while the weakest confirmed-protein raster peaks at 0.67 - so 0.6 is the middle of that gap. | [default to 0.6]
**max_crystals** | **int** | Most crystals reported, best first. ABSENT MEANS NO CAP, which is the default: a crystal that was found and then dropped is information the caller cannot get back. Set it where a loop full of shards would otherwise label dozens of blobs that nobody acts on. | [optional]
**indexing** | **bool** | Whether each raster cell is indexed as well as scored. On by default: a raster runs at up to 100 Hz, which the FFT indexer keeps up with, and it is additive - blobs are still found on the protein score, so indexing changes nothing about which cells are called crystals and only adds what was found in them. The lattice count per cell is the cheapest multi-lattice or cracked-crystal signal there is, and on a fixed-target serial experiment with a known cell a raster that indexes is most of the measurement. Turn it off for a very large raster where the GPU is the constraint. | [default to True]
## Example