FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
sim_threading.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 sim_threading.cpp
10 * @brief Implements the core event-driven simulation engine.
11 *
12 * This file contains the primary simulation loop, which orchestrates the entire
13 * simulation process. It operates on a unified, event-driven model capable of
14 * handling both pulsed and continuous-wave (CW) radar systems concurrently.
15 * The loop advances simulation time by processing events from a priority queue.
16 *
17 * A key feature is the time-stepped inner loop that calculates physics for
18 * active CW systems between discrete events. This hybrid approach allows for
19 * efficient simulation of both event-based (pulsed) and continuous (CW) phenomena.
20 * To maintain performance, expensive post-processing tasks are offloaded to
21 * worker threads, decoupling the main physics calculations from I/O and signal
22 * rendering.
23 */
24
25#include "sim_threading.h"
26
27#include <algorithm>
28#include <atomic>
29#include <chrono>
30#include <cmath>
31#include <format>
32#include <functional>
33#include <memory>
34#include <thread>
35#include <utility>
36#include <vector>
37
38#include "logging.h"
39#include "parameters.h"
41#include "radar/receiver.h"
42#include "radar/target.h"
43#include "radar/transmitter.h"
44#include "sim_events.h"
46#include "thread_pool.h"
47#include "world.h"
48
49using logging::Level;
51using radar::Receiver;
52using radar::Target;
54
55namespace core
56{
58 const std::function<void(const std::string&, int, int)>& progress_callback)
59 {
60 auto& event_queue = world->getEventQueue();
61 auto& [t_current, active_cw_transmitters] = world->getSimulationState();
62 const RealType end_time = params::endTime();
63 const RealType dt_sim = 1.0 / (params::rate() * params::oversampleRatio());
64
65 // Create shared thread-safe reporter
66 auto reporter = std::make_shared<ProgressReporter>(progress_callback);
67
68 if (progress_callback)
69 {
70 reporter->report("Initializing event-driven simulation...", 0, 100);
71 }
72
73 // Start dedicated finalizer threads for each pulsed receiver. This creates a
74 // one-thread-per-receiver pipeline for asynchronous data processing.
75 std::vector<std::jthread> finalizer_threads;
76 for (const auto& receiver_ptr : world->getReceivers())
77 {
78 if (receiver_ptr->getMode() == OperationMode::PULSED_MODE)
79 {
80 finalizer_threads.emplace_back(processing::runPulsedFinalizer, receiver_ptr.get(), &world->getTargets(),
81 reporter);
82 }
83 }
84
85 LOG(Level::INFO, "Starting unified event-driven simulation loop.");
86
87 // Throttling state for the main loop
88 auto last_report_time = std::chrono::steady_clock::now();
89 int last_reported_percent = -1;
90 const auto report_interval = std::chrono::milliseconds(100); // Max 10 updates/sec
91
92 // Main Simulation Loop
93 while (!event_queue.empty() && t_current <= end_time)
94 {
95 // Advance Clock to the next scheduled event
96 const auto [timestamp, event_type, source_object] = event_queue.top();
97 event_queue.pop();
98
99 const RealType t_event = timestamp;
100
101 // Before processing the event, run a time-stepped inner loop to calculate
102 // physics for any active continuous-wave systems. This handles the "continuous"
103 // part of the simulation between discrete events.
104 if (t_event > t_current)
105 {
106 const RealType t_next_event = t_event;
107
108 const auto start_index = static_cast<size_t>(std::ceil((t_current - params::startTime()) / dt_sim));
109 const auto end_index = static_cast<size_t>(std::ceil((t_next_event - params::startTime()) / dt_sim));
110
111 for (size_t sample_index = start_index; sample_index < end_index; ++sample_index)
112 {
113 const RealType t_step = params::startTime() + static_cast<RealType>(sample_index) * dt_sim;
114
115 for (const auto& receiver_ptr : world->getReceivers())
116 {
117 if (receiver_ptr->getMode() == OperationMode::CW_MODE && receiver_ptr->isActive())
118 {
119 ComplexType total_sample{0.0, 0.0};
120 // Query active CW sources
121 for (const auto& cw_source : active_cw_transmitters)
122 {
123 // Calculate direct and reflected paths
124 if (!receiver_ptr->checkFlag(Receiver::RecvFlag::FLAG_NODIRECT))
125 {
127 cw_source, receiver_ptr.get(), t_step);
128 }
129 for (const auto& target_ptr : world->getTargets())
130 {
132 cw_source, receiver_ptr.get(), target_ptr.get(), t_step);
133 }
134 }
135 // Store sample in the receiver's main buffer using the correct index
136 receiver_ptr->setCwSample(sample_index, total_sample);
137 }
138 }
139 }
140 }
141
142 t_current = t_event;
143
144 // Process the discrete event
145 switch (event_type)
146 {
148 {
149 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast): Type is guaranteed by event_type
150 auto* tx = static_cast<Transmitter*>(source_object);
151 // For each pulse, calculate its interaction with every receiver and target.
152 for (const auto& rx_ptr : world->getReceivers())
153 {
154 // Calculate unique Response objects for direct and reflected paths.
155 if (!rx_ptr->checkFlag(Receiver::RecvFlag::FLAG_NODIRECT))
156 {
157 if (auto response =
158 simulation::calculateResponse(tx, rx_ptr.get(), tx->getSignal(), t_event))
159 {
160 if (rx_ptr->getMode() == OperationMode::PULSED_MODE)
161 {
162 rx_ptr->addResponseToInbox(std::move(response));
163 }
164 else
165 {
166 rx_ptr->addInterferenceToLog(std::move(response));
167 }
168 }
169 }
170 for (const auto& target_ptr : world->getTargets())
171 {
172 if (auto response = simulation::calculateResponse(tx, rx_ptr.get(), tx->getSignal(),
173 t_event, target_ptr.get()))
174 {
175 if (rx_ptr->getMode() == OperationMode::PULSED_MODE)
176 {
177 rx_ptr->addResponseToInbox(std::move(response));
178 }
179 else
180 {
181 rx_ptr->addInterferenceToLog(std::move(response));
182 }
183 }
184 }
185 }
186 // Schedule next pulse
187 const RealType next_theoretical_time = t_event + 1.0 / tx->getPrf();
188
189 // Use schedule to determine actual next time (handles gaps in schedule)
190 if (const auto next_pulse_opt = tx->getNextPulseTime(next_theoretical_time);
191 next_pulse_opt && *next_pulse_opt <= end_time)
192 {
193 event_queue.push({*next_pulse_opt, EventType::TX_PULSED_START, tx});
194 }
195 break;
196 }
198 {
199 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast): Type is guaranteed by event_type
200 auto* rx = static_cast<Receiver*>(source_object);
201 rx->setActive(true);
202 event_queue.push({t_event + rx->getWindowLength(), EventType::RX_PULSED_WINDOW_END, rx});
203 break;
204 }
206 {
207 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast): Type is guaranteed by event_type
208 auto* rx = static_cast<Receiver*>(source_object);
209 rx->setActive(false);
210 // The receive window is over. Package all received data into a RenderingJob.
211 RenderingJob job{.ideal_start_time = t_event - rx->getWindowLength(),
212 .duration = rx->getWindowLength(),
213 .responses = rx->drainInbox(),
214 .active_cw_sources = active_cw_transmitters};
215 // Offload the job to the dedicated finalizer thread for this receiver.
216 rx->enqueueFinalizerJob(std::move(job));
217
218 // Schedule the start of the next receive window based on schedule
219 const RealType next_theoretical = t_event - rx->getWindowLength() + 1.0 / rx->getWindowPrf();
220
221 if (const auto next_start = rx->getNextWindowTime(next_theoretical);
222 next_start && *next_start <= end_time)
223 {
224 event_queue.push({*next_start, EventType::RX_PULSED_WINDOW_START, rx});
225 }
226 break;
227 }
229 {
230 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast): Type is guaranteed by event_type
231 auto* tx = static_cast<Transmitter*>(source_object);
232 active_cw_transmitters.push_back(tx);
233 break;
234 }
236 {
237 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast): Type is guaranteed by event_type
238 auto* tx = static_cast<Transmitter*>(source_object);
239 std::erase(active_cw_transmitters, tx);
240 break;
241 }
243 {
244 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast): Type is guaranteed by event_type
245 auto* rx = static_cast<Receiver*>(source_object);
246 rx->setActive(true);
247 break;
248 }
250 {
251 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-static-cast-downcast): Type is guaranteed by event_type
252 auto* rx = static_cast<Receiver*>(source_object);
253 rx->setActive(false);
254 // Do NOT finalize here. CW data is a continuous buffer; 'Ending' a schedule period
255 // just means we stop recording active signal (leaving zeros/silence in the buffer).
256 break;
257 }
258 }
259
260 // Throttled Progress Reporting
261 if (progress_callback)
262 {
263 const int progress = static_cast<int>(t_current / end_time * 100.0);
264
265 // Only report if percentage changed significantly OR enough time elapsed
266 if (const auto now = std::chrono::steady_clock::now();
267 progress != last_reported_percent || now - last_report_time >= report_interval)
268 {
269 reporter->report(std::format("Simulating... {:.2f}s / {:.2f}s", t_current, end_time), progress,
270 100);
271 last_reported_percent = progress;
272 last_report_time = now;
273 }
274 }
275 }
276
277 // Shutdown Phase
278 LOG(Level::INFO, "Main simulation loop finished. Waiting for finalization tasks...");
279 reporter->report("Main simulation finished. Waiting for data export...", 100, 100);
280
281 // 1. Queue CW Finalization Tasks
282 // We finalize CW receivers here to ensure the full timeline (including all schedule periods) is exported once.
283 for (const auto& receiver_ptr : world->getReceivers())
284 {
285 if (receiver_ptr->getMode() == OperationMode::CW_MODE)
286 {
287 pool.enqueue(processing::finalizeCwReceiver, receiver_ptr.get(), &pool, reporter);
288 }
289 }
290
291 // 2. Signal pulsed finalizer threads to shut down
292 for (const auto& receiver_ptr : world->getReceivers())
293 {
294 if (receiver_ptr->getMode() == OperationMode::PULSED_MODE)
295 {
296 RenderingJob shutdown_job{.duration = -1.0};
297 receiver_ptr->enqueueFinalizerJob(std::move(shutdown_job));
298 }
299 }
300
301 // Wait for any remaining CW finalization tasks in the main pool to complete.
302 pool.wait();
303
304 // The std::jthread finalizer threads are automatically joined here at the end of scope.
305 LOG(Level::INFO, "All finalization tasks complete.");
306
307 if (progress_callback)
308 {
309 reporter->report("Simulation complete", 100, 100);
310 }
311 LOG(Level::INFO, "Event-driven simulation loop finished.");
312 }
313}
Header for radar channel propagation and interaction models.
The World class manages the simulator environment.
Definition world.h:38
const std::vector< std::unique_ptr< radar::Target > > & getTargets() const noexcept
Retrieves the list of radar targets.
Definition world.h:143
SimulationState & getSimulationState() noexcept
Gets a mutable reference to the global simulation state.
Definition world.h:228
std::priority_queue< Event, std::vector< Event >, EventComparator > & getEventQueue() noexcept
Gets a mutable reference to the global event queue.
Definition world.h:219
const std::vector< std::unique_ptr< radar::Receiver > > & getReceivers() const noexcept
Retrieves the list of radar receivers.
Definition world.h:153
A simple thread pool implementation.
Definition thread_pool.h:29
Manages radar signal reception and response processing.
Definition receiver.h:36
void setActive(const bool active) noexcept
Sets the active state of the receiver.
Definition receiver.h:163
Base class for radar targets.
Definition target.h:117
Represents a radar transmitter system.
Definition transmitter.h:32
RealType getPrf() const noexcept
Retrieves the pulse repetition frequency (PRF).
Definition transmitter.h:61
double RealType
Type for real numbers.
Definition config.h:27
std::complex< RealType > ComplexType
Type for complex numbers.
Definition config.h:35
Declares the functions for the asynchronous receiver finalization pipelines.
Header file for the logging system.
#define LOG(level,...)
Definition logging.h:19
@ RX_PULSED_WINDOW_START
A pulsed receiver opens its listening window.
@ RX_PULSED_WINDOW_END
A pulsed receiver closes its listening window.
@ TX_CW_START
A continuous-wave transmitter starts transmitting.
@ TX_CW_END
A continuous-wave transmitter stops transmitting.
@ TX_PULSED_START
A pulsed transmitter begins emitting a pulse.
@ RX_CW_END
A continuous-wave receiver stops listening.
@ RX_CW_START
A continuous-wave receiver starts listening.
void runEventDrivenSim(World *world, pool::ThreadPool &pool, const std::function< void(const std::string &, int, int)> &progress_callback)
Runs the unified, event-driven radar simulation.
RealType endTime() noexcept
Get the end time for the simulation.
Definition parameters.h:97
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 finalizeCwReceiver(radar::Receiver *receiver, pool::ThreadPool *pool, std::shared_ptr< core::ProgressReporter > reporter)
The finalization task for a continuous-wave (CW) mode receiver.
OperationMode
Defines the operational mode of a radar component.
Definition radar_obj.h:36
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...
std::unique_ptr< serial::Response > calculateResponse(const Transmitter *trans, const Receiver *recv, const RadarSignal *signal, const RealType startTime, const Target *targ)
Creates a Response object by simulating a signal's interaction over its duration.
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 core structures for the event-driven simulation engine.
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.
RealType ideal_start_time
The ideal, jitter-free start time of the receive window.
Defines classes for radar targets and their Radar Cross-Section (RCS) models.
A simple thread pool implementation.
Header file for the Transmitter class in the radar namespace.
Header file for the World class in the simulator.