FERS 1.0.0
The Flexible Extensible Radar Simulator
Loading...
Searching...
No Matches
processing Namespace Reference

Functions

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.
 
void finalizeCwReceiver (radar::Receiver *receiver, pool::ThreadPool *pool, std::shared_ptr< core::ProgressReporter > reporter)
 The finalization task for a continuous-wave (CW) mode receiver.
 
void renderWindow (std::vector< ComplexType > &window, RealType length, RealType start, RealType fracDelay, 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 applyThermalNoise (std::span< ComplexType > window, RealType noiseTemperature, std::mt19937 &rngEngine)
 Applies thermal (Johnson-Nyquist) noise to a window of I/Q samples.
 
RealType quantizeAndScaleWindow (std::span< ComplexType > window)
 Simulates ADC quantization and scales a window of complex I/Q samples.
 

Function Documentation

◆ applyThermalNoise()

void processing::applyThermalNoise ( std::span< ComplexType window,
RealType  noiseTemperature,
std::mt19937 &  rngEngine 
)

Applies thermal (Johnson-Nyquist) noise to a window of I/Q samples.

Simulates the addition of white Gaussian noise based on the receiver's noise temperature and the simulation bandwidth.

Parameters
windowA span of I/Q data to which noise will be added.
noiseTemperatureThe effective noise temperature in Kelvin.
rngEngineA random number generator engine to use for noise generation.

Definition at line 113 of file signal_processor.cpp.

114 {
115 if (noiseTemperature == 0)
116 {
117 return;
118 }
119
120 const RealType b = params::rate() / (2.0 * params::oversampleRatio());
121 const RealType total_power = params::boltzmannK() * noiseTemperature * b;
122
123 // Split total power equally between the I and Q channels
124 const RealType per_channel_power = total_power / 2.0;
125 const RealType stddev = std::sqrt(per_channel_power);
126
127 noise::WgnGenerator generator(rngEngine, stddev);
128 for (auto& sample : window)
129 {
130 sample += ComplexType(generator.getSample(), generator.getSample());
131 }
132 }
Generates white Gaussian noise.
double RealType
Type for real numbers.
Definition config.h:27
std::complex< RealType > ComplexType
Type for complex numbers.
Definition config.h:35
RealType rate() noexcept
Get the rendering sample rate.
Definition parameters.h:109
RealType boltzmannK() noexcept
Get the Boltzmann constant.
Definition parameters.h:85
unsigned oversampleRatio() noexcept
Get the oversampling ratio.
Definition parameters.h:139

References params::boltzmannK(), noise::WgnGenerator::getSample(), params::oversampleRatio(), and params::rate().

Referenced by finalizeCwReceiver(), and runPulsedFinalizer().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ finalizeCwReceiver()

void processing::finalizeCwReceiver ( radar::Receiver receiver,
pool::ThreadPool pool,
std::shared_ptr< core::ProgressReporter reporter 
)

The finalization task for a continuous-wave (CW) mode receiver.

This function is submitted to the main thread pool when a CW receiver finishes its operation. It processes the entire collected I/Q buffer, applies interference and noise, and writes the final data to a file.

Parameters
receiverA pointer to the CW-mode receiver to finalize.
poolA pointer to the main thread pool for parallelizing sub-tasks.
reporterShared pointer to the progress reporter for status updates.

Definition at line 218 of file finalizer.cpp.

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 }
const std::string & getName() const noexcept
Retrieves the name of the object.
Definition object.h:68
std::shared_ptr< timing::Timing > getTiming() const
Retrieves the timing source for the radar.
Definition radar_obj.cpp:66
std::mt19937 & getRngEngine() noexcept
Gets the receiver's internal random number generator engine.
Definition receiver.h:144
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
#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 startTime() noexcept
Get the start time for the simulation.
Definition parameters.h:91
void applyThermalNoise(std::span< ComplexType > window, const RealType noiseTemperature, std::mt19937 &rngEngine)
Applies thermal (Johnson-Nyquist) noise to a window of I/Q samples.
RealType quantizeAndScaleWindow(std::span< ComplexType > window)
Simulates ADC quantization and scales a window of complex I/Q samples.

References applyThermalNoise(), fers_signal::downsample(), logging::FATAL, radar::Receiver::getMutableCwData(), radar::Object::getName(), radar::Receiver::getNoiseTemperature(), radar::Receiver::getPulsedInterferenceLog(), radar::Receiver::getRngEngine(), radar::Radar::getTiming(), logging::INFO, LOG, params::oversampleRatio(), quantizeAndScaleWindow(), params::rate(), and params::startTime().

Referenced by core::runEventDrivenSim().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ quantizeAndScaleWindow()

RealType processing::quantizeAndScaleWindow ( std::span< ComplexType window)

Simulates ADC quantization and scales a window of complex I/Q samples.

