FERS 0.1.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
hdf5_handler.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2//
3// Copyright (c) 2006-2008 Marc Brooker and Michael Inggs
4// Copyright (c) 2008-present FERS Contributors (see AUTHORS.md).
5//
6// See the GNU GPLv2 LICENSE file in the FERS project root for more information.
7
8/**
9 * @file hdf5_handler.cpp
10 * @brief Source file for HDF5 data export and import functions.
11 */
12
13#include "hdf5_handler.h"
14
15#include <algorithm>
16#include <complex>
17#include <filesystem>
18#include <format>
19#include <highfive/highfive.hpp>
20#include <stdexcept>
21#include <string>
22#include <vector>
23
24#include "core/logging.h"
25#include "core/parameters.h"
26
27using logging::Level;
28
29namespace serial
30{
32
33 namespace
34 {
35 template <typename T>
36 void createOptionalAttribute(HighFive::File& file, const std::string& name, const std::optional<T>& value)
37 {
38 if (value.has_value())
39 {
40 file.createAttribute(name, *value);
41 }
42 }
43
44 template <typename T>
45 void createOptionalUnsignedAttribute(HighFive::File& file, const std::string& name,
46 const std::optional<T>& value)
47 {
48 if (value.has_value())
49 {
50 file.createAttribute(name, static_cast<unsigned long long>(*value));
51 }
52 }
53
54 void writeBaseMetadataAttributes(HighFive::File& file, const core::OutputFileMetadata& metadata)
55 {
56 file.createAttribute("fers_metadata_schema_version", 1U);
57 file.createAttribute("fers_metadata_json", core::outputFileMetadataToJsonString(metadata));
58 file.createAttribute("receiver_id", static_cast<unsigned long long>(metadata.receiver_id));
59 file.createAttribute("receiver_name", metadata.receiver_name);
60 file.createAttribute("data_mode", metadata.mode);
61 if (metadata.sampling_rate > 0.0)
62 {
63 file.createAttribute("output_sampling_rate", metadata.sampling_rate);
64 }
65 file.createAttribute("total_samples", static_cast<unsigned long long>(metadata.total_samples));
66 file.createAttribute("sample_start", static_cast<unsigned long long>(metadata.sample_start));
67 file.createAttribute("sample_end_exclusive",
68 static_cast<unsigned long long>(metadata.sample_end_exclusive));
69 file.createAttribute("streaming_segment_count",
70 static_cast<unsigned long long>(metadata.streaming_segments.size()));
71 file.createAttribute("fmcw_source_count", static_cast<unsigned long long>(metadata.fmcw_sources.size()));
72 file.createAttribute("fmcw_dechirp_mode", metadata.fmcw_dechirp_mode);
73 file.createAttribute("fmcw_dechirp_reference_source", metadata.fmcw_dechirp_reference_source);
74 file.createAttribute("fmcw_if_decimation_enabled", metadata.fmcw_if_decimation_enabled);
75 file.createAttribute("fmcw_if_legacy_full_rate", metadata.fmcw_if_legacy_full_rate);
76 file.createAttribute("fmcw_if_group_delay_compensated", metadata.fmcw_if_group_delay_compensated);
77 }
78
79 void writeFmcwIfAttributes(HighFive::File& file, const core::OutputFileMetadata& metadata)
80 {
81 createOptionalAttribute(file, "fmcw_if_requested_sample_rate", metadata.fmcw_if_requested_sample_rate);
82 createOptionalAttribute(file, "fmcw_if_sample_rate", metadata.fmcw_if_sample_rate);
83 createOptionalAttribute(file, "fmcw_if_input_sample_rate", metadata.fmcw_if_input_sample_rate);
84 createOptionalUnsignedAttribute(file, "fmcw_if_resample_numerator", metadata.fmcw_if_resample_numerator);
85 createOptionalUnsignedAttribute(file, "fmcw_if_resample_denominator",
87 createOptionalAttribute(file, "fmcw_if_decimation_factor", metadata.fmcw_if_decimation_factor);
88 createOptionalAttribute(file, "fmcw_if_filter_bandwidth", metadata.fmcw_if_filter_bandwidth);
89 createOptionalAttribute(file, "fmcw_if_filter_transition_width", metadata.fmcw_if_filter_transition_width);
90 createOptionalAttribute(file, "fmcw_if_filter_stopband", metadata.fmcw_if_filter_stopband);
91 createOptionalAttribute(file, "fmcw_if_filter_group_delay_seconds",
93 createOptionalUnsignedAttribute(file, "fmcw_if_compensated_integer_delay_samples",
95 createOptionalAttribute(file, "fmcw_if_compensated_fractional_delay_samples",
97 createOptionalUnsignedAttribute(file, "fmcw_if_warmup_discard_samples",
99 createOptionalUnsignedAttribute(file, "fmcw_if_phase_refinement", metadata.fmcw_if_phase_refinement);
100 createOptionalAttribute(file, "fmcw_if_timing_error_seconds", metadata.fmcw_if_timing_error_seconds);
101 createOptionalAttribute(file, "fmcw_if_phase_error_radians", metadata.fmcw_if_phase_error_radians);
102 createOptionalAttribute(file, "fmcw_if_noise_variance", metadata.fmcw_if_noise_variance);
103 }
104
105 void writeFmcwWaveformAttributes(HighFive::File& file, const std::string& prefix,
106 const core::FmcwMetadata& waveform, const bool write_counts)
107 {
108 file.createAttribute(prefix + "waveform_shape", waveform.waveform_shape);
109 file.createAttribute(prefix + "chirp_bandwidth", waveform.chirp_bandwidth);
110 file.createAttribute(prefix + "chirp_duration", waveform.chirp_duration);
111 file.createAttribute(prefix + "chirp_rate", waveform.chirp_rate);
112 file.createAttribute(prefix + "start_frequency_offset", waveform.start_frequency_offset);
113 if (waveform.waveform_shape == "linear")
114 {
115 file.createAttribute(prefix + "chirp_period", waveform.chirp_period);
116 file.createAttribute(prefix + "chirp_direction", waveform.chirp_direction);
117 if (write_counts)
118 {
119 file.createAttribute(prefix + "chirp_rate_signed", waveform.chirp_rate_signed);
120 createOptionalUnsignedAttribute(file, prefix + "chirp_count", waveform.chirp_count);
121 }
122 }
123 else if (waveform.waveform_shape == "triangle" && waveform.triangle_period.has_value())
124 {
125 file.createAttribute(prefix + "triangle_period", *waveform.triangle_period);
126 if (write_counts)
127 {
128 createOptionalUnsignedAttribute(file, prefix + "triangle_count", waveform.triangle_count);
129 }
130 }
131 }
132
133 void writeDechirpReferenceAttributes(HighFive::File& file, const core::OutputFileMetadata& metadata)
134 {
135 createOptionalUnsignedAttribute(file, "fmcw_dechirp_reference_transmitter_id",
137 createOptionalAttribute(file, "fmcw_dechirp_reference_transmitter_name",
139 createOptionalUnsignedAttribute(file, "fmcw_dechirp_reference_waveform_id",
141 createOptionalAttribute(file, "fmcw_dechirp_reference_waveform_name",
143 if (metadata.fmcw_dechirp_reference_waveform.has_value())
144 {
145 writeFmcwWaveformAttributes(file, "fmcw_dechirp_reference_", *metadata.fmcw_dechirp_reference_waveform,
146 false);
147 }
148 }
149
150 void writeStreamingSegmentFmcwAttributes(HighFive::File& file, const core::OutputFileMetadata& metadata)
151 {
152 std::vector<RealType> streaming_first_chirp_starts;
153 std::vector<unsigned long long> streaming_emitted_chirp_counts;
154 std::vector<RealType> streaming_first_triangle_starts;
155 std::vector<unsigned long long> streaming_emitted_triangle_counts;
156 for (const auto& segment : metadata.streaming_segments)
157 {
158 if (segment.first_chirp_start_time.has_value())
159 {
160 streaming_first_chirp_starts.push_back(*segment.first_chirp_start_time);
161 }
162 if (segment.emitted_chirp_count.has_value())
163 {
165 static_cast<unsigned long long>(*segment.emitted_chirp_count));
166 }
167 if (segment.first_triangle_start_time.has_value())
168 {
169 streaming_first_triangle_starts.push_back(*segment.first_triangle_start_time);
170 }
171 if (segment.emitted_triangle_count.has_value())
172 {
174 static_cast<unsigned long long>(*segment.emitted_triangle_count));
175 }
176 }
177 if (!streaming_first_chirp_starts.empty())
178 {
179 auto attr = file.createAttribute<RealType>("streaming_first_chirp_start_time",
180 HighFive::DataSpace::From(streaming_first_chirp_starts));
182 }
184 {
185 auto attr = file.createAttribute<unsigned long long>(
186 "streaming_emitted_chirp_count", HighFive::DataSpace::From(streaming_emitted_chirp_counts));
188 }
190 {
191 auto attr = file.createAttribute<RealType>("streaming_first_triangle_start_time",
192 HighFive::DataSpace::From(streaming_first_triangle_starts));
194 }
196 {
197 auto attr = file.createAttribute<unsigned long long>(
198 "streaming_emitted_triangle_count", HighFive::DataSpace::From(streaming_emitted_triangle_counts));
200 }
201 }
202
203 void writeFmcwAttributes(HighFive::File& file, const core::OutputFileMetadata& metadata)
204 {
205 if (!metadata.fmcw.has_value())
206 {
207 return;
208 }
209 writeFmcwWaveformAttributes(file, "fmcw_", *metadata.fmcw, true);
211 }
212 }
213
215 {
217 writeFmcwIfAttributes(file, metadata);
219 writeFmcwAttributes(file, metadata);
220 }
221
222 void readPulseData(const std::string& name, std::vector<ComplexType>& data)
223 {
224 std::scoped_lock const lock(hdf5_global_mutex);
225
226 if (!std::filesystem::exists(name))
227 {
228 LOG(Level::FATAL, "File '{}' not found", name);
229 throw std::runtime_error("File " + name + " not found.");
230 }
231
232 LOG(Level::TRACE, "Opening file '{}'", name);
233 const HighFive::File file(name, HighFive::File::ReadOnly);
234
235 // Helper lambda to open group and read dataset
236 auto read_dataset = [&file](const std::string& groupName, std::vector<double>& buffer) -> size_t
237 {
238 const auto group = file.getGroup("/" + groupName);
239
240 const auto dataset = group.getDataSet("value");
241
242 const auto dimensions = dataset.getSpace().getDimensions();
243 const auto size = dimensions[0];
244
245 buffer.resize(size);
246 dataset.read(buffer);
247
248 return size;
249 };
250
251 LOG(Level::TRACE, "Reading dataset 'I' from file '{}'", name);
252 std::vector<double> buffer_i;
253 const auto size = read_dataset("I", buffer_i);
254
255 std::vector<double> buffer_q;
256 LOG(Level::TRACE, "Reading dataset 'Q' from file '{}'", name);
257 if (read_dataset("Q", buffer_q) != size)
258 {
259 LOG(Level::FATAL, "Dataset 'Q' is not the same size as dataset 'I' in file '{}'", name);
260 throw std::runtime_error(R"(Dataset "Q" is not the same size as dataset "I" in file )" + name);
261 }
262
263 data.resize(size);
264 for (size_t i = 0; i < size; ++i)
265 {
266 data[i] = ComplexType(buffer_i[i], buffer_q[i]);
267 }
268 LOG(Level::TRACE, "Read dataset successfully");
269 }
270
271 void addChunkToFile(HighFive::File& file, const std::vector<ComplexType>& data, const RealType time,
272 const RealType fullscale, const unsigned count, const core::PulseChunkMetadata* metadata)
273 {
274 std::scoped_lock const lock(hdf5_global_mutex);
275
276 const std::size_t size = data.size();
277
278 const std::string base_chunk_name = "chunk_" + std::format("{:06}", count);
279 const std::string i_chunk_name = base_chunk_name + "_I";
280 const std::string q_chunk_name = base_chunk_name + "_Q";
281
282 std::vector<RealType> i(size), q(size);
283 std::ranges::transform(data, i.begin(), [](const ComplexType& c) { return c.real(); });
284 std::ranges::transform(data, q.begin(), [](const ComplexType& c) { return c.imag(); });
285
286 auto write_chunk = [&](const std::string& chunkName, const std::vector<RealType>& chunkData)
287 {
288 try
289 {
290 HighFive::DataSet dataset =
291 file.createDataSet<RealType>(chunkName, HighFive::DataSpace::From(chunkData));
292 dataset.write(chunkData);
293 }
294 catch (const HighFive::Exception& err)
295 {
296 LOG(Level::FATAL, "Error while writing data to HDF5 file: {}", err.what());
297 throw std::runtime_error("Error while writing data to HDF5 file: " + chunkName + " - " + err.what());
298 }
299 };
300
301 auto set_chunk_attributes = [&](const std::string& chunkName)
302 {
303 try
304 {
305 HighFive::DataSet dataset = file.getDataSet(chunkName);
306 dataset.createAttribute("time", time);
307 dataset.createAttribute("rate", params::rate());
308 dataset.createAttribute("fullscale", fullscale);
309 if (metadata != nullptr)
310 {
311 dataset.createAttribute("chunk_index", metadata->chunk_index);
312 dataset.createAttribute("sample_count", static_cast<unsigned long long>(metadata->sample_count));
313 dataset.createAttribute("sample_start", static_cast<unsigned long long>(metadata->sample_start));
314 dataset.createAttribute("sample_end_exclusive",
315 static_cast<unsigned long long>(metadata->sample_end_exclusive));
316 }
317 }
318 catch (const HighFive::Exception& err)
319 {
320 LOG(Level::FATAL, "Error while setting attributes on chunk: {}", err.what());
321 throw std::runtime_error("Error while setting attributes on chunk: " + chunkName + " - " + err.what());
322 }
323 };
324
327
330 }
331
332 std::vector<std::vector<RealType>> readPattern(const std::string& name, const std::string& datasetName)
333 {
334 std::scoped_lock const lock(hdf5_global_mutex);
335 try
336 {
337 LOG(Level::TRACE, "Reading dataset '{}' from file '{}'", datasetName, name);
338 const HighFive::File file(name, HighFive::File::ReadOnly);
339
340 const auto dataset = file.getDataSet(datasetName);
341
342 const auto dataspace = dataset.getSpace();
343 const auto dims = dataspace.getDimensions();
344
345 if (dims.size() != 2)
346 {
347 LOG(Level::FATAL, "Invalid dataset dimensions for '{}' in file '{}'", datasetName, name);
348 throw std::runtime_error(
349 std::format(R"(Invalid dataset dimensions for "{}" in file "{}")", datasetName, name));
350 }
351
352 LOG(Level::TRACE, "Reading dataset with dimensions {}x{}", dims[0], dims[1]);
353
354 std::vector data(dims[0], std::vector<RealType>(dims[1]));
355 dataset.read(data);
356
357 LOG(Level::TRACE, "Read dataset successfully");
358
359 return data;
360 }
361 catch (const HighFive::Exception& err)
362 {
363 LOG(Level::FATAL, "Error handling HDF5 file: {}", err.what());
364 throw std::runtime_error("Error handling HDF5 file: " + std::string(err.what()));
365 }
366 }
367}
double RealType
Type for real numbers.
Definition config.h:27
std::complex< RealType > ComplexType
Type for complex numbers.
Definition config.h:35
Header file for HDF5 data export and import functions.
Header file for the logging system.
#define LOG(level,...)
Definition logging.h:19
std::string outputFileMetadataToJsonString(const OutputFileMetadata &metadata)
Serializes one output-file metadata entry to JSON.
RealType rate() noexcept
Get the rendering sample rate.
Definition parameters.h:121
std::mutex hdf5_global_mutex
Global mutex to protect all HDF5 C-library calls, which are not thread-safe.
void writeOutputFileMetadataAttributes(HighFive::File &file, const core::OutputFileMetadata &metadata)
Writes additive FERS output metadata attributes to an open HDF5 file.
void addChunkToFile(HighFive::File &file, const std::vector< ComplexType > &data, const RealType time, const RealType fullscale, const unsigned count, const core::PulseChunkMetadata *metadata)
Adds a chunk of data to an HDF5 file.
void readPulseData(const std::string &name, std::vector< ComplexType > &data)
Reads pulse data from an HDF5 file.
std::vector< std::vector< RealType > > readPattern(const std::string &name, const std::string &datasetName)
Reads a 2D antenna gain pattern from the named dataset.
Defines the Parameters struct and provides methods for managing simulation parameters.
math::Vec3 max
RealType c
FMCW waveform metadata captured for a streaming output file.
RealType chirp_period
Chirp repetition period in seconds.
std::optional< std::uint64_t > chirp_count
Optional finite chirp count.
RealType chirp_rate
Frequency sweep rate in hertz per second.
RealType chirp_duration
Active chirp duration in seconds.
RealType chirp_bandwidth
Chirp bandwidth in hertz.
std::string chirp_direction
Frequency sweep direction token.
std::string waveform_shape
FMCW waveform shape token: linear or triangle.
std::optional< std::uint64_t > triangle_count
Optional finite triangle count.
RealType start_frequency_offset
Start frequency offset relative to carrier in hertz.
std::optional< RealType > triangle_period
Full triangle period in seconds.
RealType chirp_rate_signed
Signed frequency sweep rate in hertz per second.
Metadata for one receiver output file.
std::vector< FmcwSourceMetadata > fmcw_sources
FMCW illuminators represented in the output.
std::optional< RealType > fmcw_if_noise_variance
Post-resampling complex noise variance.
std::optional< RealType > fmcw_if_requested_sample_rate
Requested IF ADC rate in hertz.
std::uint64_t total_samples
Total sample count written to the file.
RealType sampling_rate
Sample rate for this output file in hertz.
std::optional< RealType > fmcw_if_timing_error_seconds
Estimated timing error.
std::optional< std::string > fmcw_dechirp_reference_transmitter_name
LO transmitter name.
SimId receiver_id
Receiver SimId that owns the output file.
std::uint64_t sample_start
Inclusive global sample index for the file start.
std::string fmcw_dechirp_mode
Receiver dechirp mode for FMCW streaming outputs.
std::optional< FmcwMetadata > fmcw
Optional FMCW metadata for streaming outputs.
std::uint64_t sample_end_exclusive
Exclusive global sample index for the file end.
bool fmcw_if_legacy_full_rate
True for legacy full-rate dechirped IF output.
std::optional< std::uint64_t > fmcw_if_compensated_integer_delay_samples
Integer output-delay compensation.
std::optional< RealType > fmcw_if_decimation_factor
Input/output sample-rate ratio.
std::optional< RealType > fmcw_if_sample_rate
Realized IF output sample rate in hertz.
std::optional< RealType > fmcw_if_compensated_fractional_delay_samples
Fractional output-delay compensation.
std::optional< FmcwMetadata > fmcw_dechirp_reference_waveform
Custom LO waveform parameters.
std::optional< RealType > fmcw_if_input_sample_rate
Input simulation sample rate in hertz.
std::string receiver_name
Receiver display name.
std::optional< SimId > fmcw_dechirp_reference_transmitter_id
Referenced LO transmitter ID.
std::optional< RealType > fmcw_if_filter_bandwidth
One-sided IF passband in hertz.
std::string mode
Output mode label, such as pulsed or streaming.
std::optional< RealType > fmcw_if_filter_stopband
IF stopband attenuation in dB.
std::optional< RealType > fmcw_if_phase_error_radians
Estimated IF edge phase error.
std::optional< unsigned > fmcw_if_resample_numerator
Reduced rational P.
std::optional< std::uint64_t > fmcw_if_warmup_discard_samples
Startup outputs discarded by the sink.
bool fmcw_if_decimation_enabled
True when IF-rate resampling is used.
bool fmcw_if_group_delay_compensated
True when IF output timestamps are aligned to t_start.
std::vector< StreamingSegmentMetadata > streaming_segments
Streaming segments written to the file.
std::optional< RealType > fmcw_if_filter_transition_width
IF transition width in hertz.
std::optional< RealType > fmcw_if_filter_group_delay_seconds
Total filter delay.
std::optional< std::string > fmcw_dechirp_reference_waveform_name
Custom LO waveform name.
std::string fmcw_dechirp_reference_source
Receiver dechirp reference source.
std::optional< unsigned > fmcw_if_phase_refinement
Polyphase refinement factor.
std::optional< unsigned > fmcw_if_resample_denominator
Reduced rational Q.
std::optional< SimId > fmcw_dechirp_reference_waveform_id
Custom LO waveform ID.
Metadata for one pulsed output chunk written to HDF5.
std::uint64_t sample_end_exclusive
Exclusive global sample index for the chunk end.
std::uint64_t sample_count
Number of samples in the chunk.
std::uint64_t sample_start
Inclusive global sample index for the chunk start.
unsigned chunk_index
Zero-based chunk index in the receiver output sequence.