FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
finalizer.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-only
2//
3// Copyright (c) 2025-present FERS Contributors (see AUTHORS.md).
4//
5// See the GNU GPLv2 LICENSE file in the FERS project root for more information.
6
7#include "finalizer.h"
8
9#include <algorithm>
10#include <chrono>
11#include <cmath>
12#include <filesystem>
13#include <format>
14#include <highfive/highfive.hpp>
15#include <limits>
16#include <utility>
17#include <vector>
18
19#include "core/logging.h"
21#include "core/parameters.h"
22#include "core/rendering_job.h"
23#include "core/sim_threading.h"
26#include "radar/receiver.h"
27#include "serial/hdf5_handler.h"
28#include "timing/timing.h"
29
30namespace processing
31{
32 namespace
33 {
34 void finalizePulsedMetadata(core::OutputFileMetadata& metadata)
35 {
36 metadata.pulse_count = static_cast<std::uint64_t>(metadata.chunks.size());
37 metadata.total_samples = 0;
38 metadata.min_pulse_length_samples = metadata.chunks.empty() ? 0 : std::numeric_limits<std::uint64_t>::max();
39 metadata.max_pulse_length_samples = 0;
40 metadata.uniform_pulse_length = true;
41
42 for (const auto& chunk : metadata.chunks)
43 {
44 metadata.total_samples += chunk.sample_count;
45 metadata.min_pulse_length_samples = std::min(metadata.min_pulse_length_samples, chunk.sample_count);
46 metadata.max_pulse_length_samples = std::max(metadata.max_pulse_length_samples, chunk.sample_count);
47 }
48
49 if (!metadata.chunks.empty())
50 {
51 const auto expected = metadata.chunks.front().sample_count;
52 metadata.uniform_pulse_length = std::ranges::all_of(metadata.chunks, [expected](const auto& chunk)
53 { return chunk.sample_count == expected; });
54 }
55
56 metadata.sample_start = 0;
57 metadata.sample_end_exclusive = metadata.total_samples;
58 }
59
60 core::OutputFileMetadata buildCwMetadata(const radar::Receiver* receiver, const std::string& hdf5_filename,
61 const std::size_t total_samples)
62 {
63 core::OutputFileMetadata metadata{.receiver_id = receiver->getId(),
64 .receiver_name = receiver->getName(),
65 .mode = "cw",
66 .path = hdf5_filename,
67 .total_samples = static_cast<std::uint64_t>(total_samples),
68 .sample_start = 0,
69 .sample_end_exclusive = static_cast<std::uint64_t>(total_samples)};
70
71 const auto append_segment = [&](const RealType start_time, const RealType end_time)
72 {
73 const auto start_sample = static_cast<std::uint64_t>(std::min<RealType>(
74 static_cast<RealType>(total_samples),
75 std::max<RealType>(0.0, std::ceil((start_time - params::startTime()) * params::rate()))));
76 const auto end_sample = static_cast<std::uint64_t>(std::min<RealType>(
77 static_cast<RealType>(total_samples),
78 std::max<RealType>(0.0, std::ceil((end_time - params::startTime()) * params::rate()))));
79 if (start_sample < end_sample)
80 {
81 metadata.cw_segments.push_back({.start_time = start_time,
82 .end_time = end_time,
83 .sample_count = end_sample - start_sample,
84 .sample_start = start_sample,
85 .sample_end_exclusive = end_sample});
86 }
87 };
88
89 const auto& schedule = receiver->getSchedule();
90 if (schedule.empty())
91 {
92 append_segment(params::startTime(), params::endTime());
93 }
94 else
95 {
96 for (const auto& period : schedule)
97 {
98 const RealType start = std::max(params::startTime(), period.start);
99 const RealType end = std::min(params::endTime(), period.end);
100 if (start < end)
101 {
102 append_segment(start, end);
103 }
104 }
105 }
106
107 return metadata;
108 }
109 }
110
111 void runPulsedFinalizer(radar::Receiver* receiver, const std::vector<std::unique_ptr<radar::Target>>* targets,
112 std::shared_ptr<core::ProgressReporter> reporter, const std::string& output_dir,
113 std::shared_ptr<core::OutputMetadataCollector> metadata_collector)
114 {
115 const auto timing_model = receiver->getTiming()->clone();
116 if (!timing_model)
117 {
118 LOG(logging::Level::FATAL, "Failed to clone timing model for receiver '{}'", receiver->getName());
119 return;
120 }
121
122 std::filesystem::path out_path(output_dir);
123 if (!std::filesystem::exists(out_path))
124 {
125 std::filesystem::create_directories(out_path);
126 }
127 const auto hdf5_filename = (out_path / std::format("{}_results.h5", receiver->getName())).string();
128 core::OutputFileMetadata file_metadata{.receiver_id = receiver->getId(),
129 .receiver_name = receiver->getName(),
130 .mode = "pulsed",
131 .path = hdf5_filename};
132
133 std::unique_ptr<HighFive::File> h5_file;
134 {
135 std::scoped_lock lock(serial::hdf5_global_mutex);
136 h5_file = std::make_unique<HighFive::File>(hdf5_filename, HighFive::File::Truncate);
137 }
138
139 unsigned chunk_index = 0;
140
141 LOG(logging::Level::INFO, "Finalizer thread started for receiver '{}'. Outputting to '{}'.",
142 receiver->getName(), hdf5_filename);
143
144 auto last_report_time = std::chrono::steady_clock::now();
145 const auto report_interval = std::chrono::milliseconds(100);
147 const RealType dt = 1.0 / rate;
148
149 while (true)
150 {
152 if (!receiver->waitAndDequeueFinalizerJob(job))
153 {
154 break; // Shutdown signal received
155 }
156
157 const auto window_samples = static_cast<unsigned>(std::ceil(job.duration * rate));
158 std::vector pnoise(window_samples, 0.0);
159
160 RealType actual_start = job.ideal_start_time;
161 RealType frac_delay = 0.0;
162
163 if (timing_model->isEnabled())
164 {
165 pipeline::advanceTimingModel(timing_model.get(), receiver, rate);
166 std::ranges::generate(pnoise, [&] { return timing_model->getNextSample(); });
167 std::tie(actual_start, frac_delay) = pipeline::calculateJitteredStart(
168 job.ideal_start_time, pnoise[0], timing_model->getFrequency(), rate);
169 }
170
171 std::vector<ComplexType> window_buffer(window_samples);
172
173 applyThermalNoise(window_buffer, receiver->getNoiseTemperature(receiver->getRotation(actual_start)),
174 receiver->getRngEngine());
175
176 pipeline::applyCwInterference(window_buffer, actual_start, dt, receiver, job.active_cw_sources, targets);
177
178 renderWindow(window_buffer, job.duration, actual_start, frac_delay, job.responses);
179
180 if (timing_model->isEnabled())
181 {
182 pipeline::addPhaseNoiseToWindow(pnoise, window_buffer);
183 }
184
185 const RealType fullscale = pipeline::applyDownsamplingAndQuantization(window_buffer);
186
187 const auto current_chunk_index = chunk_index++;
188 const auto sample_start = file_metadata.total_samples;
189 core::PulseChunkMetadata chunk_metadata{.chunk_index = current_chunk_index,
190 .i_dataset = std::format("chunk_{:06}_I", current_chunk_index),
191 .q_dataset = std::format("chunk_{:06}_Q", current_chunk_index),
192 .start_time = actual_start,
193 .sample_count = static_cast<std::uint64_t>(window_buffer.size()),
194 .sample_start = sample_start,
195 .sample_end_exclusive = sample_start +
196 static_cast<std::uint64_t>(window_buffer.size())};
197
198 serial::addChunkToFile(*h5_file, window_buffer, actual_start, fullscale, current_chunk_index,
199 &chunk_metadata);
200 file_metadata.chunks.push_back(std::move(chunk_metadata));
201 file_metadata.total_samples = file_metadata.chunks.back().sample_end_exclusive;
202
203 if (reporter)
204 {
205 const auto now = std::chrono::steady_clock::now();
206 if ((now - last_report_time) >= report_interval)
207 {
208 reporter->report(std::format("Exporting {}: Chunk {}", receiver->getName(), chunk_index),
209 static_cast<int>(chunk_index), 0);
210 last_report_time = now;
211 }
212 }
213 }
214
215 finalizePulsedMetadata(file_metadata);
216 {
217 std::scoped_lock lock(serial::hdf5_global_mutex);
218 serial::writeOutputFileMetadataAttributes(*h5_file, file_metadata);
219 }
220
221 {
222 // Safe destruction of the HDF5 object inside a lock
223 std::scoped_lock lock(serial::hdf5_global_mutex);
224 h5_file.reset();
225 }
226
227 if (metadata_collector)
228 {
229 metadata_collector->addFile(std::move(file_metadata));
230 }
231
232 if (reporter)
233 {
234 reporter->report(std::format("Finished Exporting {}", receiver->getName()), 100, 100);
235 }
236 LOG(logging::Level::INFO, "Finalizer thread for receiver '{}' finished.", receiver->getName());
237 }
238
240 std::shared_ptr<core::ProgressReporter> reporter, const std::string& output_dir,
241 std::shared_ptr<core::OutputMetadataCollector> metadata_collector)
242 {
243 LOG(logging::Level::INFO, "Finalization task started for CW receiver '{}'.", receiver->getName());
244 if (reporter)
245 {
246 reporter->report(std::format("Finalizing CW Receiver {}", receiver->getName()), 0, 100);
247 }
248
249 auto& iq_buffer = receiver->getMutableCwData();
250 if (iq_buffer.empty())
251 {
252 LOG(logging::Level::INFO, "No CW data to finalize for receiver '{}'.", receiver->getName());
253 return;
254 }
255
256 if (reporter)
257 {
258 reporter->report(std::format("Rendering Interference for {}", receiver->getName()), 25, 100);
259 }
261
262 const auto timing_model = receiver->getTiming()->clone();
263 if (!timing_model)
264 {
265 LOG(logging::Level::FATAL, "Failed to clone timing model for CW receiver '{}'", receiver->getName());
266 return;
267 }
268
269 if (reporter)
270 {
271 reporter->report(std::format("Applying Noise for {}", receiver->getName()), 50, 100);
272 }
273 applyThermalNoise(iq_buffer, receiver->getNoiseTemperature(), receiver->getRngEngine());
274
275 if (timing_model->isEnabled())
276 {
277 std::vector pnoise(iq_buffer.size(), 0.0);
278 std::ranges::generate(pnoise, [&] { return timing_model->getNextSample(); });
279 pipeline::addPhaseNoiseToWindow(pnoise, iq_buffer);
280 }
281
282 const RealType fullscale = pipeline::applyDownsamplingAndQuantization(iq_buffer);
283
284 if (reporter)
285 {
286 reporter->report(std::format("Writing HDF5 for {}", receiver->getName()), 75, 100);
287 }
288
289 std::filesystem::path out_path(output_dir);
290 if (!std::filesystem::exists(out_path))
291 {
292 std::filesystem::create_directories(out_path);
293 }
294 const auto hdf5_filename = (out_path / std::format("{}_results.h5", receiver->getName())).string();
295 auto file_metadata = buildCwMetadata(receiver, hdf5_filename, iq_buffer.size());
296 pipeline::exportCwToHdf5(hdf5_filename, iq_buffer, fullscale, timing_model->getFrequency(), &file_metadata);
297 if (metadata_collector)
298 {
299 metadata_collector->addFile(std::move(file_metadata));
300 }
301
302 if (reporter)
303 {
304 reporter->report(std::format("Finalized {}", receiver->getName()), 100, 100);
305 }
306 }
307}
A simple thread pool implementation.
Definition thread_pool.h:29
const std::string & getName() const noexcept
Retrieves the name of the object.
Definition object.h:79
math::SVec3 getRotation(const RealType time) const
Retrieves the rotation of the object.
Definition object.h:58
std::shared_ptr< timing::Timing > getTiming() const
Retrieves the timing source for the radar.
Definition radar_obj.cpp:66
Manages radar signal reception and response processing.
Definition receiver.h:37
std::mt19937 & getRngEngine() noexcept
Gets the receiver's internal random number generator engine.
Definition receiver.h:153
const std::vector< SchedulePeriod > & getSchedule() const noexcept
Retrieves the list of active reception periods.
Definition receiver.h:277
std::vector< ComplexType > & getMutableCwData()
Retrieves the collected CW IQ data for modification.
Definition receiver.h:256
const std::vector< std::unique_ptr< serial::Response > > & getPulsedInterferenceLog() const
Retrieves the log of pulsed interferences for CW mode.
Definition receiver.h:262
SimId getId() const noexcept
Retrieves the unique ID of the receiver.
Definition receiver.h:95
RealType getNoiseTemperature() const noexcept
Retrieves the noise temperature of the receiver.
Definition receiver.h:102
bool waitAndDequeueFinalizerJob(core::RenderingJob &job)
Waits for and dequeues a RenderingJob from the finalizer queue.
Definition receiver.cpp:59
double RealType
Type for real numbers.
Definition config.h:27
Declares the functions for the asynchronous receiver finalization pipelines.
Declares focused, testable pipeline steps for receiver finalization.
Header file for HDF5 data export and import functions.
Header file for the logging system.
#define LOG(level,...)
Definition logging.h:19
@ FATAL
Fatal level for severe error events.
@ INFO
Info level for informational messages.
RealType endTime() noexcept
Get the end time for the simulation.
Definition parameters.h:109
RealType rate() noexcept
Get the rendering sample rate.
Definition parameters.h:121
RealType startTime() noexcept
Get the start time for the simulation.
Definition parameters.h:103
unsigned oversampleRatio() noexcept
Get the oversampling ratio.
Definition parameters.h:151
void addPhaseNoiseToWindow(std::span< const RealType > noise, std::span< ComplexType > window)
Applies a pre-generated sequence of phase noise samples to an I/Q buffer.
RealType applyDownsamplingAndQuantization(std::vector< ComplexType > &buffer)
Downsamples and quantizes an IQ buffer.
void advanceTimingModel(timing::Timing *timing_model, const radar::Receiver *receiver, const RealType rate)
Advances the receiver's timing model to the start of the next processing window.
void exportCwToHdf5(const std::string &filename, const std::vector< ComplexType > &iq_buffer, const RealType fullscale, const RealType ref_freq, const core::OutputFileMetadata *metadata)
Exports a finalized continuous-wave IQ buffer to an HDF5 file.
std::tuple< RealType, RealType > calculateJitteredStart(const RealType ideal_start, const RealType first_phase_noise, const RealType carrier_freq, const RealType rate)
Calculates the jittered start time and fractional delay from a phase noise sample.
void applyPulsedInterference(std::vector< ComplexType > &iq_buffer, const std::vector< std::unique_ptr< serial::Response > > &interference_log)
Renders and applies pulsed interference to a continuous-wave IQ buffer.
void applyCwInterference(std::span< ComplexType > window, const RealType actual_start, const RealType dt, const radar::Receiver *receiver, const std::vector< radar::Transmitter * > &cw_sources, const std::vector< std::unique_ptr< radar::Target > > *targets)
Applies continuous-wave interference to a time window.
void finalizeCwReceiver(radar::Receiver *receiver, pool::ThreadPool *, std::shared_ptr< core::ProgressReporter > reporter, const std::string &output_dir, std::shared_ptr< core::OutputMetadataCollector > metadata_collector)
The finalization task for a continuous-wave (CW) mode receiver.
void applyThermalNoise(std::span< ComplexType > window, const RealType noiseTemperature, std::mt19937 &rngEngine)
Applies thermal (Johnson-Nyquist) noise to a window of I/Q samples.
void renderWindow(std::vector< ComplexType > &window, const RealType length, const RealType start, const RealType fracDelay, const std::span< const std::unique_ptr< serial::Response > > responses)
Renders a time-window of I/Q data from a collection of raw radar responses.
void runPulsedFinalizer(radar::Receiver *receiver, const std::vector< std::unique_ptr< radar::Target > > *targets, std::shared_ptr< core::ProgressReporter > reporter, const std::string &output_dir, std::shared_ptr< core::OutputMetadataCollector > metadata_collector)
The main function for a dedicated pulsed-mode receiver finalizer thread.
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.
Defines the Parameters struct and provides methods for managing simulation parameters.
Radar Receiver class for managing signal reception and response handling.
Defines the data packet for asynchronous receiver finalization.
Header for receiver-side signal processing and rendering.
Header file for the main simulation runner.
std::uint64_t max_pulse_length_samples
std::vector< PulseChunkMetadata > chunks
std::uint64_t sample_end_exclusive
std::uint64_t min_pulse_length_samples
std::vector< CwSegmentMetadata > cw_segments
A data packet containing all information needed to process one receive window.
RealType duration
The duration of the receive window in seconds.
std::vector< std::unique_ptr< serial::Response > > responses
A list of all Response objects that overlap with this window.
RealType ideal_start_time
The ideal, jitter-free start time of the receive window.
std::vector< radar::Transmitter * > active_cw_sources
A list of all CW transmitters that were active during this window.
Timing source for simulation objects.