Files
leonarski_fandClaude Opus 5 8db75bf8e7 grid scan: the viewer's Grid mode is settable, not only startable
Grid was a mode with no settings. It could be selected and run, but the raster it scored
was whatever the file happened to record, what counted as a crystal was fixed at the
compile-time defaults, and the settings a Grid run actually uses - the cell, spot finding,
indexing - were built inside the MX page and therefore invisible in the mode that uses them.

The unit cell, goniometer, spot-finding and indexing sections now sit outside the page
stack and are shown for MX and Grid alike, the way the azimuthal section already was.
That is not four convenient extras: a raster is scored out of the spots, indexes every
cell by default, and is normally run against a known cell on a fixed target - and the
goniometer section is where the grid geometry itself is stated. Bragg integration, scaling
and the reference dataset stay on the MX page, because a raster integrates, scales and
merges nothing. None of them is duplicated; a second control for one setting is a pair of
controls that can disagree.

The Grid page carries the analysis: the two score thresholds, the minimum cells per
crystal, the decisive single-cell score, an optional cap and whether each cell is indexed.
Every default is read from GridScanAnalysisSettings rather than written out again, so this
panel and the web one cannot drift apart on what a default is; a test pins the numbers so
that moving one is a decision rather than a side effect. No cap is spelled as an unticked
box and not as a count that happens to mean "all". The settings ride to the run on the
experiment, which now survives the panel's round trip through the reader.

The grid geometry could already be edited but not trusted. A step of zero was quietly
replaced by a default, which is the worst answer available: the map is reshaped, every
crystal is reported somewhere else on the sample, and nothing looks wrong. It is now
refused - no grid is set, the summary says which field is at fault, and the run is refused
with it. The summary also says where the grid came from, the fields are highlighted when
they are not the file's, and images that do not fill the last row are called out. The
baseline for "not the file's" is taken when a file is opened, because datasetLoaded fires
again after every edit and by then the file's grid is indistinguishable from one typed
over it.

Two defects found while verifying this. A raster is stored with a stationary omega beside
its grid, since NXmx cannot say "no rotation" - and the panel read a goniometer of any kind
as outranking the grid, so the first grid-scan run to come back carrying that placeholder
axis flipped the panel to Rotation, and the next edit dropped the raster and got the
following run refused. A goniometer that does not scan no longer outranks a grid. And a
grid whose fields cannot make one is a half-finished edit, not a dataset that stopped being
a raster, so it no longer throws the mode away under the user mid-edit.

The Grid refusal now means what it says: no grid scan in the file AND none set in the
panel. A file that records no raster - an older one, a foreign one - can be given one and
analysed, which was the point.

Verified headlessly on a stored raster: the Grid page shows the file's 13 x 15 grid and the
analysis settings at their defaults, a run finds one crystal and draws it on the map, and
the same run with the protein score threshold at 0.95 finds none - so the settings reach
the analysis rather than merely being drawn. On a rotation dataset the mode is refused,
accepted once a grid is entered, and refused again with a zero step.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N
2026-09-08 14:46:33 +02:00