This function first finds the maximum absolute value in the I/Q data to determine the full-scale range. It then simulates the quantization process based on the configured number of ADC bits. If no quantization is specified (adc_bits=0), it normalizes the data to a maximum amplitude of 1.0.

Parameters
windowThe window of complex I/Q samples to quantize and scale.
Returns
The full-scale value used for quantization/normalization.

Definition at line 134 of file signal_processor.cpp.

135 {
136 RealType max_value = 0;
137 for (const auto& sample : window)
138 {
139 const RealType real_abs = std::fabs(sample.real());
140 const RealType imag_abs = std::fabs(sample.imag());
141
142 max_value = std::max({max_value, real_abs, imag_abs});
143 }
144
145 if (const auto adc_bits = params::adcBits(); adc_bits > 0)
146 {
147 adcSimulate(window, adc_bits, max_value);
148 }
149 else if (max_value != 0)
150 {
151 for (auto& sample : window)
152 {
153 sample /= max_value;
154 }
155 }
156 return max_value;
157 }
unsigned adcBits() noexcept
Get the ADC quantization bits.
Definition parameters.h:121

References params::adcBits().

Referenced by finalizeCwReceiver(), and runPulsedFinalizer().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ renderWindow()

void processing::renderWindow ( std::vector< ComplexType > &  window,
RealType  length,
RealType  start,
RealType  fracDelay,
std::span< const std::unique_ptr< serial::Response > >  responses 
)

Renders a time-window of I/Q data from a collection of raw radar responses.

This function orchestrates the process of converting abstract Response objects into a concrete vector of complex I/Q samples for a specific time window. It handles the superposition of multiple signals arriving at the receiver during the window and can use a thread pool for parallel processing.

Parameters
windowThe output vector where the rendered I/Q samples will be added.
lengthThe duration of the time window in seconds.
startThe start time of the window in seconds.
fracDelayA fractional sample delay to apply for fine-grained timing.
responsesA span of unique pointers to the Response objects to be rendered.

Definition at line 81 of file signal_processor.cpp.

83 {
84 const RealType end = start + length;
85 std::queue<serial::Response*> work_list;
86
87 for (const auto& response : responses)
88 {
89 if (response->startTime() <= end && response->endTime() >= start)
90 {
91 work_list.push(response.get());
92 }
93 }
94
96 const auto local_window_size = static_cast<unsigned>(std::ceil(length * rate));
97
98 std::vector local_window(local_window_size, ComplexType{});
99
100 while (!work_list.empty())
101 {
102 const auto* resp = work_list.front();
103 work_list.pop();
104 processResponse(resp, local_window, rate, start, fracDelay, local_window_size);
105 }
106
107 for (unsigned i = 0; i < local_window_size; ++i)
108 {
109 window[i] += local_window[i];
110 }
111 }

References params::oversampleRatio(), and params::rate().

Referenced by runPulsedFinalizer().

+ Here is the call graph for this function:
+ Here is the caller graph for this function:

◆ runPulsedFinalizer()

void processing::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.

This function runs in a loop, dequeuing and processing RenderingJobs for a specific receiver. It handles all expensive rendering, signal processing, and I/O for that receiver's data.

Parameters
receiverA pointer to the pulsed-mode receiver to process.
targetsA pointer to the world's list of targets for interference calculation.
reporterShared pointer to the progress reporter for status updates.

Definition at line 70 of file finalizer.cpp.

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 }
math::SVec3 getRotation(const RealType time) const
Retrieves the rotation of the object.
Definition object.h:54
bool checkFlag(RecvFlag flag) const noexcept
Checks if a specific flag is set.
Definition receiver.h:86
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
constexpr RealType PI
Mathematical constant π (pi).
Definition config.h:43
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...
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.

References core::RenderingJob::active_cw_sources, serial::addChunkToFile(), applyThermalNoise(), simulation::calculateDirectPathContribution(), simulation::calculateReflectedPathContribution(), radar::Receiver::checkFlag(), fers_signal::downsample(), core::RenderingJob::duration, logging::FATAL, radar::Receiver::FLAG_NODIRECT, radar::Object::getName(), radar::Receiver::getNoiseTemperature(), radar::Receiver::getRngEngine(), radar::Object::getRotation(), radar::Radar::getTiming(), radar::Receiver::getWindowLength(), radar::Receiver::getWindowPrf(), radar::Receiver::getWindowSkip(), core::RenderingJob::ideal_start_time, logging::INFO, LOG, params::oversampleRatio(), PI, quantizeAndScaleWindow(), params::rate(), renderWindow(), core::RenderingJob::responses, and radar::Receiver::waitAndDequeueFinalizerJob().

Referenced by core::runEventDrivenSim().

+ Here is the call graph for this function:
+ Here is the caller graph for this function: