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/**
8 * @file finalizer.cpp
9 * @brief Implements the asynchronous receiver data processing and output pipelines.
10 *
11 * This file contains the core logic for finalizing received radar data.
12 * Finalization is performed asynchronously to the main simulation loop to avoid
13 * blocking physics calculations with expensive tasks like signal rendering,
14 * processing, and file I/O.
15 *
16 * Two distinct finalization pipelines are implemented:
17 * 1. `runPulsedFinalizer`: A long-running function executed in a dedicated
18 * thread for each pulsed-mode receiver. It processes data in chunks
19 * (`RenderingJob`) as they become available from the simulation loop.
20 * 2. `finalizeCwReceiver`: A one-shot task submitted to the main thread pool
21 * for each continuous-wave receiver at the end of its operation. It processes
22 * the entire collected data buffer at once.
23 *
24 * Both pipelines apply effects like thermal noise, phase noise (jitter),
25 * interference, downsampling, and ADC quantization before writing the final
26 * I/Q data to an HDF5 file.
27 */
28
29#include "finalizer.h"
30
31#include <algorithm>
32#include <chrono>
33#include <cmath>
34#include <format>
35#include <highfive/highfive.hpp>
36#include <ranges>
37#include <tuple>
38
39#include "core/logging.h"
40#include "core/parameters.h"
41#include "core/rendering_job.h"
42#include "core/sim_threading.h"
44#include "radar/receiver.h"
45#include "radar/target.h"
46#include "radar/transmitter.h"
47#include "serial/hdf5_handler.h"
48#include "signal/dsp_filters.h"
50#include "timing/timing.h"
51
52namespace
53{
54 /**
55 * @brief Applies phase noise to a window of complex I/Q samples.
56 * @param noise A span of phase noise samples in radians.
57 * @param window The window of complex I/Q samples to modify.
58 */
59 void addPhaseNoiseToWindow(std::span<const RealType> noise, std::span<ComplexType> window)
60 {
61 for (auto [n, w] : std::views::zip(noise, window))
62 {
63 w *= std::polar(1.0, n);
64 }
65 }
66}
67
68namespace processing
69{
70 void runPulsedFinalizer(radar::Receiver* receiver, const std::vector<std::unique_ptr<radar::Target>>* targets,
71 std::shared_ptr<core::ProgressReporter> reporter)
72 {
73 // Each finalizer thread gets a private, stateful clone of the timing model
74 // to ensure thread safety and independent state progression.
75 const auto timing_model = receiver->getTiming()->clone();
76 if (!timing_model)
77 {
78 LOG(logging::Level::FATAL, "Failed to clone timing model for receiver '{}'", receiver->getName());
79 return;
80 }
81
82 const auto hdf5_filename = std::format("{}_results.h5", receiver->getName());
83 HighFive::File h5_file(hdf5_filename, HighFive::File::Truncate);
84 unsigned chunk_index = 0;
85 LOG(logging::Level::INFO, "Finalizer thread started for receiver '{}'. Outputting to '{}'.",
86 receiver->getName(), hdf5_filename);
87
88 // Throttling state
89 auto last_report_time = std::chrono::steady_clock::now();
90 const auto report_interval = std::chrono::milliseconds(100); // 10 updates/sec max
91
92 while (true)
93 {
95 if (!receiver->waitAndDequeueFinalizerJob(job))
96 {
97 break; // Shutdown signal ("poison pill" job) received.
98 }
99
100 // Process the RenderingJob for one receive window.
102 const RealType dt = 1.0 / rate;
103
104 const auto window_samples = static_cast<unsigned>(std::ceil(job.duration * rate));
105 std::vector pnoise(window_samples, 0.0);
106
107 RealType actual_start = job.ideal_start_time;
108
109 if (timing_model->isEnabled())
110 {
111 // Advance the private clock model to the start of the current window.
112 if (timing_model->getSyncOnPulse())
113 {
114 // For sync-on-pulse models, reset phase and skip to the window start.
115 timing_model->reset();
116 timing_model->skipSamples(static_cast<int>(std::floor(rate * receiver->getWindowSkip())));
117 }
118 else // TODO: should we use (else if chunk_index > 0) here to avoid skipping on the first window?
119 {
120 // For free-running models, skip the "dead time" between windows.
121 const RealType inter_pulse_skip_duration =
122 1.0 / receiver->getWindowPrf() - receiver->getWindowLength();
123 const auto samples_to_skip = static_cast<long>(std::floor(rate * inter_pulse_skip_duration));
124 timing_model->skipSamples(samples_to_skip);
125 }
126
127 // Generate phase noise for the entire window.
128 std::ranges::generate(pnoise, [&] { return timing_model->getNextSample(); });
129
130 // The first phase noise sample determines the time jitter for this window.
131 const RealType carrier = timing_model->getFrequency();
132 actual_start += pnoise[0] / (2.0 * PI * carrier);
133 }
134
135 // Decompose the jittered start time into a sample-aligned start and a
136 // fractional delay, which is passed to the rendering engine.
137 RealType frac_delay;
138 std::tie(actual_start, frac_delay) = [&actual_start, rate]
139 {
140 RealType rounded_start = std::round(actual_start * rate) / rate;
141 RealType fractional_delay = actual_start * rate - std::round(actual_start * rate);
142 return std::tuple{rounded_start, fractional_delay};
143 }();
144
145 // --- Signal Rendering and Processing Pipeline ---
146 std::vector<ComplexType> window_buffer(window_samples);
147
148 // 1. Apply thermal noise.
149 applyThermalNoise(window_buffer, receiver->getNoiseTemperature(receiver->getRotation(actual_start)),
150 receiver->getRngEngine());
151
152 // 2. Add interference from active continuous-wave sources.
153 RealType t_sample = actual_start;
154 for (auto& window_sample : window_buffer)
155 {
156 ComplexType cw_interference_sample{0.0, 0.0};
157 for (const auto* cw_source : job.active_cw_sources)
158 {
159 // TODO: use nodirect?
161 {
162 cw_interference_sample +=
163 simulation::calculateDirectPathContribution(cw_source, receiver, t_sample);
164 }
165 for (const auto& target_ptr : *targets)
166 {
167 cw_interference_sample += simulation::calculateReflectedPathContribution(
168 cw_source, receiver, target_ptr.get(), t_sample);
169 }
170 }
171 window_sample += cw_interference_sample;
172 t_sample += dt;
173 }
174
175 // 3. Render the primary pulsed responses.
176 renderWindow(window_buffer, job.duration, actual_start, frac_delay, job.responses);
177
178 // 4. Apply phase noise (jitter).
179 if (timing_model->isEnabled())
180 {
181 addPhaseNoiseToWindow(pnoise, window_buffer);
182 }
183
184 // --- Finalization and Output ---
185 // 5. Downsample if oversampling was used.
186 if (params::oversampleRatio() > 1)
187 {
188 window_buffer = std::move(fers_signal::downsample(window_buffer));
189 }
190
191 // 6. Quantize and scale to simulate ADC effects.
192 const RealType fullscale = quantizeAndScaleWindow(window_buffer);
193
194 // 7. Write the processed chunk to the HDF5 file.
195 serial::addChunkToFile(h5_file, window_buffer, actual_start, fullscale, chunk_index++);
196
197 // Throttled Reporting: Only acquire mutex and callback if enough time has passed
198 if (reporter)
199 {
200 const auto now = std::chrono::steady_clock::now();
201 if ((now - last_report_time) >= report_interval)
202 {
203 reporter->report(std::format("Exporting {}: Chunk {}", receiver->getName(), chunk_index),
204 static_cast<int>(chunk_index), 0);
205 last_report_time = now;
206 }
207 }
208 }
209
210 if (reporter)
211 {
212 // Always report final status
213 reporter->report(std::format("Finished Exporting {}", receiver->getName()), 100, 100);
214 }
215 LOG(logging::Level::INFO, "Finalizer thread for receiver '{}' finished.", receiver->getName());
216 }
217
219 std::shared_ptr<core::ProgressReporter> reporter)
220 {
221 // CW Finalization only has ~4 major steps, so throttling isn't strictly necessary,
222 // but reporting is added for visibility.
223 LOG(logging::Level::INFO, "Finalization task started for CW receiver '{}'.", receiver->getName());
224 if (reporter)
225 {
226 reporter->report(std::format("Finalizing CW Receiver {}", receiver->getName()), 0, 100);
227 }
228
229 // Process the entire collected I/Q buffer for the CW receiver.
230 auto& iq_buffer = receiver->getMutableCwData();
231 const auto& interference_log = receiver->getPulsedInterferenceLog();
232
233 if (iq_buffer.empty())
234 {
235 LOG(logging::Level::INFO, "No CW data to finalize for receiver '{}'.", receiver->getName());
236 return;
237 }
238
239 if (reporter)
240 {
241 reporter->report(std::format("Rendering Interference for {}", receiver->getName()), 25, 100);
242 }
243
244 // --- Signal Rendering and Processing Pipeline ---
245 // 1. Render pulsed interference signals into the main I/Q buffer.
246 for (const auto& response : interference_log)
247 {
248 unsigned psize;
249 RealType prate;
250 const auto rendered_pulse = response->renderBinary(prate, psize, 0.0);
251
252 const RealType dt_sim = 1.0 / prate;
253 const auto start_index = static_cast<size_t>((response->startTime() - params::startTime()) / dt_sim);
254
255 for (size_t i = 0; i < psize; ++i)
256 {
257 if (start_index + i < iq_buffer.size())
258 {
259 iq_buffer[start_index + i] += rendered_pulse[i];
260 }
261 }
262 }
263
264 // Clone the timing model to ensure thread safety.
265 const auto timing_model = receiver->getTiming()->clone();
266 if (!timing_model)
267 {
268 LOG(logging::Level::FATAL, "Failed to clone timing model for CW receiver '{}'", receiver->getName());
269 return;
270 }
271
272 if (reporter)
273 {
274 reporter->report(std::format("Applying Noise for {}", receiver->getName()), 50, 100);
275 }
276 // 2. Apply thermal noise.
277 applyThermalNoise(iq_buffer, receiver->getNoiseTemperature(), receiver->getRngEngine());
278
279 // 3. Generate and apply a single continuous phase noise sequence.
280 std::vector pnoise(iq_buffer.size(), 0.0);
281 if (timing_model->isEnabled())
282 {
283 std::ranges::generate(pnoise, [&] { return timing_model->getNextSample(); });
284 addPhaseNoiseToWindow(pnoise, iq_buffer);
285 }
286
287 // --- Finalization and Output ---
288 // 4. Downsample if oversampling was used.
289 if (params::oversampleRatio() > 1)
290 {
291 iq_buffer = std::move(fers_signal::downsample(iq_buffer));
292 }
293
294 // 5. Apply ADC quantization and scaling.
295 // TODO: Is there any point in normalizing the full buffer for CW receivers?
296 const RealType fullscale = quantizeAndScaleWindow(iq_buffer);
297
298 if (reporter)
299 {
300 reporter->report(std::format("Writing HDF5 for {}", receiver->getName()), 75, 100);
301 }
302
303 // 6. Write the entire processed buffer to an HDF5 file.
304 const auto hdf5_filename = std::format("{}_results.h5", receiver->getName());
305 try
306 {
307 HighFive::File file(hdf5_filename, HighFive::File::Truncate);
308
309 std::vector<RealType> i_data(iq_buffer.size());
310 std::vector<RealType> q_data(iq_buffer.size());
311 std::ranges::transform(iq_buffer, i_data.begin(), [](const auto& c) { return c.real(); });
312 std::ranges::transform(iq_buffer, q_data.begin(), [](const auto& c) { return c.imag(); });
313
314 HighFive::DataSet i_dataset = file.createDataSet<RealType>("I_data", HighFive::DataSpace::From(i_data));
315 i_dataset.write(i_data);
316 HighFive::DataSet q_dataset = file.createDataSet<RealType>("Q_data", HighFive::DataSpace::From(q_data));
317 q_dataset.write(q_data);
318
319 file.createAttribute("sampling_rate", params::rate());
320 file.createAttribute("start_time", params::startTime());
321 file.createAttribute("fullscale", fullscale);
322 file.createAttribute("reference_carrier_frequency", timing_model->getFrequency());
323
324 LOG(logging::Level::INFO, "Successfully exported CW data for receiver '{}' to '{}'", receiver->getName(),
325 hdf5_filename);
326 }
327 catch (const HighFive::Exception& err)
328 {
329 LOG(logging::Level::FATAL, "Error writing CW data to HDF5 file '{}': {}", hdf5_filename, err.what());
330 }
331
332 if (reporter)
333 {
334 reporter->report(std::format("Finalized {}", receiver->getName()), 100, 100);
335 }
336 }
337}
Header for radar channel propagation and interaction models.
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:68
math::SVec3 getRotation(const RealType time) const
Retrieves the rotation of the object.
Definition object.h:54
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:36
std::mt19937 & getRngEngine() noexcept
Gets the receiver's internal random number generator engine.
Definition receiver.h:144
bool checkFlag(RecvFlag flag) const noexcept
Checks if a specific flag is set.
Definition receiver.h:86
std::vector< ComplexType > & getMutableCwData()
Retrieves the collected CW IQ data for modification.
Definition receiver.h:234
const std::vector< std::unique_ptr< serial::Response > > & getPulsedInterferenceLog() const
Retrieves the log of pulsed interferences for CW mode.
Definition receiver.h:240
RealType getNoiseTemperature() const noexcept
Retrieves the noise temperature of the receiver.
Definition receiver.h:93
RealType getWindowPrf() const noexcept
Retrieves the pulse repetition frequency (PRF) of the radar window.
Definition receiver.h:107
bool waitAndDequeueFinalizerJob(core::RenderingJob &job)
Waits for and dequeues a RenderingJob from the finalizer queue.
Definition receiver.cpp:57
RealType getWindowSkip() const noexcept
Retrieves the window skip time.
Definition receiver.h:114
RealType getWindowLength() const noexcept
Retrieves the radar window length.
Definition receiver.h:100
double RealType
Type for real numbers.
Definition config.h:27
std::complex< RealType > ComplexType
Type for complex numbers.
Definition config.h:35
constexpr RealType PI
Mathematical constant π (pi).
Definition config.h:43
Header file for Digital Signal Processing (DSP) filters and upsampling/downsampling functionality.
Declares the functions for the asynchronous receiver finalization pipelines.
Header file for HDF5 data export and import functions.
Header file for the logging system.
#define LOG(level,...)
Definition logging.h:19
std::vector< ComplexType > downsample(std::span< const ComplexType > in)
Downsamples a signal by a given ratio.
@ FATAL
Fatal level for severe error events.
@ INFO
Info level for informational messages.
RealType rate() noexcept
Get the rendering sample rate.
Definition parameters.h:109
RealType startTime() noexcept
Get the start time for the simulation.
Definition parameters.h:91
unsigned oversampleRatio() noexcept
Get the oversampling ratio.
Definition parameters.h:139
void runPulsedFinalizer(radar::Receiver *receiver, const std::vector< std::unique_ptr< radar::Target > > *targets, std::shared_ptr< core::ProgressReporter > reporter)
The main function for a dedicated pulsed-mode receiver finalizer thread.
Definition finalizer.cpp:70
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 finalizeCwReceiver(radar::Receiver *receiver, pool::ThreadPool *pool, std::shared_ptr< core::ProgressReporter > reporter)
The finalization task for a continuous-wave (CW) mode receiver.
RealType quantizeAndScaleWindow(std::span< ComplexType > window)
Simulates ADC quantization and scales a window of complex 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 addChunkToFile(HighFive::File &file, const std::vector< ComplexType > &data, const RealType time, const RealType fullscale, const unsigned count)
Adds a chunk of data to an HDF5 file.
ComplexType calculateDirectPathContribution(const Transmitter *trans, const Receiver *recv, const RealType timeK)
Calculates the complex envelope contribution for a direct propagation path (Tx -> Rx) at a specific t...
ComplexType calculateReflectedPathContribution(const Transmitter *trans, const Receiver *recv, const Target *targ, const RealType timeK)
Calculates the complex envelope contribution for a reflected path (Tx -> Tgt -> Rx) at a specific tim...
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.
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.
Defines classes for radar targets and their Radar Cross-Section (RCS) models.
Timing source for simulation objects.
Header file for the Transmitter class in the radar namespace.