scan result: a grid scan's crystals and a calibration's geometry reach the stream, the file and the API
A grid scan and a powder calibration each produce an answer that is not a per-image number, and neither had anywhere to go. ScanResult gains two top-level optionals, grid and calibration, in the same idiom the three rotation_* members already use: a mode's payload is present when that mode ran and absent otherwise. images stays required, so every existing /result/scan client - the python client and the beamline scripts on it - is untouched. No oneOf or discriminator. The spec contains none today, so polymorphism is unexercised across all three generators at once, and the only thing it would buy is making "exactly one payload" structurally true rather than true by convention. powder_calibration_output has been fully specified and referenced by nothing since it was written; the calibration member reuses it. The crystal list travels the same three hops a per-image quantity does. In the CBOR end message grid_crystals is an array of maps keyed by field name - the shape a spot already uses - so a reader that does not know a field skips it and the record survives a field being added; the deserializer steps over an unknown key rather than reading its payload as the next one. In the HDF5 master it is parallel 1-D datasets under /entry/MX/crystals, one per field, because crystals are numbered rather than named and a group per crystal would put the index in the path. The reader reads them back so a stored raster re-opens with what it found. Nothing caps the list at one. A raster can find several crystals and the ordering carries the answer: sorted by score, best first. angle_deg is an axis, not a direction. It lies in [0, 180) and wraps, so 179 and 0 are adjacent and two of these do not average arithmetically - a pair of nearly parallel needles averaged across the wrap comes out as a right angle. That is stated at every layer it passes through, since it is the kind of field a consumer will reach for without reading the producer. The producer side is not wired yet: nothing fills ScanResult::grid or EndMessage::grid_crystals here. The seam is receiver/JFJochReceiver.cpp, where rotation_lattice is filled in EndDataCollection and GetFinalStatistics. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N
This commit is contained in:
@@ -784,6 +784,47 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
|
||||
dataset->file_analysis_mode =
|
||||
AnalysisModeFromName(master_file->GetString("/entry/MX/analysis_mode", ""));
|
||||
|
||||
// Grid-scan crystals: parallel 1-D datasets, one per field, all of length N. nx sets N and
|
||||
// the rest are read into that length, so a file written by a build that had one field
|
||||
// fewer still re-opens with the crystals it does have.
|
||||
if (master_file->Exists("/entry/MX/crystals/nx")) {
|
||||
auto nx = master_file->ReadOptVector<float>("/entry/MX/crystals/nx");
|
||||
auto ny = master_file->ReadOptVector<float>("/entry/MX/crystals/ny");
|
||||
auto x_um = master_file->ReadOptVector<float>("/entry/MX/crystals/xUm");
|
||||
auto y_um = master_file->ReadOptVector<float>("/entry/MX/crystals/yUm");
|
||||
auto image_number = master_file->ReadOptVector<int64_t>("/entry/MX/crystals/imageNumber");
|
||||
auto major_um = master_file->ReadOptVector<float>("/entry/MX/crystals/majorUm");
|
||||
auto minor_um = master_file->ReadOptVector<float>("/entry/MX/crystals/minorUm");
|
||||
auto angle_deg = master_file->ReadOptVector<float>("/entry/MX/crystals/angleDeg");
|
||||
auto score = master_file->ReadOptVector<float>("/entry/MX/crystals/score");
|
||||
auto ice_score = master_file->ReadOptVector<float>("/entry/MX/crystals/iceScore");
|
||||
auto res_A = master_file->ReadOptVector<float>("/entry/MX/crystals/resA");
|
||||
auto n_images = master_file->ReadOptVector<int64_t>("/entry/MX/crystals/nImages");
|
||||
|
||||
const size_t n = nx.size();
|
||||
auto f = [n](const std::vector<float> &v, size_t i) { return v.size() == n ? v[i] : 0.0f; };
|
||||
auto i64 = [n](const std::vector<int64_t> &v, size_t i) {
|
||||
return v.size() == n ? v[i] : int64_t(0);
|
||||
};
|
||||
|
||||
dataset->grid_crystals.resize(n);
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
auto &c = dataset->grid_crystals[i];
|
||||
c.nx = nx[i];
|
||||
c.ny = f(ny, i);
|
||||
c.x_um = f(x_um, i);
|
||||
c.y_um = f(y_um, i);
|
||||
c.image_number = i64(image_number, i);
|
||||
c.major_um = f(major_um, i);
|
||||
c.minor_um = f(minor_um, i);
|
||||
c.angle_deg = f(angle_deg, i);
|
||||
c.score = f(score, i);
|
||||
c.ice_score = f(ice_score, i);
|
||||
c.res_A = f(res_A, i);
|
||||
c.n_images = i64(n_images, i);
|
||||
}
|
||||
}
|
||||
|
||||
auto indexing = master_file->GetString("/entry/MX/indexing_algorithm", "none");
|
||||
if (indexing == "fft" || indexing == "FFT (CUDA)" || indexing == "FFT (FFTW)")
|
||||
dataset->experiment.IndexingAlgorithm(IndexingAlgorithmEnum::FFT);
|
||||
|
||||
Reference in New Issue
Block a user