Build Packages / Unit tests (push) Successful in 1h22m15s
Build Packages / build:windows:nocuda (push) Successful in 18m0s
Build Packages / build:windows:cuda (push) Successful in 20m30s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m32s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m39s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m55s
Build Packages / build:rugnux:windows (push) Successful in 11m25s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 20m6s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m27s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 20m19s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m34s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 20m25s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m36s
Build Packages / build:rpm (rocky8) (push) Successful in 17m43s
Build Packages / build:rpm (rocky9) (push) Successful in 13m34s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 21m28s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 18m19s
Build Packages / DIALS test (push) Successful in 12m36s
Build Packages / XDS test (durin plugin) (push) Successful in 6m56s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 6m48s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m7s
Build Packages / Generate python client (push) Successful in 11s
Build Packages / Build documentation (push) Successful in 36s
Build Packages / Create release (push) Skipped
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m11s
* `rugnux --mode calibration` writes `<prefix>.json` beside the `.poni`, whose `dataset_settings` member is a `jfjoch_broker` `dataset_settings` body as it stands. * `rugnux` and `jfjoch_viewer` read PILATUS miniCBF sweeps natively, without conversion. * Masters written by other facilities open, including Eiger 1.x and third-party NXmx variants. * `rugnux` measures the beam centre on every run, and indexes with it when the file's value indexes nothing. * A detector swung out on a 2theta arm is placed where the file says it stands, and the calibration can hold the tilt fixed. * `rugnux` writes the unmerged MTZ by default, and a P1 merge beside it, so a wrong space group can be re-merged without reprocessing. * Significant improvements to symmetry handling in `rugnux`: the lattice, the point group, the setting and the systematic absences. * The `rugnux` report gives the resolution the CC1/2 fit reached, beside the range the reflections were written to. * The `rugnux` report gives the twinning statistics measured before the space group was decided, beside the ones measured after. * The `rugnux` report gives the strong-direction diffraction limit, and warns when CC1/2 is not monotone with resolution. * `rugnux` ranks screw axes on the evidence their absences carry, rather than on how many control reflections a candidate happens to have. * Twinning is no longer reported when the L-test contradicts it. * The `rugnux` report gives the detector tilt, the measured tilt and the direct beam beside the beam centre, and a post-refined beam centre is judged against the run's own measurement rather than the file's. * `--no-refine-tilt` holds the detector tilt at the value in the file, instead of zeroing it, when the calibration starts from the spots. * The `jfjoch_viewer` grid scan view draws the cells in the proportion of the scan steps, so the map has the shape of the scanned area. Reviewed-on: #76 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
136 lines
6.5 KiB
C++
136 lines
6.5 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "HDF5ImageLocator.h"
|
|
#include "../common/CompressedImage.h"
|
|
|
|
// Raw-pixel side of the reader. Turns a global image number into a CompressedImage, using
|
|
// HDF5ImageLocator to find the file (with its open-file cache). This is the part whose "links
|
|
// to files stay" constant: switching which master the per-image metadata is read from never
|
|
// touches it. Caller must hold the global hdf5_mutex (HDF5 is not thread-safe).
|
|
// Bit depth and signedness of /entry/data/data as it is STORED. Deliberately not the same thing as
|
|
// the experiment's image format: the reader hands every image out in a signed 32-bit container
|
|
// whatever the file holds (see HDF5MetadataSource), so an output file that links to the original
|
|
// images rather than writing its own must describe them with this, not with the experiment.
|
|
struct StoredPixelFormat {
|
|
int64_t bit_depth = 0;
|
|
bool is_signed = false;
|
|
};
|
|
|
|
class HDF5ImageSource {
|
|
public:
|
|
// Plain positional-read handle on a data file, opened alongside the HDF5 one. Owns the handle.
|
|
class RawFile {
|
|
public:
|
|
explicit RawFile(const std::string &path);
|
|
~RawFile();
|
|
RawFile(const RawFile &) = delete;
|
|
RawFile &operator=(const RawFile &) = delete;
|
|
|
|
bool IsOpen() const { return handle_ != -1; }
|
|
// Read `size` bytes from byte `address`. Positional and stateless, so any number of threads
|
|
// may call it on the same handle at once. Throws on a short read.
|
|
void ReadAt(void *dst, size_t size, uint64_t address) const;
|
|
|
|
private:
|
|
intptr_t handle_ = -1; // a file descriptor on POSIX, a HANDLE on Windows
|
|
};
|
|
|
|
// Where the bytes of one image are, and what they decode to. Everything needed to read an image
|
|
// without calling HDF5 again.
|
|
struct DirectChunk {
|
|
// Shared, not borrowed: GetRawImage drops the HDF5 lock before reading through this, so a
|
|
// concurrent Clear() - which ReadFile() and Close() both do - would otherwise free the file
|
|
// and close its descriptor under the reader.
|
|
std::shared_ptr<const RawFile> file;
|
|
uint64_t address = 0;
|
|
uint32_t size = 0;
|
|
hsize_t width = 0;
|
|
hsize_t height = 0;
|
|
CompressedImageMode mode{};
|
|
CompressionAlgorithm algorithm = CompressionAlgorithm::NO_COMPRESSION;
|
|
};
|
|
|
|
void Configure(HDF5ImageLocator::Layout layout);
|
|
void Clear();
|
|
|
|
[[nodiscard]] StoredPixelFormat GetStoredPixelFormat() const;
|
|
|
|
// Where image `global` physically lives. Also used by the metadata source to find the data
|
|
// file that holds a legacy/VDS image's per-image metadata.
|
|
HDF5ImageLocator::Location Resolve(int64_t global) const;
|
|
|
|
// Read the pixels at a resolved location into a CompressedImage backed by `buffer`. Templated on
|
|
// the allocator so a caller can hand over a buffer that does not zero what it is about to
|
|
// overwrite (RawByteBuffer).
|
|
template<class Alloc>
|
|
CompressedImage ReadImageAt(std::vector<uint8_t, Alloc> &buffer, const HDF5ImageLocator::Location &loc) const {
|
|
const auto &ds = GetDataset(loc);
|
|
const std::vector<hsize_t> start = {static_cast<hsize_t>(loc.local_index), 0, 0};
|
|
|
|
if (ds.direct_chunk)
|
|
ds.dataset->ReadDirectChunk(buffer, start);
|
|
else
|
|
ds.dataset->ReadVectorToU8(buffer, start, {1, ds.height, ds.width});
|
|
|
|
return {buffer.data(), buffer.size(), ds.width, ds.height, ds.mode, ds.algorithm};
|
|
}
|
|
|
|
// Ask HDF5 where image `loc` is in the file rather than asking it for the image. This is a
|
|
// lookup in the chunk index and nothing else - no read - so the mutex is held for a fraction of
|
|
// what an actual read costs, and the read itself then happens on any number of threads at once
|
|
// through ReadDirect(). Caller must hold hdf5_mutex.
|
|
//
|
|
// Empty when this file cannot be served that way: one chunk per image is what makes an image a
|
|
// single contiguous run of bytes, and a chunk that has never been written has no address at all.
|
|
// The caller falls back to ReadImageAt() then.
|
|
std::optional<DirectChunk> PrepareDirectRead(const HDF5ImageLocator::Location &loc) const;
|
|
|
|
// Read what PrepareDirectRead() found. Touches no HDF5 and no shared state, so it needs no
|
|
// mutex; this is the whole point of the two-step split.
|
|
static CompressedImage ReadDirect(RawByteBuffer &buffer, const DirectChunk &chunk);
|
|
|
|
std::vector<HDF5DataSourceMessage> GetSourceMapping(uint64_t first_image,
|
|
std::optional<uint64_t> image_count,
|
|
uint64_t total_images,
|
|
uint64_t stride = 1) const;
|
|
|
|
private:
|
|
HDF5ImageLocator locator_;
|
|
|
|
// /entry/data/data and everything asked of it here - its rank and dimensions, its element type,
|
|
// its chunking, its compression - are properties of the file, identical for every image in it.
|
|
// They used to be looked up again for each image: four HDF5 object opens per frame, inside the
|
|
// global hdf5_mutex that every worker thread queues on. Resolve them once per file instead.
|
|
//
|
|
// The entry keeps the file alive, so the pointer it is keyed by cannot be recycled underneath it
|
|
// and the dataset handle cannot outlive the file it belongs to.
|
|
struct OpenDataset {
|
|
std::shared_ptr<HDF5ReadOnlyFile> file;
|
|
std::unique_ptr<HDF5DataSet> dataset;
|
|
std::shared_ptr<RawFile> raw;
|
|
// HDF5 addresses count from the end of the user block, so they are file offsets only once
|
|
// its size is added. Zero for everything this project writes, but not for every file.
|
|
uint64_t user_block = 0;
|
|
hsize_t width = 0;
|
|
hsize_t height = 0;
|
|
CompressedImageMode mode{};
|
|
CompressionAlgorithm algorithm = CompressionAlgorithm::NO_COMPRESSION;
|
|
bool direct_chunk = false;
|
|
};
|
|
// Keyed by file AND dataset path: a master whose VDS sources are datasets in itself serves
|
|
// several of them out of one file, and keying by file alone would hand back the wrong one.
|
|
mutable std::map<std::pair<const HDF5ReadOnlyFile *, std::string>, OpenDataset> dataset_cache_;
|
|
const OpenDataset &GetDataset(const HDF5ImageLocator::Location &loc) const;
|
|
};
|