McStas Readout Master 0.3.3
Loading...
Searching...
No Matches
CollectorClass.h
Go to the documentation of this file.
1// Copyright (C) 2026 European Spallation Source, ERIC. See LICENSE file
2//===----------------------------------------------------------------------===//
7//===----------------------------------------------------------------------===//
8#pragma once
9
10#include <string>
11#include <iomanip>
12#include <utility>
13#include <optional>
14#include <random>
15#include <map>
16#include <set>
17
18#include "Structs.h"
19#include "Readout.h"
20#include "enums.h"
21#include "hdf_interface.h"
22#include "version.hpp"
23#include "efu_time.h"
25
26
27#ifdef WIN32
28// Export symbols if compile flags "READOUT_SHARED" and "READOUT_EXPORT" are set on Windows.
29 #ifdef READOUT_SHARED
30 #ifdef READOUT_EXPORT
31 #define RL_API __declspec(dllexport)
32 #else
33 #define RL_API __declspec(dllimport)
34 #endif
35 #else
36 // Disable definition if linking statically.
37 #define RL_API
38 #endif
39#else
40// Disable definition for non-Win32 systems.
41#define RL_API
42#endif
43
47RL_API int validate_collector_file(const std::string & filename);
48RL_API int validate_collector_file_impl(const HighFive::File & file, const std::string & filename);
49
50RL_API void ensure_file_attributes(HighFive::File & file);
51RL_API void ensure_collector_group_attributes(HighFive::Group & group);
52RL_API void ensure_parameter_group_attributes(HighFive::Group & group);
53
54RL_API std::string validate_file_attributes(const HighFive::File & file);
55RL_API std::string validate_collector_group(const HighFive::Group & group);
56RL_API std::string validate_collector_root(const HighFive::Group & group) ;
57
58RL_API bool validate_collector_files(
59 const std::vector<std::string> & in_filenames,
60 std::set<std::string> & datasets,
61 std::map<std::string, size_t> & sizes,
62 std::set<std::string> & valid_files,
63 std::string & collector_name,
64 std::set<std::string> & parameters
65 );
66
73RL_API bool append_collector_files(const std::string & out_filename, const std::vector<std::string> & in_filenames, bool reset_datasets);
74
80RL_API bool concatenate_collector_files(const std::string & out_filename, const std::vector<std::string> & in_filenames);
81
88RL_API bool merge_collector_files(const std::string & out_filename, const std::vector<std::string> & in_filenames, bool remove_after_merge);
89
95RL_API bool combine_collector_files(const std::string & out_filename, const std::vector<std::string> & in_filenames);
96
104RL_API void merge_collector_datasets(const std::string & out_filename, const std::vector<std::string> & in_filenames, const std::string & which_dataset, bool remove_after_merge);
105
113RL_API void copy_collector_parameters(const std::string & out_filename, const std::vector<std::string> & in_filenames);
114
115// A singleton object to hold the current runtime's output file for the collector
117protected:
118 CollectorSink() = default;
119
120 std::set<std::string> users_;
121 std::optional<std::string> filename_{std::nullopt};
122 std::optional<HighFive::File> file_{std::nullopt};
123 std::optional<HighFive::Group> collector_, parameters_{std::nullopt};
124
125public:
126 static const std::string & type_attribute() {
127 static const std::string name{"type"};
128 return name;
129 }
130 static const std::string & collector_group_type() {
131 static const std::string name{"Readouts"};
132 return name;
133 }
134 static const std::string & readout_dataset_name() {
135 static const std::string name{"readouts"};
136 return name;
137 }
138 static const std::string & cue_dataset_name() {
139 static const std::string name{"cues"};
140 return name;
141 }
142 static const std::string & weight_dataset_name() {
143 static const std::string name{"weights"};
144 return name;
145 }
146 static const std::string & normalization_dataset_name() {
147 static const std::string name{"normalizations"};
148 return name;
149 }
150
155 static const std::string & detector_attribute_name() {
156 static const std::string name{"detector"};
157 return name;
158 }
159 static const std::string & efu_address_attribute_name() {
160 static const std::string name{"efu_address"};
161 return name;
162 }
163 static const std::string & efu_port_attribute_name() {
164 static const std::string name{"efu_port"};
165 return name;
166 }
167
168 static const std::string & parameter_group_type() {
169 static const std::string name{"Parameters"};
170 return name;
171 }
172 static const std::string & parameter_group_name() {
173 static const std::string name{"parameters"};
174 return name;
175 }
176 static const std::string & parameter_unit_attribute_name() {
177 static const std::string name{"unit"};
178 return name;
179 }
180 static const std::string & parameter_description_attribute_name() {
181 static const std::string name{"description"};
182 return name;
183 }
184 static const std::string & program_attribute_name() {
185 static const std::string name{"program"};
186 return name;
187 }
188 static const std::string & version_attribute_name() {
189 static const std::string name{"version"};
190 return name;
191 }
192 static const std::string & revision_attribute_name() {
193 static const std::string name{"revision"};
194 return name;
195 }
196 static const std::string & program_attribute_value() {
197 static const std::string name{"libreadout"};
198 return name;
199 }
200 static const std::string & version_attribute_value() {
201 static const std::string name{reinterpret_cast<const char *>(libreadout::version::version_number)};
202 return name;
203 }
204 static const std::string & revision_attribute_value() {
205 static const std::string name{reinterpret_cast<const char *>(libreadout::version::git_revision)};
206 return name;
207 }
208
209 CollectorSink(CollectorSink &other) = delete;
210 void operator=(const CollectorSink &) = delete;
211 // Defined in CollectorClass.cpp and exported: the singleton must live in
212 // libreadout so that every module of a process (the library itself, any C++
213 // consumer executing these header-inline methods) shares ONE sink. A
214 // header-inline definition would give each Windows module its own copy.
215 RL_API static CollectorSink * instance();
216 static void destroy() {
217 instance()->teardown();
218 }
219
220 bool is_setup() const { return filename_.has_value(); }
221 [[nodiscard]] std::string current_filename() const { return filename_.value_or(""); }
222 [[nodiscard]] size_t user_count() const { return users_.size(); }
223
224 void setup(const std::string& filename) {
225 filename_ = filename;
226 try {
227 // If the file doesn't exist, create it, otherwise open it for read/write so we can add to it
228 file_ = HighFive::File(filename, HighFive::File::OpenOrCreate);
229 } catch (HighFive::Exception & ex) {
230 std::cout << "Error opening file " << filename << " for writing:\n" << ex.what();
231 throw;
232 }
233 collector_ = file_->getGroup("/");
234 ensure_file_attributes(*file_);
235 }
236
237 void teardown() {
238 if (users_.empty()) {
239 if (file_.has_value()) {
240 file_->flush();
241 file_ = std::nullopt;
242 }
243 filename_ = std::nullopt;
244 collector_ = std::nullopt;
245 parameters_ = std::nullopt;
246 }
247 }
248
249
250 std::optional<HighFive::Group> getCollector(
251 const std::string& name,
252 const DetectorType& detector,
253 const ReadoutType& readout
254 ) {
255 return getCollector(name, hdf_compound_type(readout), detector);
256 }
257
265 std::optional<HighFive::Group> getCollector(
266 const std::string& name,
267 const HighFive::DataType& datatype,
268 const std::optional<DetectorType>& detector = std::nullopt,
269 const std::optional<std::string>& description = std::nullopt
270 ) {
271 using namespace HighFive;
272 if (!file_.has_value()) {
273 std::cerr << "File not initialized, cannot get dataset!" << std::endl;
274 return std::nullopt;
275 }
276 if (!collector_.has_value()) {
277 collector_ = file_->getGroup("/");
278 }
279 if (!collector_->exist(name)) {
280 auto group = collector_->createGroup(name);
281 group.createAttribute<std::string>(type_attribute(), collector_group_type());
282 if (detector.has_value()) {
283 group.createAttribute<std::string>(detector_attribute_name(), detectorType_name(detector.value()));
284 }
285
286 // but should chunk file operations to avoid too much disk IO?
287 DataSetCreateProps props;
288 props.add(Chunking(std::vector<hsize_t>{100}));
289
290 // create the readouts dataset, set to an empty vector []
291 auto ds = group.createDataSet(readout_dataset_name(), DataSpace({0}, {DataSpace::UNLIMITED}), datatype, props);
292 if (description.has_value()) {
293 ds.createAttribute<std::string>(parameter_description_attribute_name(), description.value());
294 }
295 // create the weights dataset, and set its value to [0.]
296 const auto ws = group.createDataSet(weight_dataset_name(), DataSpace({1}, {DataSpace::UNLIMITED}), AtomicType<double>(), props);
297 ws.select({0},{1}).write(0.);
298 // create the cues dataset, and set it to [0u]
299 const auto cs = group.createDataSet(cue_dataset_name(), DataSpace({1}, {DataSpace::UNLIMITED}), AtomicType<uint32_t>(), props);
300 cs.select({0},{1}).write(0);
301 // create the normalizations dataset, and set its value to [0]
302 const auto ns = group.createDataSet(normalization_dataset_name(), DataSpace({1}, {DataSpace::UNLIMITED}), AtomicType<uint64_t>(), props);
303 ns.select({0},{1}).write(0);
304
305 users_.insert(name);
306 }
307 // TODO verify that the group has the right components?
308 return collector_->getGroup(name);
309 }
310
311 [[nodiscard]] auto removeCollector(const std::string& name) {
312 return users_.erase(name);
313 }
314
315 template<class T>
316 void addParameter(
317 const std::string& name,
318 T value,
319 const std::optional<std::string> & unit = std::nullopt,
320 const std::optional<std::string> & description = std::nullopt
321 ) {
322 using namespace HighFive;
323 if (!file_.has_value()) {
324 std::cerr << "File not initialized, cannot add parameter!" << std::endl;
325 return;
326 }
327 if (!parameters_.has_value()) {
328 parameters_ = collector_->createGroup(parameter_group_name());
329 ensure_parameter_group_attributes(parameters_.value());
330 }
331 if (!parameters_->exist(name)) {
332 DataSetCreateProps props;
333 props.add(Chunking(std::vector<hsize_t>{100}));
334 auto ds = parameters_->createDataSet(name, DataSpace({1}, {DataSpace::UNLIMITED}), create_datatype<T>(), props);
335 ds.select({0},{1}).write(value);
336 if (unit.has_value()) {
337 ds.createAttribute(parameter_unit_attribute_name(), unit.value());
338 }
339 if (description.has_value()) {
340 ds.createAttribute(parameter_description_attribute_name(), description.value());
341 }
342 } else {
343 // verify that the existing dataset matches the new value's type
344 auto existing_ds = parameters_->getDataSet(name);
345 if (existing_ds.getDataType() != create_datatype<T>()) {
346 std::cerr << "Parameter " << name << " already exists with a different type!" << std::endl;
347 }
348 if (const auto n = parameter_unit_attribute_name();
349 unit.has_value()
350 && (!existing_ds.hasAttribute(n) || existing_ds.getAttribute(n).read<std::string>() != unit.value())
351 ) {
352 std::cerr << "Parameter " << name << " already exists with a different unit!" << std::endl;
353 }
354 if (const auto n = parameter_description_attribute_name();
355 description.has_value()
356 && (!existing_ds.hasAttribute(n) || existing_ds.getAttribute(n).read<std::string>() != description.value())
357 ) {
358 std::cerr << "Parameter " << name << " already exists with a different description!" << std::endl;
359 }
360 if (existing_ds.select({existing_ds.getDimensions().back()-1},{1}).read<T>() != value) {
361 std::cerr << "Parameter " << name << " already exists with a different value! Overwriting." << std::endl;
362 existing_ds.write(value);
363 }
364 }
365 }
366};
367
368
370 std::string name_;
371 std::optional<HighFive::Group> group_;
372 std::optional<HighFive::DataSet> dataset_;
373 double weight_{0};
374 uint64_t normalization_{0};
375 std::optional<DetectorType> detector_{DetectorType::Reserved};
376 std::optional<ReadoutType> readout_{ReadoutType::CAEN};
377 std::optional<HighFive::DataType> datatype_{std::nullopt};
378 size_t record_size_{0};
379
380public:
381
382 explicit Collector(
383 const std::string &filename,
384 std::string name,
385 const int Type=0x34,
386 const uint64_t normalization=0
387 ): name_{std::move(name)}, normalization_{normalization}, detector_{detectorType_from_int(Type)}, readout_{readoutType_from_detectorType(detector_.value())} {
388 datatype_ = hdf_compound_type(readout_.value());
389 record_size_ = datatype_->getSize();
390 const auto sink = CollectorSink::instance();
391 if (!sink->is_setup()) {
392 sink->setup(filename);
393 }
394 group_ = sink->getCollector(name_, detector_.value(), readout_.value());
395 dataset_ = group_->getDataSet(CollectorSink::readout_dataset_name());
396 }
397
408 explicit Collector(
409 const std::string &filename,
410 std::string name,
411 const std::string &type_description,
412 const uint64_t normalization=0,
413 const int Type=0
414 ): name_{std::move(name)}, normalization_{normalization},
415 detector_{Type > 0 ? std::optional(detectorType_from_int(Type)) : std::nullopt},
416 readout_{std::nullopt} {
417 const auto schema = parse_type_description(type_description);
418 datatype_ = build_hdf5_compound_type(schema);
419 record_size_ = schema.total_size;
420 const auto sink = CollectorSink::instance();
421 if (!sink->is_setup()) {
422 sink->setup(filename);
423 }
424 group_ = sink->getCollector(name_, datatype_.value(), detector_, type_description);
425 dataset_ = group_->getDataSet(CollectorSink::readout_dataset_name());
426 }
427
428 [[nodiscard]] size_t record_size() const { return record_size_; }
429
434 void addRecord(const double weight, const void * data) {
435 if (!dataset_.has_value() || !datatype_.has_value()) {
436 std::cerr << "Dataset not initialized, cannot save record!" << std::endl;
437 return;
438 }
439 weight_ += weight;
440 auto & d = dataset_.value();
441 const auto pos = d.getDimensions().back();
442 d.resize({pos + 1});
443 d.select({pos}, {1}).write_raw(static_cast<const uint8_t *>(data), datatype_.value());
444 }
445
446 // Add a readout with time and weight information to the writer's storage
447 void addReadout(const uint8_t Ring, const uint8_t FEN, const double tof, const double weight, const void * data) {
448 if (!readout_.has_value()) {
449 throw std::runtime_error("addReadout requires a typed Collector; use addRecord with a description-based Collector");
450 }
451 weight_ += weight;
452 switch (readout_.value()) {
453 case ReadoutType::CAEN: {
454 const CAEN_event event{Ring, FEN, tof, weight, static_cast<const CAEN_readout_t*>(data)};
455 return saveReadout(event);
456 }
457 case ReadoutType::TTLMonitor: {
458 const TTLMonitor_event event{Ring, FEN, tof, weight, static_cast<const TTLMonitor_readout_t*>(data)};
459 return saveReadout(event);
460 }
461 case ReadoutType::CDT: {
462 const CDT_event event{Ring, FEN, tof, weight, static_cast<const CDT_readout_t*>(data)};
463 return saveReadout(event);
464 }
465 case ReadoutType::VMM3: {
466 const VMM3_event event{Ring, FEN, tof, weight, static_cast<const VMM3_readout_t*>(data)};
467 return saveReadout(event);
468 }
469 case ReadoutType::BM0: {
470 const BM0_event event{Ring, FEN, tof, weight, static_cast<const BM0_readout_t*>(data)};
471 return saveReadout(event);
472 }
473 case ReadoutType::BM2: {
474 const BM2_event event{Ring, FEN, tof, weight, static_cast<const BM2_readout_t*>(data)};
475 return saveReadout(event);
476 }
477 case ReadoutType::BMI: {
478 const BMI_event event{Ring, FEN, tof, weight, static_cast<const BMI_readout_t*>(data)};
479 return saveReadout(event);
480 }
481 default:
482 throw std::runtime_error("Saving this readout type is not implemented yet!");
483 }
484 }
485
486 template<class ... Args> void addParameter(Args && ... args) {
487 CollectorSink::instance()->addParameter(std::forward<Args>(args)...);
488 }
489
492 void setEFU(const std::string & address, int port) {
493 if (!group_.has_value() || address.empty() || port <= 0) return;
494 if (!group_->hasAttribute(CollectorSink::efu_address_attribute_name())) {
495 group_->createAttribute<std::string>(CollectorSink::efu_address_attribute_name(), address);
496 }
497 if (!group_->hasAttribute(CollectorSink::efu_port_attribute_name())) {
498 group_->createAttribute<int>(CollectorSink::efu_port_attribute_name(), port);
499 }
500 }
501
502 ~Collector() {
503 // Add the accumulated weight to the existing dataset value -- which was initialized to [0.]
504 const auto wds = group_->getDataSet(CollectorSink::weight_dataset_name());
505 auto selection = wds.select({wds.getDimensions().back() - 1}, {1});
506 selection.write(weight_ + selection.read<double>());
507 // Record the number of readouts written -- where this points' readouts *end* (was initialized to [0u])
508 if (dataset_.has_value()) {
509 const auto count = dataset_->getDimensions().back();
510 const auto cds = group_->getDataSet(CollectorSink::cue_dataset_name());
511 auto cs = cds.select({cds.getDimensions().back()-1}, {1});
512 cs.write<uint32_t>(static_cast<uint32_t>(count));
513 }
514 // Add the normalization to the existing dataset value -- which was initialized to [0]
515 const auto nds = group_->getDataSet(CollectorSink::normalization_dataset_name());
516 auto ns = nds.select({nds.getDimensions().back() - 1}, {1});
517 ns.write(normalization_ + ns.read<uint64_t>());
518
519 const auto sink = CollectorSink::instance();
520 if (const auto count = sink->removeCollector(name_); count != 1) {
521 std::cerr << "Warning: removed collector " << name_ << " from sink " << count << " times, but expected to remove exactly one." << std::endl;
522 }
523 // try closing the sink file (internally it checks if any collectors are still using it):
524 sink->teardown();
525 }
526
527private:
528 template<class T>
529 void saveReadout(T data) {
530 //TODO Consider buffering this internally and only writing when the buffer is full (or closing the file)
531 if (!dataset_.has_value() || !datatype_.has_value()) {
532 std::cerr << "Dataset not initialized, cannot save readout!" << std::endl;
533 return;
534 }
535 auto & d = dataset_.value();
536 // the dataset should be 1-D ... hopefully that's true
537 auto pos = d.getDimensions().back();
538 auto size = pos + 1;
539 d.resize({size});
540 d.select({pos}, {1}).write_raw(reinterpret_cast<const uint8_t *>(&data), datatype_.value());
541 }
542};
543
544
545std::string filename_for_collector(const std::string & basepath, const std::string & basename, const std::string & extension = "h5");
546std::string filename_for_collector_node(const std::string & basepath, const std::string & basename, int node, int nodes);
RL_API bool merge_collector_files(const std::string &out_filename, const std::vector< std::string > &in_filenames, bool remove_after_merge)
Merge (append) readouts from multiple same-point collector files, optionally removing the inputs afte...
RL_API bool combine_collector_files(const std::string &out_filename, const std::vector< std::string > &in_filenames)
Combine multiple collector files automatically choosing append vs concatenate based on parameter iden...
RL_API void copy_collector_parameters(const std::string &out_filename, const std::vector< std::string > &in_filenames)
Copy parameters from multiple collector files into a single output file, for a specified scan point i...
RL_API void merge_collector_datasets(const std::string &out_filename, const std::vector< std::string > &in_filenames, const std::string &which_dataset, bool remove_after_merge)
Merge dataset(s) from multiple collector files into a single output file, for a specified scan point ...
RL_API bool append_collector_files(const std::string &out_filename, const std::vector< std::string > &in_filenames, bool reset_datasets)
Append readouts from multiple same-point collector files into a single output file.
RL_API int validate_collector_file(const std::string &filename)
Validate that a given file is a valid collector file, and return the number of scan points it contain...
RL_API bool concatenate_collector_files(const std::string &out_filename, const std::vector< std::string > &in_filenames)
Concatenate readouts from multiple different-point collector files into a single multi-point output f...
Umbrella header for the C API used by the McStas components.
ESS and Bifrost readout data structures.
Parser for C struct type descriptions into a runtime type schema.
RL_API TypeSchema parse_type_description(const std::string &description)
Parse a C struct type description string into a TypeSchema.
Minimal beam-monitor readout event: channel only.
Position-resolving beam-monitor readout event: channel, x, y.
Integrating beam-monitor readout event: channel, sum, 24-bit ADC.
CAEN readout event: group channel and amplitudes A-D.
CDT (DREAM-family) readout event: output module, cathode, anode.
std::optional< HighFive::Group > getCollector(const std::string &name, const HighFive::DataType &datatype, const std::optional< DetectorType > &detector=std::nullopt, const std::optional< std::string > &description=std::nullopt)
Get (or create) a collector group for records of an arbitrary compound datatype.
static const std::string & detector_attribute_name()
The detector identity is a GROUP attribute, alongside the EFU routing attributes: together they say w...
Collector(const std::string &filename, std::string name, const std::string &type_description, const uint64_t normalization=0, const int Type=0)
A description-based collector: records of a user-defined C struct layout.
void setEFU(const std::string &address, int port)
Write optional EFU destination attributes onto this collector group. Only writes if address is non-em...
void addRecord(const double weight, const void *data)
Store one record of the collector's compound type, accumulating its weight.
TTL beam-monitor readout event: channel, position, ADC.
VMM3 readout event: BC, OTADC, GEO, TDC, VMM, channel.