135 lines
5.5 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "JFJochReader.h"
#include <future>
JFJochReader &JFJochReader::Experiment(const DiffractionExperiment &experiment) {
std::unique_lock ul(m);
default_experiment = experiment;
return *this;
}
void JFJochReader::SummationThread(int64_t image0, int64_t n_image, int64_t image_jump, JFJochReaderImage &image) {
std::vector<uint8_t> buffer;
DataMessage msg;
for (int64_t i = image0; i < n_image; i += image_jump) {
bool ret = LoadImage_i(dataset, msg, buffer, i, false);
if (ret) {
auto image_sum = std::make_shared<JFJochReaderImage>(msg, dataset);
{
std::unique_lock ul(summation_mutex);
image.AddImage(*image_sum);
}
}
}
}
std::shared_ptr<JFJochReaderImage> JFJochReader::LoadImage(int64_t image_number, int64_t summation_factor) {
// It would be a mess to load two images at the same time
// so loading is protected via mutex
// yet copying share_ptr pointer is atomic and needs no mutex protection
std::unique_lock ul(m);
std::vector<uint8_t> buffer;
DataMessage msg;
if (LoadImage_i(dataset, msg, buffer, image_number, true)) {
auto image = std::make_shared<JFJochReaderImage>(msg, dataset);
if (summation_factor > 4) {
int64_t nthread = std::min<int64_t>(summation_factor - 1, 8);
std::vector<std::future<void>> futures;
for (int i = 0; i < nthread; i++)
futures.emplace_back(std::async(std::launch::async,
&JFJochReader::SummationThread, this,
image_number + 1 + i,
image_number + summation_factor,
nthread,
std::ref(*image)));
for (auto &f: futures)
f.get();
} else if (summation_factor > 1) {
SummationThread(image_number + 1, image_number + summation_factor, 1, *image);
}
return image;
}
return {};
}
void JFJochReader::SetStartMessage(const std::shared_ptr<JFJochReaderDataset> &val) {
std::unique_lock ul(m);
dataset = val;
}
std::shared_ptr<const JFJochReaderDataset> JFJochReader::GetDataset() const {
std::unique_lock ul(m);
return dataset;
}
void JFJochReader::UpdateGeomMetadata(const DiffractionExperiment &experiment) {
std::unique_lock ul(m);
if (!dataset)
return;
auto new_dataset = std::make_shared<JFJochReaderDataset>(*dataset);
// At the moment subset of options is limited to safe ones...need to change it in the future
new_dataset->experiment.BeamX_pxl(experiment.GetBeamX_pxl());
new_dataset->experiment.BeamY_pxl(experiment.GetBeamY_pxl());
new_dataset->experiment.DetectorDistance_mm(experiment.GetDetectorDistance_mm());
new_dataset->experiment.IncidentEnergy_keV(experiment.GetIncidentEnergy_keV());
new_dataset->experiment.PoniRot1_rad(experiment.GetDatasetSettings().GetPoniRot1_rad());
new_dataset->experiment.PoniRot2_rad(experiment.GetDatasetSettings().GetPoniRot2_rad());
new_dataset->experiment.PoniRot3_rad(experiment.GetDatasetSettings().GetPoniRot3_rad());
new_dataset->experiment.SetUnitCell(experiment.GetUnitCell());
new_dataset->experiment.SetSpaceGroup(experiment.GetGemmiSpaceGroup());
new_dataset->experiment.PolarizationFactor(experiment.GetPolarizationFactor());
new_dataset->experiment.Goniometer(experiment.GetGoniometer());
new_dataset->experiment.GridScan(experiment.GetGridScan());
new_dataset->experiment.ImportGridScanAnalysisSettings(experiment.GetGridScanAnalysisSettings());
new_dataset->experiment.ImportIndexingSettings(experiment.GetIndexingSettings());
new_dataset->experiment.ImportBraggIntegrationSettings(experiment.GetBraggIntegrationSettings());
new_dataset->experiment.DetectIceRings(experiment.IsDetectIceRings());
dataset = new_dataset;
}
void JFJochReader::UpdateUserMask(const std::vector<uint32_t> &mask) {
std::unique_lock ul(m);
if (!dataset)
return;
auto new_dataset = std::make_shared<JFJochReaderDataset>(*dataset);
// Copy-on-write: the mask is shared with the old snapshot, so edit a fresh copy, not in place.
auto new_mask = std::make_shared<PixelMask>(*dataset->pixel_mask);
new_mask->LoadUserMask(dataset->experiment, mask);
new_dataset->pixel_mask = new_mask;
dataset = new_dataset;
}
std::shared_ptr<JFJochReaderSpots> JFJochReader::ReadAllSpots(int64_t start_image, int64_t end_image,
int64_t stride) const {
if (start_image < 0)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Start image must be non-negative");
if (start_image > end_image)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Start image number is greater than end image number");
if (stride == 0)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Stride cannot be zero");
size_t nelems = (end_image - start_image) / stride + 1;
auto ret = std::make_shared<JFJochReaderSpots>();
ret->start_image = static_cast<int64_t>(start_image);
ret->stride = static_cast<int64_t>(stride);
ret->spots.reserve(nelems);
for (int i = 0; i < nelems; i++)
ret->spots.emplace_back(ReadSpots(start_image + i * stride));
return ret;
}